import type { CreateHostContextOptions } from 'devframe/node'
import type { DevframeHost, DevframeNodeContext } from 'devframe/types'
import type { DevframeCommandsHost } from '../types/commands'
import type { DevframeDockActivation, DevframeDocksActiveState, DevframeDocksHost } from '../types/docks'
import type { JsonRenderer, JsonRenderSpec } from '../types/json-render'
import type { DevframeMessageEntry, DevframeMessageEntryInput, DevframeMessagesHost } from '../types/messages'
import type { DevframeTerminalsHost } from '../types/terminals'
import { createHostContext } from 'devframe/node'
import { debounce } from 'perfect-debounce'
import { DevframeCommandsHost as CommandsHostImpl } from './host-commands'
import { DevframeDocksHost as DocksHostImpl } from './host-docks'
import { DevframeMessagesHost as MessagesHostImpl } from './host-messages'
import { DevframeTerminalsHost as TerminalsHostImpl } from './host-terminals'
import { builtinHubRpcDeclarations } from './rpc-builtins'

declare module 'devframe/types' {
  interface DevframeRpcClientFunctions {
    /**
     * Server→client request to switch the active dock. Broadcast by the hub
     * context in response to `ctx.docks.activate()` (driven by the
     * `hub:docks:activate` RPC). The client host registers a handler that
     * calls its local `switchEntry(dockId)`; the target dock reads
     * `activation.params` to react (e.g. focus a session). Do not register
     * manually.
     *
     * @internal
     */
    'devframe:docks:activate': (activation: DevframeDockActivation) => Promise<void>
    /**
     * Server→client notification that terminal sessions changed. Broadcast
     * by the hub context; a hub-aware client re-reads terminal state in
     * response. Do not register manually.
     *
     * @internal
     */
    'devframe:terminals:updated': () => Promise<void>
    /**
     * Server→client notification that the message list changed. Broadcast
     * by the hub context; a hub-aware client re-reads message state in
     * response. Do not register manually.
     *
     * @internal
     */
    'devframe:messages:updated': () => Promise<void>
  }

  interface DevframeRpcServerFunctions {
    /**
     * Ask the active viewer to switch its focused dock to `dockId`, optionally
     * carrying `params` for the target dock to interpret (e.g.
     * `{ sessionId }` for the terminals dock). Any connected client may call
     * it — a mounted devframe in its own iframe steers the host shell's dock
     * selection. Handled by {@link import('./rpc-builtins').hubDocksActivate}.
     */
    'hub:docks:activate': (input: { dockId: string, params?: Record<string, unknown> }) => Promise<void>
    /**
     * Invoke a registered server command by id; trailing args are forwarded to
     * the command's handler. Handled by
     * {@link import('./rpc-builtins').hubCommandsExecute}.
     */
    'hub:commands:execute': (id: string, ...args: any[]) => Promise<unknown>
    /**
     * Add a message from a browser client into the hub's messages feed
     * (marked `from: 'browser'`); returns the serializable entry. Handled by
     * {@link import('./rpc-builtins').hubMessagesAdd}.
     */
    'hub:messages:add': (input: DevframeMessageEntryInput) => Promise<DevframeMessageEntry>
    /** Patch a message by id; resolves the updated entry (or `undefined`). */
    'hub:messages:update': (id: string, patch: Partial<DevframeMessageEntryInput>) => Promise<DevframeMessageEntry | undefined>
    /** Remove a message by id. */
    'hub:messages:remove': (id: string) => Promise<void>
    /** Remove every message. */
    'hub:messages:clear': () => Promise<void>
    /**
     * Send input to an interactive PTY session spawned via
     * `ctx.terminals.startPtySession`. Handled by
     * {@link import('./rpc-builtins').hubTerminalsWrite}.
     */
    'hub:terminals:write': (id: string, data: string) => Promise<void>
    /** Resize an interactive PTY session by id. */
    'hub:terminals:resize': (id: string, cols: number, rows: number) => Promise<void>
  }
}

/**
 * Hub-augmented node context — extends devframe's framework-neutral
 * `DevframeNodeContext` with the hub-level subsystems (`docks`,
 * `terminals`, `messages`, `commands`) and the deprecated
 * `createJsonRenderer` compatibility factory.
 *
 * Framework kits further extend this with their own slots (e.g.
 * `viteConfig`, `viteServer`). Host-specific capabilities (editor open,
 * filesystem reveal, etc.) ship as kit-registered RPC functions rather
 * than as part of this surface. JSON-render itself is not part of the hub:
 * it is an opt-in integration (`@devframes/json-render`) that augments any
 * devframe context and contributes its own dock type — prefer
 * `createJsonRenderView` from `@devframes/json-render/node` over the
 * deprecated factory below.
 */
