import { type AgentSideConnection, type PermissionOption } from "@agentclientprotocol/sdk"
import * as Log from "@saeeol/core/util/log"
import { Hash } from "@saeeol/core/util/hash"
import { Filesystem } from "@/util/filesystem"
import { type Event, type ToolPart } from "@saeeol/sdk/v2"
import { Result } from "effect"
import { type ACPSessionManager } from "./session"
import { toToolKind, toLocations, getNewContent, log, decodeTodos, sendUsageUpdate } from "./agent-utils"
import type { AgentDeps } from "./agent-types"

const permLog = Log.create({ service: "acp-permissions" })

export function startEventSubscription(deps: AgentDeps, abort: AbortController, started: { value: boolean }) {
  if (started.value) return
  started.value = true
  runEventSubscription(deps, abort).catch((error: unknown) => { if (!abort.signal.aborted) log.error("event subscription failed", { error }) })
}

async function runEventSubscription(deps: AgentDeps, abort: AbortController) {
  while (true) {
    if (abort.signal.aborted) return
    const events = await deps.sdk.global.event({ signal: abort.signal })
    for await (const event of events.stream) {
      if (abort.signal.aborted) return
      const payload = event?.payload
      if (!payload) continue
      await handleEvent(deps, payload as Event).catch((error: unknown) => { log.error("failed to handle event", { error, type: payload.type }) })
    }
  }
}

async function handleEvent(deps: AgentDeps, event: Event) {
  switch (event.type) {
    case "permission.asked": return handlePermission(deps, event.properties)
    case "message.part.updated": return handleMessagePartUpdated(deps, event.properties)
    case "message.part.delta": return handleMessagePartDelta(deps, event.properties)
  }
}

async function handlePermission(deps: AgentDeps, permission: any) {
  const session = deps.sessionManager.tryGet(permission.sessionID)
  if (!session) return
  const prev = deps.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
  const next = prev
    .then(async () => {
      const directory = session.cwd
      const res = await deps.connection
        .requestPermission({ sessionId: permission.sessionID, toolCall: { toolCallId: permission.tool?.callID ?? permission.id, status: "pending", title: permission.permission, rawInput: permission.metadata, kind: toToolKind(permission.permission), locations: toLocations(permission.permission, permission.metadata) }, options: deps.permissionOptions })
        .catch(async (error: unknown) => { permLog.error("failed to request permission from ACP", { error, permissionID: permission.id, sessionID: permission.sessionID }); await deps.sdk.permission.reply({ requestID: permission.id, reply: "reject", directory }); return undefined })
      if (!res) return
      if (res.outcome.outcome !== "selected") { await deps.sdk.permission.reply({ requestID: permission.id, reply: "reject", directory }); return }
      if (res.outcome.optionId !== "reject" && permission.permission == "edit") {
        const metadata = permission.metadata || {}
        const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
        const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
        const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
        const newContent = getNewContent(content, diff)
        if (newContent) void deps.connection.writeTextFile({ sessionId: session.id, path: filepath, content: newContent })
      }
      await deps.sdk.permission.reply({ requestID: permission.id, reply: res.outcome.optionId as "once" | "always" | "reject", directory })
    })
    .catch((error: unknown) => { permLog.error("failed to handle permission", { error, permissionID: permission.id }) })
    .finally(() => { if (deps.permissionQueues.get(permission.sessionID) === next) deps.permissionQueues.delete(permission.sessionID) })
  deps.permissionQueues.set(permission.sessionID, next)
}

async function handleMessagePartUpdated(deps: AgentDeps, props: any) {
  const part = props.part
  const session = deps.sessionManager.tryGet(part.sessionID)
  if (!session) return
  const sessionId = session.id
  if (part.type === "tool") {
    await toolStart(deps, sessionId, part)
    switch (part.state.status) {
      case "pending": deps.bashSnapshots.delete(part.callID); return
      case "running": return handleToolRunning(deps, sessionId, part)
      case "completed": return handleToolCompleted(deps, sessionId, part)
      case "error": return handleToolError(deps, sessionId, part)
    }
  }
  if (part.type !== "text" && part.type !== "file") return
  return
}

