/** @jsxImportSource @opentui/solid */
/**
 * TuiApp - OpenTUI/Solid application shell
 * DD §13.2. Projection-only display plus single OpenTUI textarea input owner.
 *
 * @module packages/tui/src/TuiApp
 */

import { createCliRenderer, type CliRenderer, type TextareaRenderable, type KeyEvent } from '@opentui/core'
import { render, useRenderer, useTerminalDimensions } from '@opentui/solid'
import { createEffect, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
import type { SessionProjection } from './types.js'

export interface TuiAppProps {
  client: {
    subscribe(handler: (projection: SessionProjection) => void): () => void
    get_snapshot(): SessionProjection | null
  }
  onSubmit?: (input: string) => void | Promise<void>
  onSlashCommand?: (input: string) => void | Promise<void>
  onResolvePermission?: (prompt_id: string, selected_option: string) => void | Promise<void>
  onExit?: () => void | Promise<void>
}

export interface TuiAppState {
  projection: SessionProjection | null
  active_view: 'tasks' | 'agents' | 'tools' | 'diff' | 'help'
  busy: boolean
  status: string
}

const THEME = {
  bg: '#0b0f14',
  surface: '#111827',
  surface2: '#1f2937',
  text: '#e5e7eb',
  muted: '#9ca3af',
  faint: '#6b7280',
  accent: '#22d3ee',
  success: '#22c55e',
  warning: '#f59e0b',
  error: '#ef4444',
  border: '#374151',
}

const PROMPT_HISTORY_LIMIT = 200
const TEXTAREA_MIN_ROWS = 1
const TEXTAREA_MAX_ROWS = 6
const EXIT_CONFIRM_MS = 5000

type PromptHistoryState = {
  items: string[]
  index: number | null
  draft: string
}

type PromptHistoryMove = {
  state: PromptHistoryState
  apply: boolean
  text?: string
  cursor?: number
}

function createPromptHistory(): PromptHistoryState {
  return { items: [], index: null, draft: '' }
}

function pushPromptHistory(state: PromptHistoryState, prompt: string): PromptHistoryState {
  const text = prompt.trim()
  if (!text) return state
  if (state.items[state.items.length - 1] === text) {
    return { ...state, index: null, draft: '' }
  }
  return { items: [...state.items, text].slice(-PROMPT_HISTORY_LIMIT), index: null, draft: '' }
}

function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text: string, cursor: number): PromptHistoryMove {
  if (state.items.length === 0) return { state, apply: false }
  if (dir === -1 && cursor !== 0) return { state, apply: false }
  if (dir === 1 && cursor !== text.length) return { state, apply: false }

  if (state.index === null) {
    if (dir === 1) return { state, apply: false }
    const idx = state.items.length - 1
    return { state: { ...state, index: idx, draft: text }, text: state.items[idx], cursor: 0, apply: true }
  }

  const idx = state.index + dir
  if (idx < 0) return { state, apply: false }
  if (idx >= state.items.length) {
    return { state: { ...state, index: null }, text: state.draft, cursor: state.draft.length, apply: true }
  }
  return { state: { ...state, index: idx }, text: state.items[idx], cursor: dir === -1 ? 0 : state.items[idx].length, apply: true }
}

export class TuiApp {
  private client: TuiAppProps['client']
  private onSubmit?: TuiAppProps['onSubmit']
  private onSlashCommand?: TuiAppProps['onSlashCommand']
  private onResolvePermission?: TuiAppProps['onResolvePermission']
  private onExit?: TuiAppProps['onExit']
  private renderer: CliRenderer | null = null
  private unsubscribeClient: (() => void) | null = null
  private setProjection?: (projection: SessionProjection | null) => void
  private setView?: (view: TuiAppState['active_view']) => void
  private setBusy?: (busy: boolean) => void
  private setStatus?: (status: string) => void

  constructor(props: TuiAppProps) {
    this.client = props.client
    this.onSubmit = props.onSubmit
    this.onSlashCommand = props.onSlashCommand
    this.onResolvePermission = props.onResolvePermission
    this.onExit = props.onExit
  }

