import {
  RequestError,
  type AgentSideConnection,
  type AuthenticateRequest,
  type InitializeRequest,
  type InitializeResponse,
  type ListSessionsRequest,
  type ListSessionsResponse,
  type LoadSessionRequest,
  type NewSessionRequest,
  type ForkSessionRequest,
  type ForkSessionResponse,
  type ResumeSessionRequest,
  type ResumeSessionResponse,
  type SessionInfo,
} from "@agentclientprotocol/sdk"
import { LoadAPIKeyError } from "ai"
import type { SaeeolClient } from "@saeeol/sdk/v2"
import { ProviderID, ModelID } from "../provider/schema"
import { MessageV2 } from "@/session/message-v2"
import { ConfigMCP } from "@/config/mcp"
import { AppRuntime } from "@/effect/app-runtime"
import { Agent as AgentModule } from "../agent/agent"
import { InstallationVersion } from "@saeeol/core/installation/version"
import type { ACPConfig } from "./types"
import type { ACPSessionManager } from "./session"
import type { AgentDeps, ModeOption } from "./agent-types"
import { log, defaultModel, sendUsageUpdate, sortProvidersByName, modelVariantsFromProviders, buildAvailableModels, formatModelIdWithVariant, buildVariantMeta, buildConfigOptions } from "./agent-utils"
import { processMessage } from "./agent-message"

export async function initialize(params: InitializeRequest): Promise<InitializeResponse> {
  log.info("initialize", { protocolVersion: params.protocolVersion })
  const authMethod = { description: "Run `saeeol auth login` in the terminal", name: "Login with Saeeol", id: "saeeol-login" } as any
  if (params.clientCapabilities?._meta?.["terminal-auth"] === true) {
    authMethod._meta = { "terminal-auth": { command: "saeeol", args: ["auth", "login"], label: "Saeeol Login" } }
  }
  return {
    protocolVersion: 1,
    agentCapabilities: { loadSession: true, mcpCapabilities: { http: true, sse: true }, promptCapabilities: { embeddedContext: true, image: true }, sessionCapabilities: { fork: {}, list: {}, resume: {} } },
    authMethods: [authMethod],
    agentInfo: { name: "Saeeol", version: InstallationVersion },
  }
}

export async function newSession(deps: AgentDeps, params: NewSessionRequest) {
  const directory = params.cwd
  try {
    const model = await defaultModel(deps.config, directory)
    const state = await deps.sessionManager.create(params.cwd, params.mcpServers, model)
    const sessionId = state.id
    log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
    const load = await loadSessionMode(deps, { cwd: directory, mcpServers: params.mcpServers, sessionId })
    return { sessionId, configOptions: load.configOptions, models: load.models, modes: load.modes, _meta: load._meta }
  } catch (e) {
    const error = MessageV2.fromError(e, { providerID: ProviderID.make(deps.config.defaultModel?.providerID ?? "unknown") })
    if (LoadAPIKeyError.isInstance(error)) throw RequestError.authRequired()
    throw e
  }
}

