import type { GoatChainContext } from '#cli/core/model'
import type { McpServerConfig } from '#cli/compat/mcp'
import type {
  AgentRuntime,
  AgentRuntimeSessions,
  AgentSession,
  ManualCompactionResult,
  RuntimeConfig,
  SessionSnapshot,
  ToolApprovalMode,
} from '#cli/compat/runtime'
import type { CliAgentBridge, CliSessionModeId } from '../agent-bridge'
import path from 'node:path'
import { resolveLegacyStateRoots } from '#cli/commands/server-helpers'
import { debugLog } from '#cli/core/debug-log'
import {
  RUNTIME_SNAPSHOT_DIR,
  runtimeSnapshotPath,
  safeLoadRuntimeSnapshot,
  writeJsonFile,
} from '#cli/compat/runtime-host'
import { createCliRuntime, readCliRuntimeConfig } from './runtime-host'
import {
  importLegacySessions,
  patchImportedSnapshot,
  RUNTIME_SESSION_METADATA_KEY,
} from './runtime-import'

function normalizeText(value: unknown): string {
  return String(value ?? '').trim()
}

function normalizeModeId(value: unknown): CliSessionModeId {
  return value === 'plan' ? 'plan' : 'agent'
}

function normalizeToolApprovalMode(value: unknown): ToolApprovalMode {
  if (value === 'auto' || value === 'normal' || value === 'strict')
    return value
  if (value === 'all')
    return 'auto'
  return null
}

function asRecord(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== 'object' || Array.isArray(value))
    return {}
  return value as Record<string, unknown>
}

function cloneJson<T>(value: T): T {
  return JSON.parse(JSON.stringify(value ?? null)) as T
}

function cloneEnvRecord(
  source: Record<string, string | undefined> | null | undefined,
): Record<string, string | undefined> {
  return Object.entries(source ?? {}).reduce<Record<string, string | undefined>>(
    (next, [key, value]) => {
      next[key] = value
      return next
    },
    {},
  )
}

function runtimeConfigsMatch(
  left: RuntimeConfig | null | undefined,
  right: RuntimeConfig | null | undefined,
): boolean {
  return normalizeText(left?.modelId) === normalizeText(right?.modelId)
    && normalizeText(left?.adapter) === normalizeText(right?.adapter)
    && normalizeText(left?.provider) === normalizeText(right?.provider)
    && normalizeText(left?.baseUrl) === normalizeText(right?.baseUrl)
    && normalizeText(left?.apiKey) === normalizeText(right?.apiKey)
    && Boolean(left?.nextApiOAuth) === Boolean(right?.nextApiOAuth)
    && Number(left?.maxInputTokens ?? 0) === Number(right?.maxInputTokens ?? 0)
    && Number(left?.maxOutputTokens ?? 0) === Number(right?.maxOutputTokens ?? 0)
}

function mcpServersMatch(
  left: readonly McpServerConfig[] | null | undefined,
  right: readonly McpServerConfig[] | null | undefined,
): boolean {
  return JSON.stringify(left ?? []) === JSON.stringify(right ?? [])
}

function parseRuntimeConfig(
  raw: unknown,
  fallback: RuntimeConfig,
): RuntimeConfig {
  const record = asRecord(raw)
  const modelId = normalizeText(record.modelId) || fallback.modelId
  const adapter = normalizeText(record.adapter) || fallback.adapter
  const provider = normalizeText(record.provider) || fallback.provider || adapter
  const baseUrl = normalizeText(record.baseUrl) || fallback.baseUrl
  const apiKey = normalizeText(record.apiKey) || fallback.apiKey
  const maxInputTokens
    = Number(record.maxInputTokens ?? fallback.maxInputTokens ?? 0) || undefined
  const maxOutputTokens
    = Number(record.maxOutputTokens ?? fallback.maxOutputTokens ?? 0) || undefined
  const env = record.env && typeof record.env === 'object'
    ? cloneJson(record.env as Record<string, string | undefined>)
    : fallback.env

  return {
    modelId,
    ...(adapter ? { adapter: adapter as RuntimeConfig['adapter'] } : {}),
    ...(provider ? { provider } : {}),
    ...(baseUrl ? { baseUrl } : {}),
    ...(apiKey ? { apiKey } : {}),
    ...(record.nextApiOAuth === true || fallback.nextApiOAuth === true
      ? { nextApiOAuth: true }
      : {}),
    ...(maxInputTokens ? { maxInputTokens } : {}),
    ...(maxOutputTokens ? { maxOutputTokens } : {}),
    ...(env ? { env } : {}),
  }
}