export interface DevframeHubContext extends DevframeNodeContext {
  readonly host: DevframeHost
  docks: DevframeDocksHost
  terminals: DevframeTerminalsHost
  messages: DevframeMessagesHost
  commands: DevframeCommandsHost
  /**
   * Create a `JsonRenderer` handle for building json-render powered UIs.
   *
   * @deprecated json-render moved out of the hub into the opt-in
   * `@devframes/json-render` integration in 0.7. This factory is kept
   * working (not just type-compatible) for the 0.7 series so existing call
   * sites don't break — use `createJsonRenderView(ctx, { id, spec })` from
   * `@devframes/json-render/node` instead. Will be removed in 0.8.
   */
  createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer
}

/**
 * Options for {@link createHubContext} — devframe's
 * {@link CreateHostContextOptions} plus any hub-level additions kits layer on
 * through declaration merging.
 */
export interface CreateHubContextOptions extends CreateHostContextOptions {}

/**
 * Create a hub-level node context: wraps devframe's `createHostContext`,
 * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`),
 * registers the hub's built-in RPC commands, and wires the shared-state
 * synchronization that powers a hub-aware client UI.
 */
export async function createHubContext(options: CreateHubContextOptions): Promise<DevframeHubContext> {
  const baseContext = await createHostContext({
    ...options,
    builtinRpcDeclarations: [
      ...builtinHubRpcDeclarations,
      ...(options.builtinRpcDeclarations ?? []),
    ],
  })
  const context = baseContext as DevframeHubContext

  const docks = new DocksHostImpl(context)
  const terminals = new TerminalsHostImpl(context)
  const messages = new MessagesHostImpl(context)
  const commands = new CommandsHostImpl(context)

  context.docks = docks
  context.terminals = terminals
  context.messages = messages
  context.commands = commands

  await docks.init()

  // Deprecated pre-0.7 compatibility factory — restored as a working (not
  // merely type-compatible) shim so 0.7 only *deprecates* it rather than
  // breaking it outright; see the `createJsonRenderer` JSDoc above. Kept
  // self-contained against `context.rpc.sharedState` (as it was pre-0.7)
  // rather than delegating to `@devframes/json-render`'s
  // `createJsonRenderView`, so the hub still carries no dependency — direct
  // or peer — on that opt-in package. Removed in 0.8: migrate call sites to
  // `createJsonRenderView` before then.
  let jsonRenderCounter = 0
  context.createJsonRenderer = (initialSpec: JsonRenderSpec): JsonRenderer => {
    const stateKey = `devframe:json-render:${jsonRenderCounter++}`
    const statePromise = context.rpc.sharedState.get(stateKey as any, {
      initialValue: initialSpec as any,
    })

    return {
      _stateKey: stateKey,
      async updateSpec(spec) {
        const state = await statePromise
        state.mutate(() => spec as any)
      },
      async updateState(newState) {
        const state = await statePromise
        state.mutate((draft: any) => {
          draft.state = { ...draft.state, ...newState }
        })
      },
    }
  }

  const debounceMs = options.mode === 'build' ? 0 : 10

  const docksSharedState = await context.rpc.sharedState.get('devframe:docks', { initialValue: [] })
  const refreshDocks = debounce(() => {
    docksSharedState.mutate(() => docks.values())
  }, debounceMs)
  docks.events.on('dock:entry:updated', refreshDocks)
  docksSharedState.mutate(() => docks.values())

  // Cross-iframe dock activation. A dock activation is a discrete user intent
  // ("go to Terminals now"), so it fires immediately (no debounce, which could
  // coalesce two distinct requests) both as a live broadcast — the host shell
  // switches its active dock — and into a shared-state slot, so a dock that
  // only mounts *because* of the switch still converges on the request.
  const activeDockSharedState = await context.rpc.sharedState.get<DevframeDocksActiveState>(
    'devframe:docks:active',
    { initialValue: { activation: null } },
  )
  docks.events.on('dock:activate', (activation) => {
    activeDockSharedState.mutate((state) => {
      state.activation = activation
    })
    context.rpc.broadcast({
      method: 'devframe:docks:activate',
      args: [activation],
    })
  })

  const broadcastTerminals = debounce(() => {
    context.rpc.broadcast({
      method: 'devframe:terminals:updated',
      args: [],
    })
    docksSharedState.mutate(() => docks.values())
  }, debounceMs)
  terminals.events.on('terminal:session:updated', broadcastTerminals)

  const broadcastMessages = debounce(() => {
    context.rpc.broadcast({
      method: 'devframe:messages:updated',
      args: [],
    })
    docksSharedState.mutate(() => docks.values())
  }, debounceMs)
  messages.events.on('message:added', broadcastMessages)
  messages.events.on('message:updated', broadcastMessages)
  messages.events.on('message:removed', broadcastMessages)
  messages.events.on('message:cleared', broadcastMessages)

  const commandsSharedState = await context.rpc.sharedState.get('devframe:commands', { initialValue: [] })
  const syncCommands = debounce(() => {
    commandsSharedState.mutate(() => commands.list())
  }, debounceMs)
  commands.events.on('command:registered', syncCommands)
  commands.events.on('command:unregistered', syncCommands)

  commandsSharedState.mutate(() => commands.list())

  return context
}