export async function loadSession(deps: AgentDeps, params: LoadSessionRequest) {
  const directory = params.cwd
  const sessionId = params.sessionId
  try {
    const model = await defaultModel(deps.config, directory)
    await deps.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
    log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
    const result = await loadSessionMode(deps, { cwd: directory, mcpServers: params.mcpServers, sessionId })
    const messages = await deps.sdk.session.messages({ sessionID: sessionId, directory }, { throwOnError: true }).then((x: any) => x.data).catch((err: any) => { log.error("unexpected error when fetching message", { error: err }); return undefined })
    const lastUser = messages?.findLast((m: any) => m.info.role === "user")?.info
    if (lastUser?.role === "user") {
      result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}`
      deps.sessionManager.setModel(sessionId, { providerID: ProviderID.make(lastUser.model.providerID), modelID: ModelID.make(lastUser.model.modelID) })
      if (result.modes?.availableModes.some((m: any) => m.id === lastUser.agent)) { result.modes.currentModeId = lastUser.agent; deps.sessionManager.setMode(sessionId, lastUser.agent) }
      result.configOptions = buildConfigOptions({ currentModelId: result.models.currentModelId, availableModels: result.models.availableModels, modes: result.modes })
    }
    for (const msg of messages ?? []) { log.debug("replay message", msg); await processMessage(deps, msg) }
    await sendUsageUpdate(deps.connection, deps.sdk, sessionId, directory)
    return result
  } catch (e) {
    const error = MessageV2.fromError(e, { providerID: ProviderID.make(deps.config.defaultModel?.providerID ?? "unknown") })
    if (LoadAPIKeyError.isInstance(error)) throw RequestError.authRequired()
    throw e
  }
}

export async function listSessions(deps: AgentDeps, params: ListSessionsRequest): Promise<ListSessionsResponse> {
  try {
    const cursor = params.cursor ? Number(params.cursor) : undefined
    const limit = 100
    const sessions = await deps.sdk.session.list({ directory: params.cwd ?? undefined, roots: true }, { throwOnError: true }).then((x: any) => x.data ?? [])
    const sorted = sessions.toSorted((a: any, b: any) => b.time.updated - a.time.updated)
    const filtered = cursor ? sorted.filter((s: any) => s.time.updated < cursor) : sorted
    const page = filtered.slice(0, limit)
    const entries: SessionInfo[] = page.map((session: any) => ({ sessionId: session.id, cwd: session.directory, title: session.title, updatedAt: new Date(session.time.updated).toISOString() }))
    const last = page[page.length - 1]
    const next = filtered.length > limit && last ? String(last.time.updated) : undefined
    const response: ListSessionsResponse = { sessions: entries }
    if (next) response.nextCursor = next
    return response
  } catch (e) {
    const error = MessageV2.fromError(e, { providerID: ProviderID.make(deps.config.defaultModel?.providerID ?? "unknown") })
    if (LoadAPIKeyError.isInstance(error)) throw RequestError.authRequired()
    throw e
  }
}

export async function forkSession(deps: AgentDeps, params: ForkSessionRequest): Promise<ForkSessionResponse> {
  const directory = params.cwd
  const mcpServers = params.mcpServers ?? []
  try {
    const model = await defaultModel(deps.config, directory)
    const forked = await deps.sdk.session.fork({ sessionID: params.sessionId, directory }, { throwOnError: true }).then((x: any) => x.data)
    if (!forked) throw new Error("Fork session returned no data")
    const sessionId = forked.id
    await deps.sessionManager.load(sessionId, directory, mcpServers, model)
    log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
    const mode = await loadSessionMode(deps, { cwd: directory, mcpServers, sessionId })
    const messages = await deps.sdk.session.messages({ sessionID: sessionId, directory }, { throwOnError: true }).then((x: any) => x.data).catch((err: any) => { log.error("unexpected error when fetching message", { error: err }); return undefined })
    for (const msg of messages ?? []) { log.debug("replay message", msg); await processMessage(deps, msg) }
    await sendUsageUpdate(deps.connection, deps.sdk, sessionId, directory)
    return mode
  } catch (e) {
    const error = MessageV2.fromError(e, { providerID: ProviderID.make(deps.config.defaultModel?.providerID ?? "unknown") })
    if (LoadAPIKeyError.isInstance(error)) throw RequestError.authRequired()
    throw e
  }
}

export async function resumeSession(deps: AgentDeps, params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
  const directory = params.cwd
  const sessionId = params.sessionId
  const mcpServers = params.mcpServers ?? []
  try {
    const model = await defaultModel(deps.config, directory)
    await deps.sessionManager.load(sessionId, directory, mcpServers, model)
    log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
    const result = await loadSessionMode(deps, { cwd: directory, mcpServers, sessionId })
    await sendUsageUpdate(deps.connection, deps.sdk, sessionId, directory)
    return result
  } catch (e) {
    const error = MessageV2.fromError(e, { providerID: ProviderID.make(deps.config.defaultModel?.providerID ?? "unknown") })
    if (LoadAPIKeyError.isInstance(error)) throw RequestError.authRequired()
    throw e
  }
}

async function loadAvailableModes(deps: AgentDeps, directory: string): Promise<ModeOption[]> {
  const agents = await deps.config.sdk.app.agents({ directory }, { throwOnError: true }).then((resp: any) => resp.data!)
  return agents.filter((agent: any) => agent.mode !== "subagent" && !agent.hidden).map((agent: any) => ({ id: agent.name, name: agent.name, description: agent.description }))
}

async function resolveModeState(deps: AgentDeps, directory: string, sessionId: string) {
  const availableModes = await loadAvailableModes(deps, directory)
  const currentModeId = deps.sessionManager.get(sessionId).modeId || await (async () => {
    if (!availableModes.length) return undefined
    const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc: any) => svc.defaultAgent()))
    const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
    deps.sessionManager.setMode(sessionId, resolvedModeId)
    return resolvedModeId
  })()
  return { availableModes, currentModeId }
}

async function loadSessionMode(deps: AgentDeps, params: LoadSessionRequest) {
  const directory = params.cwd
  const model = await defaultModel(deps.config, directory)
  const sessionId = params.sessionId
  const providers = await deps.sdk.config.providers({ directory }).then((x: any) => x.data!.providers)
  const entries = sortProvidersByName(providers)
  const availableVariants = modelVariantsFromProviders(entries as any, model)
  const variant = deps.sessionManager.getVariant(sessionId)
  const currentModelId = formatModelIdWithVariant(model, variant, availableVariants, true)
    const availableModels = buildAvailableModels(entries as any, { includeVariants: true })
  const modeState = await resolveModeState(deps, directory, sessionId)
  const currentModeId = modeState.currentModeId
  const modes = currentModeId ? { availableModes: modeState.availableModes, currentModeId } : undefined
  const commands = await deps.config.sdk.command.list({ directory }, { throwOnError: true }).then((resp: any) => resp.data!)
  const availableCommands = commands.map((command: any) => ({ name: command.name, description: command.description ?? "" }))
  const names = new Set(availableCommands.map((c: any) => c.name))
  if (!names.has("compact")) availableCommands.push({ name: "compact", description: "compact the session" })
  const mcpServers: Record<string, ConfigMCP.Info> = {}
  for (const server of params.mcpServers) {
    if ("type" in server) { mcpServers[server.name] = { url: server.url, headers: server.headers.reduce<Record<string, string>>((acc: any, { name, value }: any) => { acc[name] = value; return acc }, {}), type: "remote" } }
    else { mcpServers[server.name] = { type: "local", command: [server.command, ...server.args], environment: server.env.reduce<Record<string, string>>((acc: any, { name, value }: any) => { acc[name] = value; return acc }, {}) } }
  }
  await Promise.all(Object.entries(mcpServers).map(async ([key, mcp]) => { await deps.sdk.mcp.add({ directory, name: key, config: mcp }, { throwOnError: true }).catch((error: any) => { log.error("failed to add mcp server", { name: key, error }) }) }))
  setTimeout(() => { void deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "available_commands_update", availableCommands } }) }, 0)
  return {
    sessionId,
    models: { currentModelId: formatModelIdWithVariant(model, variant, availableVariants, true), availableModels },
    modes,
    configOptions: buildConfigOptions({ currentModelId: formatModelIdWithVariant(model, variant, availableVariants, true), availableModels, modes }),
    _meta: buildVariantMeta({ model, variant, availableVariants }),
  }
}