function readRuntimeEnvelopeFromSnapshot(
  snapshot: SessionSnapshot | null,
  fallback: RuntimeConfig,
): Readonly<{
  runtimeConfig: RuntimeConfig | null
  modeId: CliSessionModeId
  toolApprovalMode: ToolApprovalMode
  mcpServers: McpServerConfig[]
}> {
  const metadata = asRecord(snapshot?.metadata)
  const runtimeMeta = asRecord(metadata[RUNTIME_SESSION_METADATA_KEY])
  return {
    runtimeConfig: snapshot
      ? parseRuntimeConfig(runtimeMeta.runtimeConfig, {
          ...fallback,
          modelId:
            normalizeText(asRecord(runtimeMeta.runtimeConfig).modelId)
            || fallback.modelId,
        })
      : null,
    modeId: normalizeModeId(runtimeMeta.modeId),
    toolApprovalMode: normalizeToolApprovalMode(runtimeMeta.toolApprovalMode),
    mcpServers: Array.isArray(runtimeMeta.mcpServers)
      ? cloneJson(runtimeMeta.mcpServers as McpServerConfig[])
      : [],
  }
}

export function createCliAgent(opts: Readonly<{
  context: GoatChainContext
  processEnv: Record<string, string | undefined>
  stateDir: string
  tools?: {
    websearch?: {
      apiKey?: string | (() => string)
      apiEndpoint?: string | (() => string | undefined)
      numResults?: number | (() => number | undefined)
    }
  }
  mcpServers?: readonly McpServerConfig[] | (() => readonly McpServerConfig[])
  forcedSkills?: readonly string[] | (() => readonly string[])
}>): CliAgentBridge {
  const stateDir = path.resolve(opts.stateDir)
  let pendingMode: CliSessionModeId = 'agent'
  let runtimePromise: Promise<AgentRuntime> | null = null
  let migrationPromise: Promise<void> | null = null

  const getStateEnv = (): Record<string, string | undefined> => ({
    ...cloneEnvRecord(
      (opts.context.env ?? {}) as Record<string, string | undefined>,
    ),
    ...cloneEnvRecord(opts.processEnv),
  })

  const getRuntimeEnv = (): Record<string, string | undefined> => ({
    ...getStateEnv(),
    ...cloneEnvRecord(
      (typeof process !== 'undefined' ? process.env : undefined) as
      | Record<string, string | undefined>
      | undefined,
    ),
  })

  const getRuntimeConfig = (): RuntimeConfig => readCliRuntimeConfig(getRuntimeEnv())

  const logSessionPersistence = (sessionId: string) => {
    const normalizedSessionId = normalizeText(sessionId)
    if (!normalizedSessionId)
      return
    debugLog('cli.session.persistence', {
      sessionId: normalizedSessionId,
      stateDir,
      snapshotPath: runtimeSnapshotPath(stateDir, normalizedSessionId),
    })
  }

  const migrateLegacySnapshots = async (): Promise<void> => {
    if (migrationPromise)
      return await migrationPromise

    const runningMigration = (async () => {
      const stateRoots = resolveLegacyStateRoots({
        activeStateDir: stateDir,
        env: getStateEnv(),
      })
      let migrationRuntime: AgentRuntime | null = null
      const getMigrationRuntime = async (): Promise<AgentRuntime> => {
        if (migrationRuntime)
          return migrationRuntime
        migrationRuntime = await createCliRuntime({
          context: opts.context,
          stateDir,
          processEnv: opts.processEnv,
          tools: opts.tools,
          mcpServers: opts.mcpServers,
          forcedSkills: opts.forcedSkills,
        })
        return migrationRuntime
      }

      await importLegacySessions({
        activeStateDir: stateDir,
        stateRoots,
        fallbackRuntimeConfig: getRuntimeConfig(),
        fallbackCwd: normalizeText(opts.context.cwd) || process.cwd(),
        hasRuntimeSnapshot: async sessionId =>
          Boolean(await safeLoadRuntimeSnapshot({
            directory: path.join(stateDir, RUNTIME_SNAPSHOT_DIR),
            sessionId,
          })),
        materializeSession: async (payload) => {
          const runtime = await getMigrationRuntime()
          if (payload.messages.length > 0) {
            await runtime.sessions.createWithMessages(payload.messages, {
              sessionId: payload.sessionId,
              cwd: payload.cwd,
              title: payload.title,
              modeId: payload.modeId,
              runtimeConfig: payload.runtimeConfig,
              toolApprovalMode: payload.toolApprovalMode,
              mcpServers: payload.mcpServers,
              metadata: payload.metadata,
            })
          }
          else {
            await runtime.sessions.create({
              sessionId: payload.sessionId,
              cwd: payload.cwd,
              title: payload.title,
              modeId: payload.modeId,
              runtimeConfig: payload.runtimeConfig,
              toolApprovalMode: payload.toolApprovalMode,
              mcpServers: payload.mcpServers,
              metadata: payload.metadata,
            })
          }

          const snapshot = await runtime.sessions.get(payload.sessionId)
          if (!snapshot)
            throw new Error(`Runtime snapshot missing for ${payload.sessionId}`)
          await writeJsonFile(
            runtimeSnapshotPath(stateDir, payload.sessionId),
            patchImportedSnapshot(snapshot, payload),
          )
        },
      })
    })()
    migrationPromise = runningMigration

    try {
      await runningMigration
    }
    finally {
      if (migrationPromise === runningMigration)
        migrationPromise = null
    }
  }

  const ensureRuntime = async (): Promise<AgentRuntime> => {
    if (runtimePromise)
      return await runtimePromise

    runtimePromise = (async () => {
      await migrateLegacySnapshots()
      return await createCliRuntime({
        context: opts.context,
        stateDir,
        processEnv: opts.processEnv,
        tools: opts.tools,
        mcpServers: opts.mcpServers,
        forcedSkills: opts.forcedSkills,
      })
    })()

    try {
      return await runtimePromise
    }
    catch (error) {
      runtimePromise = null
      throw error
    }
  }

  const ensureMigratedRuntime = async (): Promise<AgentRuntime> => {
    if (!runtimePromise)
      return await ensureRuntime()
    await migrateLegacySnapshots()
    return await runtimePromise
  }

  const runtimeSessions: AgentRuntimeSessions = {
    list: async () => (await ensureMigratedRuntime()).sessions.list(),
    get: async sessionId => (await ensureMigratedRuntime()).sessions.get(sessionId),
    create: async input => (await ensureRuntime()).sessions.create(input),
    createWithMessages: async (messages, input) =>
      await (await ensureRuntime()).sessions.createWithMessages(messages, input),
    destroy: async sessionId => await (await ensureRuntime()).sessions.destroy(sessionId),
    rename: async (sessionId, title) =>
      await (await ensureRuntime()).sessions.rename(sessionId, title),
    updateMetadata: async (sessionId, metadata) =>
      await (await ensureRuntime()).sessions.updateMetadata(sessionId, metadata),
    interrupt: async sessionId =>
      await (await ensureRuntime()).sessions.interrupt(sessionId),
  }

  const runtimeFacade: AgentRuntime = {
    sessions: runtimeSessions,
    listSessions: async () => await (await ensureMigratedRuntime()).listSessions(),
    openSession: async sessionId => await (await ensureMigratedRuntime()).openSession(sessionId),
    createSession: async input => await (await ensureRuntime()).createSession(input),
    destroySession: async sessionId =>
      await (await ensureRuntime()).destroySession(sessionId),
  }

  const openSession = async (sessionId: string): Promise<AgentSession | null> => {
    const normalizedSessionId = normalizeText(sessionId)
    if (!normalizedSessionId)
      return null
    return await (await ensureMigratedRuntime()).openSession(normalizedSessionId)
  }

  const ensureSession = async (input?: {
    sessionId?: string
    cwd?: string
    modeId?: CliSessionModeId
    runtimeConfig?: RuntimeConfig
    toolApprovalMode?: ToolApprovalMode
    mcpServers?: McpServerConfig[]
    metadata?: Record<string, unknown>
  }): Promise<AgentSession> => {
    const requestedSessionId = normalizeText(input?.sessionId)
    const runtime = requestedSessionId
      ? await ensureMigratedRuntime()
      : await ensureRuntime()
    const nextMode = normalizeModeId(input?.modeId ?? pendingMode)
    const nextCwd = normalizeText(input?.cwd) || normalizeText(opts.context.cwd) || process.cwd()
    const nextRuntimeConfig = input?.runtimeConfig ?? getRuntimeConfig()
    const hasToolApprovalMode = Object.prototype.hasOwnProperty.call(
      input ?? {},
      'toolApprovalMode',
    )
    const nextToolApprovalMode = normalizeToolApprovalMode(input?.toolApprovalMode)
    const hasMcpServers = Array.isArray(input?.mcpServers)
    const nextMcpServers = Array.isArray(input?.mcpServers)
      ? cloneJson(input.mcpServers)
      : undefined
    const nextMetadata = input?.metadata ? cloneJson(input.metadata) : undefined

    if (requestedSessionId) {
      const existing = await runtime.openSession(requestedSessionId)
      if (existing) {
        const snapshot = await runtime.sessions.get(existing.id)
        const envelope = readRuntimeEnvelopeFromSnapshot(snapshot, nextRuntimeConfig)
        const update: Record<string, unknown> = {}
        if (normalizeText(snapshot?.cwd) !== nextCwd)
          update.cwd = nextCwd
        if (!runtimeConfigsMatch(envelope.runtimeConfig, nextRuntimeConfig))
          update.runtimeConfig = nextRuntimeConfig
        if (envelope.modeId !== nextMode)
          await existing.setMode(nextMode)
        if (
          hasToolApprovalMode
          && envelope.toolApprovalMode !== nextToolApprovalMode
        ) {
          update.toolApprovalMode = nextToolApprovalMode
        }
        if (
          hasMcpServers
          && Array.isArray(nextMcpServers)
          && !mcpServersMatch(envelope.mcpServers, nextMcpServers)
        ) {
          update.mcpServers = nextMcpServers
        }
        if (nextMetadata)
          update.metadata = nextMetadata
        if (Object.keys(update).length > 0)
          await existing.updateConfig(update)
        logSessionPersistence(existing.id)
        return existing
      }
    }

    const created = await runtime.createSession({
      sessionId: requestedSessionId || undefined,
      cwd: nextCwd,
      modeId: nextMode,
      runtimeConfig: nextRuntimeConfig,
      ...(hasToolApprovalMode ? { toolApprovalMode: nextToolApprovalMode } : {}),
      ...(hasMcpServers ? { mcpServers: nextMcpServers } : {}),
      ...(nextMetadata ? { metadata: nextMetadata } : {}),
    })
    logSessionPersistence(created.id)
    return created
  }

  return {
    runtime: runtimeFacade,
    sessions: runtimeSessions,
    openSession,
    ensureSession,
    async steerSession(sessionId: string, input: string) {
      const session = await openSession(sessionId)
      if (!session)
        return false
      return await session.steer(input)
    },
    setMode(modeId: CliSessionModeId) {
      pendingMode = normalizeModeId(modeId)
    },
    getMode() {
      return pendingMode
    },
    async triggerManualCompression(
      sessionId?: string,
    ): Promise<ManualCompactionResult> {
      const normalizedSessionId = normalizeText(sessionId)
      const session = normalizedSessionId
        ? await openSession(normalizedSessionId)
        : null
      if (!session) {
        return {
          success: false,
          error: normalizedSessionId
            ? `Session not found: ${normalizedSessionId}`
            : 'Manual compaction requires an active session id',
        }
      }
      return await session.triggerManualCompaction()
    },
    invalidateSession(_sessionId: string) {
      // Runtime sessions are reopened directly from the runtime facade.
    },
  }
}