  async start(): Promise<void> {
    if (this.renderer) return

    this.renderer = await createCliRenderer({
      targetFps: 30,
      maxFps: 60,
      useMouse: false,
      autoFocus: false,
      exitOnCtrlC: false,
      screenMode: 'alternate-screen',
      externalOutputMode: 'passthrough',
      consoleMode: 'disabled',
      clearOnShutdown: true,
      openConsoleOnError: false,
      useKittyKeyboard: {},
      backgroundColor: THEME.bg,
    })
    this.renderer.setBackgroundColor(THEME.bg)

    await render(() => (
      <AirCodingView
        initialProjection={this.client.get_snapshot()}
        bindState={(bindings) => {
          this.setProjection = bindings.setProjection
          this.setView = bindings.setView
          this.setBusy = bindings.setBusy
          this.setStatus = bindings.setStatus
        }}
        onSubmit={(input) => this.submit(input)}
        onSlashCommand={(input) => this.slash(input)}
        onResolvePermission={(prompt_id, selected_option) => this.resolvePermission(prompt_id, selected_option)}
        onExit={() => this.exit()}
      />
    ), this.renderer)

    this.unsubscribeClient = this.client.subscribe((projection) => {
      this.setProjection?.(projection)
    })
  }

  stop(): void {
    this.unsubscribeClient?.()
    this.unsubscribeClient = null
    this.setProjection = undefined
    this.setView = undefined
    this.setBusy = undefined
    this.setStatus = undefined

    if (this.renderer && !this.renderer.isDestroyed) {
      this.renderer.setTerminalTitle('')
      this.renderer.externalOutputMode = 'passthrough'
      this.renderer.destroy()
    }
    this.renderer = null
  }

  set_view(view: TuiAppState['active_view']): void {
    this.setView?.(view)
  }

  set_busy(busy: boolean, status?: string): void {
    this.setBusy?.(busy)
    if (status) this.setStatus?.(status)
  }

  set_status(status: string): void {
    this.setStatus?.(status)
  }

  private async submit(input: string): Promise<void> {
    const text = input.trim()
    if (!text) return
    this.setBusy?.(true)
    this.setStatus?.(`Running: ${text.slice(0, 72)}`)
    try {
      await this.onSubmit?.(text)
      this.setStatus?.('Ready')
    } catch (error) {
      this.setStatus?.(error instanceof Error ? error.message : String(error))
      throw error
    } finally {
      this.setBusy?.(false)
    }
  }

  private async slash(input: string): Promise<void> {
    const text = input.trim()
    if (!text) return
    if (text === '/quit' || text === '/exit') {
      await this.exit()
      return
    }
    this.setStatus?.(`Command: ${text}`)
    await this.onSlashCommand?.(text)
  }

  private async resolvePermission(prompt_id: string, selected_option: string): Promise<void> {
    if (!this.onResolvePermission) {
      this.setStatus?.('Permission selection requires runtime resolver')
      return
    }
    this.setStatus?.(`Permission: ${selected_option}`)
    await this.onResolvePermission(prompt_id, selected_option)
  }

  private async exit(): Promise<void> {
    await this.onExit?.()
  }
}

type StateBindings = {
  setProjection: (projection: SessionProjection | null) => void
  setView: (view: TuiAppState['active_view']) => void
  setBusy: (busy: boolean) => void
  setStatus: (status: string) => void
}

type FooterPhase = 'idle' | 'running' | 'permission' | 'confirm_exit' | 'error'