async function handleToolRunning(deps: AgentDeps, sessionId: string, part: ToolPart) {
  const output = bashOutput(deps, part)
  const content: any[] = []
  if (output) {
    const hash = Hash.fast(output)
    if (part.tool === "bash") {
      if (deps.bashSnapshots.get(part.callID) === hash) {
        await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "tool_call_update", toolCallId: part.callID, status: "in_progress", kind: toToolKind(part.tool), title: part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input } }).catch((error: unknown) => { log.error("failed to send tool in_progress to ACP", { error }) })
        return
      }
      deps.bashSnapshots.set(part.callID, hash)
    }
    content.push({ type: "content", content: { type: "text", text: output } })
  }
  await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "tool_call_update", toolCallId: part.callID, status: "in_progress", kind: toToolKind(part.tool), title: part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, ...(content.length > 0 && { content }) } }).catch((error: unknown) => { log.error("failed to send tool in_progress to ACP", { error }) })
}

async function handleToolCompleted(deps: AgentDeps, sessionId: string, part: ToolPart) {
  if (part.state.status !== "completed") return
  deps.toolStarts.delete(part.callID)
  deps.bashSnapshots.delete(part.callID)
  const kind = toToolKind(part.tool)
  const content: any[] = [{ type: "content", content: { type: "text", text: part.state.output } }]
  if (kind === "edit") {
    const input = part.state.input
    content.push({ type: "diff", path: typeof input["filePath"] === "string" ? input["filePath"] : "", oldText: typeof input["oldString"] === "string" ? input["oldString"] : "", newText: typeof input["newString"] === "string" ? input["newString"] : typeof input["content"] === "string" ? input["content"] : "" })
  }
  if (part.tool === "todowrite") {
    const parsedTodos = decodeTodos(part.state.output)
    if (Result.isSuccess(parsedTodos)) {
      await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "plan", entries: parsedTodos.success.map((todo) => ({ priority: "medium" as const, status: (todo.status === "cancelled" ? "completed" : todo.status) as any, content: todo.content })) } }).catch((error: unknown) => { log.error("failed to send session update for todo", { error }) })
    } else { log.error("failed to parse todo output", { error: parsedTodos.failure }) }
  }
  await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "tool_call_update", toolCallId: part.callID, status: "completed", kind, content, title: part.state.title, rawInput: part.state.input, rawOutput: { output: part.state.output, metadata: part.state.metadata } } }).catch((error: unknown) => { log.error("failed to send tool completed to ACP", { error }) })
}

async function handleToolError(deps: AgentDeps, sessionId: string, part: ToolPart) {
  if (part.state.status !== "error") return
  deps.toolStarts.delete(part.callID)
  deps.bashSnapshots.delete(part.callID)
  await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "tool_call_update", toolCallId: part.callID, status: "failed", kind: toToolKind(part.tool), title: part.tool, rawInput: part.state.input, content: [{ type: "content", content: { type: "text", text: part.state.error } }], rawOutput: { error: part.state.error, metadata: part.state.metadata } } }).catch((error: unknown) => { log.error("failed to send tool error to ACP", { error }) })
}

async function handleMessagePartDelta(deps: AgentDeps, props: any) {
  const session = deps.sessionManager.tryGet(props.sessionID)
  if (!session) return
  const sessionId = session.id
  const message = await deps.sdk.session.message({ sessionID: props.sessionID, messageID: props.messageID, directory: session.cwd }, { throwOnError: true }).then((x: any) => x.data).catch((error: any) => { log.error("unexpected error when fetching message", { error }); return undefined })
  if (!message || message.info.role !== "assistant") return
  const part = message.parts.find((p: any) => p.id === props.partID)
  if (!part) return
  if (part.type === "text" && props.field === "text" && part.ignored !== true) {
    await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "agent_message_chunk", messageId: props.messageID, content: { type: "text", text: props.delta } } }).catch((error: any) => { log.error("failed to send text delta to ACP", { error }) })
    return
  }
  if (part.type === "reasoning" && props.field === "text") {
    await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "agent_thought_chunk", messageId: props.messageID, content: { type: "text", text: props.delta } } }).catch((error: any) => { log.error("failed to send reasoning delta to ACP", { error }) })
  }
}

export function bashOutput(deps: AgentDeps, part: ToolPart) {
  if (part.tool !== "bash") return
  if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
  const output = part.state.metadata["output"]
  if (typeof output !== "string") return
  return output
}

export async function toolStart(deps: AgentDeps, sessionId: string, part: ToolPart) {
  if (deps.toolStarts.has(part.callID)) return
  deps.toolStarts.add(part.callID)
  await deps.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "tool_call", toolCallId: part.callID, title: part.tool, kind: toToolKind(part.tool), status: "pending", locations: [], rawInput: {} } }).catch((error: any) => { log.error("failed to send tool pending to ACP", { error }) })
}
