/** Registers the conversation components, shared store, and service callbacks. */
import type { Context } from 'cordis'
import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
import type { ISessions, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
import type { ViewTab } from './contract/views.ts'
import type {
  ChatViewInjected, ComposerBarInjected, ConversationInjected, ConversationSessionInjected, DetailsInjected,
} from './contract/slots.ts'
import { resolveToolPath } from './contract/tool-call-model.ts'
import { createChatStore } from './stores.ts'
import { ConversationService } from './service.ts'
import type { IConversation } from './service.ts'
import { InputHub } from './input/hub.ts'
import { InputBar } from './skeleton/InputBar.tsx'
import { ChatView } from './chat/ChatView.tsx'
import { bashToolviewSample } from './toolviews/bash-sample.tsx'
import { todoToolview } from './toolviews/todo-row.tsx'
import { todoDockEntry } from './skeleton/TodoPanel.tsx'
import { queueDockEntry } from './queue/QueueDock.tsx'
import { ConversationRoot } from './skeleton/ConversationRoot.tsx'
import { ConversationSession } from './skeleton/ConversationSession.tsx'
import { DetailsPanel } from './skeleton/DetailsPanel.tsx'

/** Services required by the conversation plugin. */
export const inject = ['slots', 'layout', 'sessions', 'workspaces']

/** Resolve the session-scoped conversation face (scope-addressed send/cancel), failing loud. */
function scopedConversation(sessions: ISessions, id: SessionId): IConversation {
  const scoped = sessions.scope(id)
  if (scoped === undefined) throw new Error(`ui-conversation: session "${id}" resolved no scope`)
  const conversation = scoped.get('conversation')
  if (conversation === undefined) throw new Error('ui-conversation: conversation service unavailable through the session scope')
  return conversation
}

/** Mounts the conversation plugin.
 * @param ctx - Client root context.
 */
export function apply(ctx: Context): void {
  const sessions = ctx.sessions
  const workspaces = ctx.workspaces
  const layout = ctx.layout
  const slots = ctx.slots

  // Apply-time construction keeps store identity bound to this fiber.
  const chatStore = createChatStore()

  const viewTabs = (): ViewTab[] => {
    const tabs: ViewTab[] = []
    for (const entry of slots.entries('conversation.view')) {
      /* v8 ignore next -- unreachable: list registration validates id at load. */
      if (entry.options.id === undefined) continue
      tabs.push({ id: entry.options.id, label: entry.options.label ?? entry.options.id })
    }
    return tabs
  }

  // The per-session input machine registry (InputService face; published as
  // ctx.conversation.input by the service below sharing this one instance).
  const inputHub = new InputHub(ctx)

  // Decision 19/20: the input machine feeds every session-scope slot
  // component through the standard provide channel — the 'input' hook plus
  // the two public actions. Materialization is the shell creation trigger
  // (per-session lazy; scope disposer tears down).
  ctx.effect(() => sessions.provide({
    hooks: ['input'],
    props: ['inputActions'],
    resolve: (binding) => {
      const shell = inputHub.shellFor(binding)
      return {
        hooks: { input: shell.state },
        props: { inputActions: shell.actions },
      }
    },
  }), 'ui-conversation: input standard-kit provider')

  // Resident current-session-optional shell. It owns the stable Hero/composer
  // frame while strict session slots fill only their session-bound regions.
  slots.register({
    name: 'conversation',
    children: {
      'conversation.session': { kind: 'single', scope: 'session' },
      'conversation.composer': { kind: 'chain', scope: 'session' },
      'conversation.composer.bar': { kind: 'single', scope: 'session' },
      'conversation.input.overlay': { kind: 'list', scope: 'session' },
      'conversation.input.dock': { kind: 'list', scope: 'session' },
      'conversation.composer.dock': { kind: 'list', scope: 'session' },
      'conversation.input.left': { kind: 'list', scope: 'session' },
      'conversation.input.right': { kind: 'list', scope: 'session' },
      'conversation.hero.workspace': { kind: 'single', scope: 'root' },
    },
    inject: (sessionId: SessionId | undefined): ConversationInjected => ({
      selectWorkspace: async (workspaceId) => {
        const nextId = await workspaces.connectWorkspace(workspaceId)
        if (sessionId !== undefined && nextId !== sessionId) {
          const from = inputHub.shell(sessionId)
          const draft = from.snapshot.draft
          if (draft !== '') {
            inputHub.shell(nextId).setDraft(draft)
            from.setDraft('')
          }
        }
        sessions.open(nextId)
      },
    }),
  }, ConversationRoot)

  // The strict session subtree owns only per-session store and view content;
  // the resident parent keeps Hero and composer layout identity stable.
  slots.register({
    name: 'conversation.session',
    children: { 'conversation.view': { kind: 'list', scope: 'session' } },
    store: chatStore,
    inject: (sessionId: SessionId, _actions: BoundActions<typeof chatStore>): ConversationSessionInjected => ({
      views: {
        list: viewTabs,
        subscribe: fn => slots.subscribe('conversation.view', fn),
        version: () => slots.getVersion('conversation.view'),
      },
      bindDraftMirror: write => inputHub.shell(sessionId).bindMirror(write),
      open: (id) => { sessions.open(id) },
    }),
  }, ConversationSession)

  // The default composer body: its own single slot inside the composer
  // chain's fallback (decision 20). Public machine surface arrives via the
  // provide channel above; the keyboard command face and the stop/retry
  // verbs ride this inject (package-internal — hub and bar are one plugin).
  slots.register({
    name: 'conversation.composer.bar',
    // The two named control seats in the bar's tool row (plan beside the
    // access control, model right); empty until their owning plugins
    // register (B ruling).
    children: {
      'conversation.input.plan': { kind: 'single', scope: 'session' },
      'conversation.input.model': { kind: 'single', scope: 'session' },
    },
    inject: (sessionId: SessionId): ComposerBarInjected => {
      const shell = inputHub.shell(sessionId)
      return {
        keyboard: shell,
        stop: () => {
          scopedConversation(sessions, sessionId).cancel().catch(() => {
            // Stop failure surfaces via snapshot.promptError; nothing to restore.
          })
        },
        hooks: { notices: shell.notices, lexicon: shell.lexicon },
      }
    },
  }, InputBar)

  // The chat view: first entry of the ring this package just declared.
  // Declaring the keyed toolview hole here is claiming it: ChatView is the
  // only component authorized to render per-tool rows. Shares the chat
  // store, so its selection writes land in the same per-session instance the
  // details panel reads.
  slots.register({
    name: 'conversation.view',
    id: 'chat',
    order: 0,
    label: 'Chat',
    children: {
      'conversation.chat.toolview': { kind: 'keyed', scope: 'session' },
      'conversation.chat.commandview': { kind: 'keyed', scope: 'session' },
    },
    store: chatStore,
    inject: (sessionId: SessionId, actions: BoundActions<typeof chatStore>): ChatViewInjected => {
      const scoped = scopedConversation(sessions, sessionId)
      return {
        openDetails: (target) => {
          actions.select(target)
          layout.openDetails()
        },
        openFile: (path) => {
          const cwd = sessions.list.getSnapshot().byId[sessionId]?.cwd
          void workspaces.openPath(resolveToolPath(cwd, path)).catch(() => {
            // Host/OS open failures stay silent in the chat row; the native
            // app surfaces its own error dialog when the path is unusable.
          })
        },
        loadOlder: () => { void scoped.loadOlder() },
      }
    },
  }, ChatView)

  // Class-plugin mount (packages/AGENTS.md service form): the service
  // registers itself as `conversation` and lives on its own child fiber.
  // Mounted AFTER the chat entry register above — construction guarantee for
  // toolview registrants using `inject: ['conversation']` as their load-order
  // seam: the service being present implies the chat entry (and with it the
  // 'conversation.chat.toolview' declaration) is on the ledger.
  ctx.plugin(ConversationService, { input: inputHub })

  // The bash sample rides that exact seam, in third-party posture
  // (ToolRow-matching Bash · {description} chrome; scoped badge in child sessions).
  ctx.plugin(bashToolviewSample)

  // The todo_write row rides the same seam (a product registration, not a sample).
  ctx.plugin(todoToolview)

  // The plan strip rides the input dock above the queue rows (same posture).
  ctx.plugin(todoDockEntry)

  // The read-only queue dock entry (T9 file territory) rides the same
  // registration seam into the input dock declared above.
  ctx.plugin(queueDockEntry)

  slots.register({
    name: 'details',
    store: chatStore,
    inject: (): DetailsInjected => ({
      closeDetails: () => { layout.closeDetails() },
    }),
  }, DetailsPanel)

}