function AirCodingView(props: {
  initialProjection: SessionProjection | null
  bindState: (bindings: StateBindings) => void
  onSubmit: (input: string) => void | Promise<void>
  onSlashCommand: (input: string) => void | Promise<void>
  onResolvePermission: (prompt_id: string, selected_option: string) => void | Promise<void>
  onExit: () => void | Promise<void>
}) {
  const renderer = useRenderer()
  const dimensions = useTerminalDimensions()
  const [projection, setProjection] = createSignal<SessionProjection | null>(props.initialProjection)
  const [activeView, setActiveView] = createSignal<TuiAppState['active_view']>('tasks')
  const [busy, setBusy] = createSignal(false)
  const [status, setStatus] = createSignal('Ready')
  const [footerPhase, setFooterPhase] = createSignal<FooterPhase>('idle')
  const [toast, setToast] = createSignal('')
  let textarea: TextareaRenderable | undefined
  let history = createPromptHistory()
  let pasteTick: ReturnType<typeof setTimeout> | undefined
  let exitConfirmUntil = 0

  props.bindState({
    setProjection,
    setView: setActiveView,
    setBusy: (next) => {
      setBusy(next)
      setFooterPhase(next ? 'running' : 'idle')
    },
    setStatus: (next) => {
      setStatus(next)
      if (/error|failed|blocked|失败|错误/i.test(next)) setFooterPhase('error')
      else if (!busy()) setFooterPhase('idle')
    },
  })

  const focusPrompt = () => {
    if (textarea && !textarea.isDestroyed) textarea.focus()
  }

  const submitPrompt = () => {
    if (!textarea || textarea.isDestroyed || busy()) return
    const text = textarea.plainText.trim()
    if (!text) return
    history = pushPromptHistory(history, text)
    textarea.setText('')
    exitConfirmUntil = 0
    setFooterPhase('running')
    setStatus(text.startsWith('/') ? `Command: ${text}` : `Sending: ${text.slice(0, 72)}`)
    if (text.startsWith('/')) {
      void props.onSlashCommand(text)
    } else {
      void props.onSubmit(text)
    }
    focusPrompt()
  }

  const refreshPasteLayout = () => {
    if (pasteTick) clearTimeout(pasteTick)
    pasteTick = setTimeout(() => {
      pasteTick = undefined
      if (!textarea || textarea.isDestroyed) return
      textarea.getLayoutNode().markDirty()
      renderer.requestRender()
      void renderer.idle().then(() => renderer.requestRender()).catch(() => {})
    }, 0)
  }

  const applyHistoryMove = (dir: -1 | 1) => {
    if (!textarea || textarea.isDestroyed) return false
    const text = textarea.plainText
    const move = movePromptHistory(history, dir, text, textarea.cursorOffset)
    history = move.state
    if (!move.apply) return false
    textarea.setText(move.text ?? '')
    textarea.cursorOffset = move.cursor ?? 0
    textarea.getLayoutNode().markDirty()
    renderer.requestRender()
    return true
  }

  const activePermission = () => projection()?.permission_prompts[0]

  const resolvePermissionByIndex = (index: number) => {
    const prompt = activePermission()
    if (!prompt) return false
    const options = prompt.options.length > 0 ? prompt.options : ['allow', 'deny']
    const selected = options[index]
    if (!selected) return false
    setFooterPhase('permission')
    setStatus(`Permission: ${selected}`)
    void props.onResolvePermission(prompt.prompt_id, selected)
    return true
  }

  const handleKeyDown = (event: KeyEvent) => {
    const prompt = activePermission()
    if (prompt) {
      if (event.name === 'left' || event.name === 'h') {
        event.preventDefault()
        setToast('Use 1/2/3 to choose a permission option')
        return
      }
      if (/^[1-9]$/.test(event.name) && resolvePermissionByIndex(Number(event.name) - 1)) {
        event.preventDefault()
        return
      }
      if ((event.name === 'a' || event.name === 'y') && resolvePermissionByIndex(0)) {
        event.preventDefault()
        return
      }
      if ((event.name === 'd' || event.name === 'n') && resolvePermissionByIndex(Math.min(1, (prompt.options.length || 2) - 1))) {
        event.preventDefault()
        return
      }
    }

    if (event.name === 'return') {
      event.preventDefault()
      submitPrompt()
      return
    }

    if (event.ctrl && event.name === 'c') {
      event.preventDefault()
      if (textarea && !textarea.isDestroyed && textarea.plainText.length > 0) {
        textarea.setText('')
        history = { ...history, index: null, draft: '' }
        setFooterPhase('idle')
        setStatus('Draft cleared; press Ctrl+C again to exit')
        focusPrompt()
        return
      }
      const now = Date.now()
      if (now < exitConfirmUntil) {
        void props.onExit()
        return
      }
      exitConfirmUntil = now + EXIT_CONFIRM_MS
      setFooterPhase('confirm_exit')
      setStatus('Press Ctrl+C again within 5s to exit')
      return
    }

    if (event.name === 'up' && applyHistoryMove(-1)) {
      event.preventDefault()
      return
    }

    if (event.name === 'down' && applyHistoryMove(1)) {
      event.preventDefault()
      return
    }

    if (event.name === 'escape') {
      event.preventDefault()
      exitConfirmUntil = 0
      setActiveView('tasks')
      setFooterPhase(busy() ? 'running' : activePermission() ? 'permission' : 'idle')
      focusPrompt()
      return
    }

    if (event.ctrl && event.name === 'l') {
      event.preventDefault()
      renderer.requestRender()
      return
    }

    if (!event.ctrl || event.meta) return

    const next = shortcutToView(event.name)
    if (next) {
      event.preventDefault()
      setActiveView(next)
      focusPrompt()
    }
  }

  onMount(() => {
    renderer.setTerminalTitle('AirCoding')
    focusPrompt()
  })

  onCleanup(() => {
    if (pasteTick) clearTimeout(pasteTick)
    renderer.setTerminalTitle('')
  })

  createEffect(() => {
    const hasPermission = (projection()?.permission_prompts.length ?? 0) > 0
    if (hasPermission) setFooterPhase('permission')
    else if (!busy() && footerPhase() === 'permission') setFooterPhase('idle')
  })

  createEffect(() => {
    projection()
    activeView()
    busy()
    status()
    footerPhase()
    toast()
    renderer.requestRender()
  })

  return (
    <box width="100%" height="100%" flexDirection="column" backgroundColor={THEME.bg}>
      <Header projection={projection()} />
      <Nav active={activeView()} />
      <box flexGrow={1} flexShrink={1} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
        <Content projection={projection()} active={activeView()} height={Math.max(8, dimensions().height - 11)} />
      </box>
      <Prompt
        busy={busy()}
        phase={footerPhase()}
        status={status()}
        toast={toast()}
        permission={activePermission()}
        textareaRef={(area) => { textarea = area }}
        onSubmit={submitPrompt}
        onKeyDown={handleKeyDown}
        onPaste={refreshPasteLayout}
        onContentChange={() => renderer.requestRender()}
      />
    </box>
  )
}

