import type { App } from '@types'

import * as readline from 'readline'

import color from 'ansis'

import { get, handleError, HOST, withSignal } from './http'
import { program } from './setup'
import { STATE_ICONS } from './commands/manage'

let appNamesCache: string[] = []
let appNamesCacheTime = 0

const APP_CACHE_TTL = 5000

function tokenize(input: string): string[] {
  const tokens: string[] = []
  let current = ''
  let quote: string | null = null

  for (const ch of input) {
    if (quote) {
      if (ch === quote) {
        quote = null
      } else {
        current += ch
      }
    } else if (ch === '"' || ch === "'") {
      quote = ch
    } else if (ch === ' ' || ch === '\t') {
      if (current) {
        tokens.push(current)
        current = ''
      }
    } else {
      current += ch
    }
  }
  if (current) tokens.push(current)
  return tokens
}

async function fetchAppNames(): Promise<string[]> {
  const now = Date.now()
  if (appNamesCache.length > 0 && now - appNamesCacheTime < APP_CACHE_TTL) {
    return appNamesCache
  }
  try {
    const apps = await get<App[]>('/api/apps')
    if (apps) {
      appNamesCache = apps.map(a => a.name)
      appNamesCacheTime = now
    }
  } catch {
    // use stale cache
  }
  return appNamesCache
}

function getCommandNames(): string[] {
  return program.commands
    .filter((cmd) => !(cmd as any)._hidden)
    .map((cmd) => cmd.name())
}

async function printBanner(): Promise<void> {
  const apps = await get<App[]>('/api/apps')
  if (!apps) {
    console.log(color.bold.cyan('  \u{1F43E} Toes') + `  ${HOST}`)
    console.log()
    return
  }

  // Cache app names from banner fetch
  appNamesCache = apps.map(a => a.name)
  appNamesCacheTime = Date.now()

  const visibleApps = apps.filter(a => !a.tool)

  console.log()
  console.log(color.bold.cyan('  \u{1F43E} Toes') + `  ${HOST}`)
  console.log()

  // App status line
  const parts = visibleApps.map(a => {
    const icon = STATE_ICONS[a.state] ?? '\u25CB'
    return `${icon} ${a.name}`
  })
  if (parts.length > 0) {
    console.log('  ' + parts.join('    '))
    console.log()
  }

  const running = visibleApps.filter(a => a.state === 'running').length
  const stopped = visibleApps.filter(a => a.state !== 'running').length
  const summary = []
  if (running) summary.push(`${running} running`)
  if (stopped) summary.push(`${stopped} stopped`)
  if (summary.length > 0) {
    console.log(color.gray(`  ${summary.join(', ')} \u2014 type "help" for commands`))
  } else {
    console.log(color.gray('  no apps \u2014 type "help" for commands'))
  }
  console.log()
}

export async function shell(): Promise<void> {
  await printBanner()

  // Configure Commander to throw instead of exiting
  program.exitOverride()
  program.configureOutput({
    writeOut: (str: string) => process.stdout.write(str),
    writeErr: (str: string) => process.stderr.write(str),
  })

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    prompt: color.cyan('toes> '),
    completer: (line: string, callback: (err: null, result: [string[], string]) => void) => {
      const tokens = tokenize(line)
      const trailing = line.endsWith(' ')

      if (tokens.length === 0 || (tokens.length === 1 && !trailing)) {
        // Complete command names
        const partial = tokens[0] ?? ''
        const commands = getCommandNames()
        const hits = commands.filter(c => c.startsWith(partial))
        callback(null, [hits, partial])
      } else {
        // Complete app names
        const partial = trailing ? '' : (tokens[tokens.length - 1] ?? '')
        const names = appNamesCache
        const hits = names.filter(n => n.startsWith(partial))
        callback(null, [hits, partial])
      }
    },
  })

  // Refresh app names cache in background for tab completion
  fetchAppNames()

  let activeAbort: AbortController | null = null

  rl.on('SIGINT', () => {
    if (activeAbort) {
      activeAbort.abort()
      activeAbort = null
      console.log()
      rl.prompt()
    } else {
      // Clear current line
      rl.write(null, { ctrl: true, name: 'u' })
      console.log()
      rl.prompt()
    }
  })

  rl.prompt()

  for await (const line of rl) {
    const input = line.trim()

    if (!input) {
      rl.prompt()
      continue
    }

    if (input === 'exit' || input === 'quit') {
      break
    }

    if (input === 'clear') {
      console.clear()
      rl.prompt()
      continue
    }

    if (input === 'help') {
      program.outputHelp()
      rl.prompt()
      continue
    }

    const tokens = tokenize(input)

    // Set up AbortController for this command
    activeAbort = new AbortController()
    const signal = activeAbort.signal

    // Pause readline so commands can use their own prompts
    rl.pause()

    try {
      await withSignal(signal, () => program.parseAsync(['node', 'toes', ...tokens]))
    } catch (err: unknown) {
      // Commander throws on exitOverride — suppress help/version exits
      if (err && typeof err === 'object' && 'code' in err) {
        const code = (err as { code: string }).code
        if (code === 'commander.helpDisplayed' || code === 'commander.version') {
          // Already printed, just continue
        } else if (code === 'commander.unknownCommand') {
          console.error(`Unknown command: ${tokens[0]}`)
        } else {
          // Other Commander errors (missing arg, etc.)
          // Commander already printed the error message
        }
      } else if (signal.aborted) {
        // Command was cancelled by Ctrl+C
      } else {
        handleError(err)
      }
    } finally {
      activeAbort = null
    }

    // Refresh app names cache after commands that might change state
    fetchAppNames()

    rl.resume()
    rl.prompt()
  }

  rl.close()
  console.log()
}
