import { randomUUID } from 'node:crypto'
import { statSync } from 'node:fs'
import { homedir } from 'node:os'
import path from 'node:path'
import { createDimSdkShellProps } from './dimsdk-app'

const EXEC_USAGE = [
  'Usage:',
  '  dim [--cwd dir] [--session id] "prompt"',
  '  echo "prompt" | dim',
  '',
  'Notes:',
  '  - Tools are auto-approved.',
].join('\n')

type ExecParseResult = Readonly<{
  cwd: string
  promptParts: string[]
  sessionId?: string
  error?: string
}>

function expandTilde(input: string): string {
  if (!input.startsWith('~'))
    return input
  const home = homedir()
  if (input === '~')
    return home
  if (input.startsWith('~/') || input.startsWith('~\\'))
    return path.join(home, input.slice(2))
  return input
}

function resolveCwd(input: string, baseCwd: string): string {
  const raw = String(input ?? '').trim()
  if (!raw)
    return baseCwd
  return path.resolve(baseCwd, expandTilde(raw))
}

export function parseExecArgs(
  argv: readonly string[],
  baseCwd: string,
): ExecParseResult {
  const promptParts: string[] = []
  let cwd = baseCwd
  let sessionId: string | undefined

  for (let i = 0; i < argv.length; i++) {
    const arg = String(argv[i] ?? '')
    if (!arg)
      continue
    if (arg === '--') {
      promptParts.push(...argv.slice(i + 1))
      break
    }
    const inlineCwd = arg.startsWith('-cwd=') ? arg.slice(5) : undefined
    if (
      arg === '-c'
      || arg === '-cwd'
      || arg === '--cwd'
      || arg === '--chdir'
      || inlineCwd !== undefined
    ) {
      const next = inlineCwd ?? argv[++i]
      if (!next) {
        return {
          cwd,
          promptParts,
          sessionId,
          error: 'Missing value for -c/-cwd/--cwd.',
        }
      }
      cwd = resolveCwd(String(next), baseCwd)
      continue
    }
    if (arg === '-s' || arg === '--session') {
      const next = argv[++i]
      if (!next) {
        return {
          cwd,
          promptParts,
          sessionId,
          error: 'Missing value for -s/--session.',
        }
      }
      const trimmed = String(next ?? '').trim()
      if (!trimmed) {
        return {
          cwd,
          promptParts,
          sessionId,
          error: 'Session id cannot be empty.',
        }
      }
      sessionId = trimmed
      continue
    }
    if (arg.startsWith('-'))
      return { cwd, promptParts, sessionId, error: `Unknown option: ${arg}` }
    promptParts.push(arg)
  }

  return { cwd, promptParts, sessionId }
}

async function readStdin(proc: NodeJS.Process): Promise<string> {
  const stdin = proc.stdin
  if (stdin.isTTY)
    return ''
  stdin.setEncoding('utf8')
  let data = ''
  for await (const chunk of stdin)
    data += chunk
  return data
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return value != null && typeof value === 'object' && !Array.isArray(value)
}

function asRecord(value: unknown): Record<string, unknown> {
  if (!isRecord(value))
    return {}
  return value
}

function formatTextContent(content: unknown): string {
  if (typeof content === 'string')
    return content
  if (Array.isArray(content)) {
    return content
      .map((item) => {
        if (isRecord(item) && 'text' in item)
          return String(item.text ?? '')
        try {
          return JSON.stringify(item)
        }
        catch {
          return String(item)
        }
      })
      .join('\n')
  }
  try {
    return JSON.stringify(content, null, 2)
  }
  catch {
    return String(content)
  }
}

function writeOut(proc: NodeJS.Process, text: string): void {
  if (!text)
    return
  if (proc.stdout && typeof proc.stdout.write === 'function') {
    proc.stdout.write(text)
    return
  }
  console.log(text)
}

function writeStatus(proc: NodeJS.Process, text: string): void {
  const line = String(text ?? '').trim()
  if (!line)
    return
  if (proc.stderr && typeof proc.stderr.write === 'function') {
    proc.stderr.write(`${line}\n`)
    return
  }
  console.error(line)
}

function normalizeToolName(name: unknown, fallback: string): string {
  const n = String(name ?? '').trim()
  return n || fallback
}