function Header(props: { projection: SessionProjection | null }) {
  const taskCount = () => props.projection?.tasks.length ?? 0
  return (
    <box flexDirection="column" paddingLeft={2} paddingRight={2} paddingTop={1} backgroundColor={THEME.surface}>
      <box flexDirection="row" justifyContent="space-between">
        <text fg={THEME.accent}>AirCoding v1.0.0-alpha</text>
        <text fg={statusColor(props.projection?.status)}>{(props.projection?.status ?? 'idle').toUpperCase()}</text>
      </box>
      <text fg={THEME.muted}>{props.projection?.title ?? 'No active session'} · {taskCount()} tasks · {props.projection?.agents.length ?? 0} agents</text>
    </box>
  )
}

function Nav(props: { active: TuiAppState['active_view'] }) {
  const items: Array<[TuiAppState['active_view'], string]> = [
    ['tasks', 'Ctrl+1 Tasks'],
    ['agents', 'Ctrl+2 Agents'],
    ['tools', 'Ctrl+3 Tools'],
    ['diff', 'Ctrl+4 Diff'],
    ['help', 'Ctrl+H Help'],
  ]
  return (
    <box flexDirection="row" gap={1} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} backgroundColor={THEME.surface2}>
      <For each={items}>{([view, label]) => (
        <text fg={props.active === view ? THEME.accent : THEME.muted}>{label}</text>
      )}</For>
    </box>
  )
}

function Content(props: { projection: SessionProjection | null; active: TuiAppState['active_view']; height: number }) {
  const currentProjection = () => props.projection

  return (
    <Show when={currentProjection()} fallback={<EmptySession />}>
      <box flexDirection="column" gap={1} height={props.height}>
        <Show when={props.active === 'tasks'}>
          <TasksView projection={currentProjection()!} />
        </Show>
        <Show when={props.active === 'agents'}>
          <AgentsView projection={currentProjection()!} />
        </Show>
        <Show when={props.active === 'tools'}>
          <ToolsView projection={currentProjection()!} />
        </Show>
        <Show when={props.active === 'diff'}>
          <DiffView projection={currentProjection()!} />
        </Show>
        <Show when={props.active === 'help'}>
          <HelpView />
        </Show>
      </box>
    </Show>
  )
}

