import { parse as parseDataUrl } from "@/boxes/dataurl"
import { RequestError, type Role, type ToolKind, type Usage, type PlanEntry } from "@agentclientprotocol/sdk"
import * as Log from "@saeeol/core/util/log"
import { pathToFileURL } from "url"
import { Hash } from "@saeeol/core/util/hash"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { MessageV2 } from "@/session/message-v2"
import { Result, Schema } from "effect"
import { Todo } from "@/session/todo"
import { applyPatch } from "diff"
import type { AssistantMessage, SaeeolClient } from "@saeeol/sdk/v2"
import { InstallationVersion } from "@saeeol/core/installation/version"
import { fetchDefaultModel } from "@saeeol/gateway"
import type { ToolCallContent } from "@agentclientprotocol/sdk"
import type { ACPConfig } from "./types"
import type { ACPSessionManager } from "./session"
import type { SessionConfigOption, ModeOption, ModelOption } from "./agent-types"

export { type ModeOption, type ModelOption } from "./agent-types"

const decodeTodos = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Todo.Info)))
const DEFAULT_VARIANT_VALUE = "default"
export const log = Log.create({ service: "acp-agent" })

export async function getContextLimit(sdk: SaeeolClient, providerID: ProviderID, modelID: ModelID, directory: string) {
  const providers = await sdk.config
    .providers({ directory })
    .then((x) => x.data?.providers ?? [])
    .catch((error) => { log.error("failed to get providers for context limit", { error }); return [] })
  const provider = providers.find((p) => p.id === providerID)
  return provider?.models[modelID]?.limit.context ?? null
}

export async function sendUsageUpdate(
  connection: { sessionUpdate: (p: any) => Promise<any> },
  sdk: SaeeolClient,
  sessionID: string,
  directory: string,
) {
  const messages = await sdk.session
    .messages({ sessionID, directory }, { throwOnError: true })
    .then((x) => x.data)
    .catch((error) => { log.error("failed to fetch messages for usage update", { error }); return undefined })
  if (!messages) return
  const assistantMessages = messages.filter(
    (m): m is { info: AssistantMessage; parts: any[] } => m.info.role === "assistant",
  )
  const lastAssistant = assistantMessages[assistantMessages.length - 1]
  if (!lastAssistant) return
  const msg = lastAssistant.info
  if (!msg.providerID || !msg.modelID) return
  const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory)
  if (!size) return
  const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
  const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
  await connection
    .sessionUpdate({ sessionId: sessionID, update: { sessionUpdate: "usage_update", used, size, cost: { amount: totalCost, currency: "USD" } } })
    .catch((error) => { log.error("failed to send usage update", { error }) })
}

export function toToolKind(toolName: string): ToolKind {
  const tool = toolName.toLocaleLowerCase()
  switch (tool) {
    case "bash": return "execute"
    case "webfetch": return "fetch"
    case "edit": case "patch": case "write": return "edit"
    case "grep": case "glob": case "context7_resolve_library_id": case "context7_get_library_docs": return "search"
    case "read": return "read"
    default: return "other"
  }
}

export function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
  const tool = toolName.toLocaleLowerCase()
  switch (tool) {
    case "read": case "edit": case "write": return input["filePath"] ? [{ path: input["filePath"] }] : []
    case "glob": case "grep": return input["path"] ? [{ path: input["path"] }] : []
    default: return []
  }
}

export function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
  const result = applyPatch(fileOriginal, unifiedDiff)
  if (result === false) { log.error("Failed to apply unified diff (context mismatch)"); return undefined }
  return result
}

export function parseUri(uri: string): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
  try {
    if (uri.startsWith("file://")) {
      const path = uri.slice(7)
      const name = path.split("/").pop() || path
      return { type: "file", url: uri, filename: name, mime: "text/plain" }
    }
    if (uri.startsWith("zed://")) {
      const url = new URL(uri)
      const path = url.searchParams.get("path")
      if (path) { const name = path.split("/").pop() || path; return { type: "file", url: pathToFileURL(path).href, filename: name, mime: "text/plain" } }
    }
    return { type: "text", text: uri }
  } catch { return { type: "text", text: uri } }
}

export function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
  return [...providers].sort((a, b) => { const na = a.name.toLowerCase(); const nb = b.name.toLowerCase(); return na < nb ? -1 : na > nb ? 1 : 0 })
}

export function modelVariantsFromProviders(
  providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
  model: { providerID: ProviderID; modelID: ModelID },
): string[] {
  const provider = providers.find((entry) => entry.id === model.providerID)
  if (!provider) return []
  const modelInfo = provider.models[model.modelID]
  if (!modelInfo?.variants) return []
  return Object.keys(modelInfo.variants)
}