export async function runExec(
  argv: readonly string[],
  proc: NodeJS.Process,
): Promise<number> {
  const baseCwd = String(proc.cwd?.() ?? '.')
  const parsed = parseExecArgs(argv, baseCwd)
  if (parsed.error) {
    console.error(parsed.error)
    console.error(EXEC_USAGE)
    return 1
  }

  const cwd = resolveCwd(parsed.cwd, baseCwd)
  try {
    const stat = statSync(cwd)
    if (!stat.isDirectory()) {
      console.error(`Not a directory: ${cwd}`)
      return 1
    }
  }
  catch {
    console.error(`Directory not found: ${cwd}`)
    return 1
  }

  let prompt = parsed.promptParts.join(' ').trim()
  if (!prompt)
    prompt = (await readStdin(proc)).trim()
  if (!prompt) {
    console.error('Missing prompt.')
    console.error(EXEC_USAGE)
    return 1
  }

  const env: Record<string, string | undefined> = { ...proc.env }
  const sessionId = parsed.sessionId ?? randomUUID()
  env.DIMCODE_TOOL_APPROVALS = 'auto'
  env.DIMCODE_SESSION_ID = sessionId
  // One-shot mode needs the full config (provider, API key, model) loaded from
  // disk immediately — there is no UI to defer hydration to.
  env.DIMCODE_FAST_START = '0'

  const { agent } = await createDimSdkShellProps({
    cwd,
    env,
    sessionId,
  })

  const ensureSession = agent.ensureSession
  if (typeof ensureSession !== 'function')
    throw new Error('CLI runtime session API is unavailable')

  const session = await ensureSession({
    sessionId,
    cwd,
    modeId: agent.getMode?.() ?? 'agent',
    toolApprovalMode: 'auto',
  })

  let previewOutput = ''
  let settledOutput: string | null = null
  let needsAction = false
  let runError: unknown = null
  const toolOutputs: string[] = []
  const toolStates = new Map<string, { name: string, done: boolean }>()

  try {
    for await (const event of session.run({ content: prompt })) {
      if (event.type === 'assistant.delta') {
        previewOutput += event.delta
        continue
      }

      if (event.type === 'assistant.message') {
        const content = formatTextContent(event.message.content)
        if (content)
          settledOutput = content
        continue
      }

      if (event.type === 'tool.started') {
        toolStates.set(event.toolCallId, {
          name: normalizeToolName(event.toolName, event.toolCallId),
          done: false,
        })
        writeStatus(
          proc,
          `[tool] ${normalizeToolName(event.toolName, event.toolCallId)} running`,
        )
        continue
      }

      if (event.type === 'tool.delta') {
        if (event.kind !== 'output')
          continue
        const rendered = String(event.delta ?? '')
        if (rendered)
          toolOutputs.push(rendered)
        const stream = event.isStderr ? proc.stderr : proc.stdout
        if (stream && typeof stream.write === 'function' && rendered)
          stream.write(rendered)
        continue
      }

      if (event.type === 'tool.finished') {
        const toolName = normalizeToolName(event.toolName, event.toolCallId)
        const entry = toolStates.get(event.toolCallId) ?? {
          name: toolName,
          done: false,
        }
        if (!entry.done) {
          toolStates.set(event.toolCallId, { name: toolName, done: true })
          writeStatus(proc, `[tool] ${toolName} ${event.isError ? 'failed' : 'done'}`)
        }
        const outputRecord = asRecord(event.output)
        const rendered = formatTextContent(
          outputRecord.output ?? outputRecord.content ?? event.output,
        )
        if (rendered)
          toolOutputs.push(rendered)
        continue
      }

      if (event.type === 'action.required') {
        needsAction = true
        const label = event.kind === 'ask_user' ? 'ask_user' : 'tool_approval'
        writeStatus(proc, `[paused] ${label}`)
        continue
      }

      if (event.type === 'run.error') {
        runError = event.error
        continue
      }
    }
  }
  catch (error) {
    console.error(
      `Execution failed: ${error instanceof Error ? error.message : String(error)}`,
    )
    return 1
  }

  const assistantOutput = settledOutput ?? previewOutput

  if (assistantOutput)
    writeOut(proc, assistantOutput)

  if (!assistantOutput.trim() && toolOutputs.length) {
    const fallback = toolOutputs.join('\n')
    if (fallback.trim())
      writeOut(proc, fallback)
  }

  if (assistantOutput && !assistantOutput.endsWith('\n'))
    writeOut(proc, '\n')

  writeStatus(proc, `session_id: ${session.id}`)

  if (needsAction) {
    console.error('Execution paused: user input required.')
    return 2
  }

  if (runError) {
    const detail = runError instanceof Error
      ? runError.message
      : (() => {
          try {
            return JSON.stringify(runError)
          }
          catch {
            return String(runError)
          }
        })()
    console.error(detail ? `Execution failed: ${detail}` : 'Execution failed.')
    return 1
  }

  return 0
}