function EmptySession() {
  return (
    <box flexDirection="column" gap={1}>
      <text fg={THEME.warning}>No active session projection.</text>
      <text fg={THEME.muted}>Run air init and air run from a project directory.</text>
    </box>
  )
}

function TasksView(props: { projection: SessionProjection }) {
  return (
    <box flexDirection="column" gap={1}>
      <text fg={THEME.accent}>Tasks</text>
      <Show when={props.projection.tasks.length > 0} fallback={<text fg={THEME.muted}>No tasks yet.</text>}>
        <For each={props.projection.tasks.slice(0, 14)}>{(task) => (
          <box flexDirection="column">
            <text fg={statusColor(task.status)}>{statusMark(task.status)} {task.title || task.id}</text>
            <text fg={THEME.faint}>  {task.id} · {task.type} · {task.status} · attempts {task.attempts}</text>
          </box>
        )}</For>
      </Show>
    </box>
  )
}

function AgentsView(props: { projection: SessionProjection }) {
  return (
    <box flexDirection="column" gap={1}>
      <text fg={THEME.accent}>Agents</text>
      <Show when={props.projection.agents.length > 0} fallback={<text fg={THEME.muted}>No agents yet.</text>}>
        <For each={props.projection.agents.slice(0, 14)}>{(agent) => (
          <box flexDirection="column">
            <text fg={statusColor(agent.status)}>{statusMark(agent.status)} {agent.type}</text>
            <text fg={THEME.faint}>  {agent.id} · {agent.status}{agent.task_id ? ` · task ${agent.task_id}` : ''}</text>
          </box>
        )}</For>
      </Show>
    </box>
  )
}

function ToolsView(props: { projection: SessionProjection }) {
  const p = () => props.projection as SessionProjection & { tool_runs?: Array<{ tool_run_id: string; tool_name: string; status: string; duration_ms?: number }> }
  return (
    <box flexDirection="column" gap={1}>
      <text fg={THEME.accent}>Tool runs</text>
      <Show when={(p().tool_runs ?? []).length > 0} fallback={<text fg={THEME.muted}>No tool runs yet.</text>}>
        <For each={(p().tool_runs ?? []).slice(-14).reverse()}>{(tool) => (
          <box flexDirection="row" gap={1}>
            <text fg={statusColor(tool.status)}>{statusMark(tool.status)}</text>
            <text fg={THEME.text}>{tool.tool_name}</text>
            <text fg={THEME.faint}>{tool.status}{tool.duration_ms ? ` · ${tool.duration_ms}ms` : ''}</text>
          </box>
        )}</For>
      </Show>
    </box>
  )
}

function DiffView(props: { projection: SessionProjection }) {
  const completed = () => props.projection.tasks.filter((task) => task.status === 'completed').length
  const failed = () => props.projection.tasks.filter((task) => task.status === 'failed').length
  const blocked = () => props.projection.tasks.filter((task) => task.status === 'blocked').length
  return (
    <box flexDirection="column" gap={1}>
      <text fg={THEME.accent}>Session summary</text>
      <text fg={THEME.success}>Completed: {completed()}</text>
      <text fg={THEME.error}>Failed: {failed()}</text>
      <text fg={THEME.warning}>Blocked: {blocked()}</text>
      <text fg={THEME.muted}>Use /results for produced files. Projection data is sourced from runtime events.</text>
    </box>
  )
}

function HelpView() {
  return (
    <box flexDirection="column" gap={1}>
      <text fg={THEME.accent}>Help</text>
      <text fg={THEME.text}>Type a task in the prompt and press Enter.</text>
      <text fg={THEME.text}>Slash commands: /help, /status, /tools, /tasks, /results, /quit.</text>
      <text fg={THEME.text}>Navigation: Ctrl+1 tasks, Ctrl+2 agents, Ctrl+3 tools, Ctrl+4 diff, Ctrl+H help, Esc tasks.</text>
      <text fg={THEME.text}>Prompt: Up/Down browse history at text boundaries; paste refreshes layout automatically.</text>
      <text fg={THEME.muted}>Ctrl+C clears draft first, then asks for a second Ctrl+C within 5s to exit. Permission prompts use 1/2/3 or a/d.</text>
    </box>
  )
}