export function buildAvailableModels(
  providers: Array<{ id: string; name: string; models: Record<string, any> }>,
  options: { includeVariants?: boolean } = {},
): ModelOption[] {
  const includeVariants = options.includeVariants ?? false
  return providers.flatMap((provider) => {
    const unsorted: Array<{ id: string; name: string; variants?: Record<string, any> }> = Object.values(provider.models)
    const models = Provider.sort(unsorted)
    return models.flatMap((model: any) => {
      const base: ModelOption = { modelId: `${provider.id}/${model.id}`, name: `${provider.name}/${model.name}` }
      if (!includeVariants || !model.variants) return [base]
      const variants = Object.keys(model.variants).filter((v) => v !== DEFAULT_VARIANT_VALUE)
      return [base, ...variants.map((variant) => ({ modelId: `${provider.id}/${model.id}/${variant}`, name: `${provider.name}/${model.name} (${variant})` }))]
    })
  })
}

export function formatModelIdWithVariant(
  model: { providerID: ProviderID; modelID: ModelID },
  variant: string | undefined,
  availableVariants: string[],
  includeVariant: boolean,
) {
  const base = `${model.providerID}/${model.modelID}`
  if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
  return `${base}/${variant}`
}

export function buildVariantMeta(input: { model: { providerID: ProviderID; modelID: ModelID }; variant?: string; availableVariants: string[] }) {
  return { saeeol: { modelId: `${input.model.providerID}/${input.model.modelID}`, variant: input.variant ?? null, availableVariants: input.availableVariants } }
}

export function parseModelSelection(
  modelId: string,
  providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } {
  const parsed = Provider.parseModel(modelId)
  const provider = providers.find((p) => p.id === parsed.providerID)
  if (!provider) return { model: parsed, variant: undefined }
  if (provider.models[parsed.modelID]) return { model: parsed, variant: undefined }
  const segments = parsed.modelID.split("/")
  if (segments.length > 1) {
    const candidateVariant = segments[segments.length - 1]
    const baseModelId = segments.slice(0, -1).join("/")
    const baseModelInfo = provider.models[baseModelId]
    if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants)
      return { model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) }, variant: candidateVariant }
  }
  return { model: parsed, variant: undefined }
}

export function buildConfigOptions(input: {
  currentModelId: string; availableModels: ModelOption[]; modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined
}): SessionConfigOption[] {
  const options: SessionConfigOption[] = [{ id: "model", name: "Model", category: "model", type: "select", currentValue: input.currentModelId, options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })) }]
  if (input.modes) options.push({ id: "mode", name: "Session Mode", category: "mode", type: "select", currentValue: input.modes.currentModeId, options: input.modes.availableModes.map((m) => ({ value: m.id, name: m.name, ...(m.description ? { description: m.description } : {}) })) })
  return options
}

export async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> {
  const sdk = config.sdk
  const configured = config.defaultModel
  if (configured) return configured
  const directory = cwd ?? process.cwd()
  const specified = await sdk.config.get({ directory }, { throwOnError: true }).then((resp) => { const cfg = resp.data; if (!cfg || !cfg.model) return undefined; return Provider.parseModel(cfg.model) }).catch((error) => { log.error("failed to load user config for default model", { error }); return undefined })
  const providers = await sdk.config.providers({ directory }, { throwOnError: true }).then((x) => x.data?.providers ?? []).catch((error) => { log.error("failed to list providers for default model", { error }); return [] })
  if (specified && providers.length) { const provider = providers.find((p) => p.id === specified.providerID); if (provider && provider.models[specified.modelID]) return specified }
  if (specified && !providers.length) return specified
  const saeeolProvider = providers.find((p) => p.id === "saeeol")
  if (saeeolProvider) { const [best] = Provider.sort(Object.values(saeeolProvider.models)); if (best) return { providerID: ProviderID.make(best.providerID), modelID: ModelID.make(best.id) } }
  const models = providers.flatMap((p) => Object.values(p.models))
  const [best] = Provider.sort(models)
  if (best) return { providerID: ProviderID.make(best.providerID), modelID: ModelID.make(best.id) }
  if (specified) return specified
  if (providers.some((p) => p.id === "saeeol")) { const freeModel = await fetchDefaultModel(); return { providerID: ProviderID.saeeol, modelID: ModelID.make(freeModel) } }
  throw new Error("no model available: no providers are configured and no default model is set")
}

export function buildUsage(msg: AssistantMessage): Usage {
  return {
    totalTokens: msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + (msg.tokens.cache?.read ?? 0) + (msg.tokens.cache?.write ?? 0),
    inputTokens: msg.tokens.input, outputTokens: msg.tokens.output,
    thoughtTokens: msg.tokens.reasoning || undefined, cachedReadTokens: msg.tokens.cache?.read || undefined, cachedWriteTokens: msg.tokens.cache?.write || undefined,
  }
}

export { decodeTodos, DEFAULT_VARIANT_VALUE }