function Prompt(props: {
  busy: boolean
  phase: FooterPhase
  status: string
  toast: string
  permission?: SessionProjection['permission_prompts'][number]
  textareaRef: (area?: TextareaRenderable) => void
  onSubmit: () => void
  onKeyDown: (event: KeyEvent) => void
  onPaste: () => void
  onContentChange: () => void
}) {
  const permissionOptions = () => props.permission?.options.length ? props.permission.options : ['allow', 'deny']
  const phaseLabel = () => {
    if (props.permission) return 'Permission'
    if (props.phase === 'confirm_exit') return 'Confirm exit'
    if (props.phase === 'error') return 'Attention'
    return props.busy ? 'Running' : 'Ready'
  }
  const phaseColor = () => {
    if (props.permission || props.phase === 'confirm_exit') return THEME.warning
    if (props.phase === 'error') return THEME.error
    return props.busy ? THEME.warning : THEME.success
  }

  return (
    <box flexDirection="column" paddingLeft={2} paddingRight={2} paddingBottom={1} backgroundColor={THEME.surface}>
      <Show when={props.permission}>
        <box flexDirection="column" paddingBottom={1}>
          <text fg={THEME.warning}>Permission required: {props.permission?.subject || props.permission?.tool_name || props.permission?.prompt_id}</text>
          <text fg={THEME.muted}>Risk: {props.permission?.risk_level || 'unknown'} · {props.permission?.reason || 'No reason provided'}</text>
          <box flexDirection="row" gap={1}>
            <For each={permissionOptions()}>{(option, index) => (
              <text fg={index() === 0 ? THEME.success : THEME.warning}>{index() + 1}. {option}</text>
            )}</For>
          </box>
        </box>
      </Show>
      <box flexDirection="row" justifyContent="space-between" paddingBottom={1}>
        <text fg={phaseColor()}>{phaseLabel()}</text>
        <text fg={props.phase === 'error' ? THEME.error : THEME.muted}>{props.toast || props.status}</text>
      </box>
      <textarea
        width="100%"
        minHeight={TEXTAREA_MIN_ROWS}
        maxHeight={TEXTAREA_MAX_ROWS}
        wrapMode="word"
        placeholder={props.busy ? 'Task is running...' : 'Ask AirCoding to change this project, or type /help'}
        placeholderColor={THEME.faint}
        textColor={THEME.text}
        focusedTextColor={THEME.text}
        backgroundColor={THEME.bg}
        focusedBackgroundColor={THEME.bg}
        cursorColor={THEME.accent}
        focused={!props.busy}
        onSubmit={props.onSubmit}
        onKeyDown={props.onKeyDown}
        onPaste={props.onPaste}
        onContentChange={props.onContentChange}
        ref={props.textareaRef}
      />
    </box>
  )
}

function shortcutToView(name: string): TuiAppState['active_view'] | undefined {
  switch (name) {
    case '1': return 'tasks'
    case '2': return 'agents'
    case '3': return 'tools'
    case '4': return 'diff'
    case 'h': return 'help'
    default: return undefined
  }
}

function statusColor(status: string | undefined): string {
  switch (status) {
    case 'completed':
    case 'ok':
    case 'active':
    case 'running':
      return THEME.success
    case 'failed':
    case 'error':
    case 'lost':
      return THEME.error
    case 'blocked':
    case 'pending':
    case 'cancelled':
      return THEME.warning
    default:
      return THEME.muted
  }
}

function statusMark(status: string | undefined): string {
  switch (status) {
    case 'completed':
    case 'ok':
      return '✓'
    case 'failed':
    case 'error':
      return '✗'
    case 'running':
    case 'active':
      return '●'
    case 'blocked':
      return '!'
    default:
      return '○'
  }
}
