import { randomUUID } from "node:crypto";

import type { SessionRun } from "@codeflowmu/protocol";
import type { McpServerConfig } from "@cursor/sdk";
import type {
  AgentCreateSpec,
  AgentSdkAdapter,
  AgentSendSpec,
} from "./AgentSdkAdapter.ts";
import {
  buildStaticGoogleTools,
  parseAllowedToolsFromEnv,
  resolveOneShotPythonBin,
} from "./FcopMcpOneShot.ts";
import { buildGeminiWorkspaceToolDeclarations } from "./geminiWorkspaceTools.ts";
import { routeGoogleToolCall } from "./GoogleToolCallRouter.ts";
import type {
  RunHandle,
  RuntimeEvent,
  RuntimeEventType,
  Unsubscribe,
} from "../types/state.ts";

export interface DoubaoArkAdapterOptions {
  apiKey?: string;
  baseUrl?: string;
  defaultModel?: string;
  fetchImpl?: typeof fetch;
}

type ArkResponseOutput = {
  type?: string;
  id?: string;
  call_id?: string;
  name?: string;
  arguments?: string;
  content?: Array<{ type?: string; text?: string }>;
};

type ArkResponsesBody = {
  id?: string;
  output_text?: string;
  output?: ArkResponseOutput[];
  usage?: Record<string, unknown>;
  error?: unknown;
};

type ArkFunctionTool = {
  type: "function";
  name: string;
  description: string;
  parameters: Record<string, unknown>;
};

type ProposedToolCall = {
  call_id: string;
  name: string;
  arguments: string;
  legacyText: boolean;
};

type StdioMcpServerConfig = Extract<McpServerConfig, { command: string }>;

function isStdioMcpConfig(
  config: McpServerConfig | undefined,
): config is StdioMcpServerConfig {
  return typeof (config as { command?: unknown } | undefined)?.command === "string";
}

function normalizeJsonSchema(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(normalizeJsonSchema);
  if (!value || typeof value !== "object") return value;
  const out: Record<string, unknown> = {};
  for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
    out[key] = key === "type" && typeof item === "string"
      ? item.toLowerCase()
      : normalizeJsonSchema(item);
  }
  return out;
}

function wireToolName(name: string): string {
  return name.replace(/\./g, "__").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
}

function finalText(body: ArkResponsesBody): string {
  if (typeof body.output_text === "string" && body.output_text.trim()) {
    return body.output_text;
  }
  return (body.output ?? [])
    .filter((item) => item.type === "message")
    .flatMap((item) => item.content ?? [])
    .filter((item) => item.type === "output_text" && typeof item.text === "string")
    .map((item) => item.text ?? "")
    .join("\n");
}

function legacyTextToolCalls(text: string, round: number): ProposedToolCall[] {
  const match = text.match(/<\|FunctionCallBegin\|>([\s\S]*?)<\|FunctionCallEnd\|>/);
  if (!match?.[1]) return [];
  try {
    const parsed = JSON.parse(match[1]) as unknown;
    if (!Array.isArray(parsed)) return [];
    return parsed.flatMap((item, index) => {
      if (!item || typeof item !== "object") return [];
      const record = item as Record<string, unknown>;
      const name = String(record["name"] ?? "").trim();
      if (!name) return [];
      const args = record["arguments"] ?? record["parameters"] ?? {};
      return [{
        call_id: `legacy-${round}-${index + 1}-${randomUUID()}`,
        name,
        arguments: typeof args === "string" ? args : JSON.stringify(args),
        legacyText: true,
      }];
    });
  } catch {
    return [];
  }
}

function firstMcpConfig(spec: AgentSendSpec): McpServerConfig | undefined {
  const key = Object.keys(spec.mcpServers ?? {})[0];
  return key ? spec.mcpServers?.[key] : undefined;
}

function buildArkTools(spec: AgentSendSpec): {
  declarations: ArkFunctionTool[];
  actualNames: Map<string, string>;
  mcpConfig?: StdioMcpServerConfig;
} {
  const candidateMcpConfig = firstMcpConfig(spec);
  const mcpConfig = isStdioMcpConfig(candidateMcpConfig)
    ? candidateMcpConfig
    : undefined;
  let allowed = parseAllowedToolsFromEnv(mcpConfig?.env);
  if (spec.runMode === "pm_self_report_only") {
    const selfReportTools = new Set([
      "write_report", "read_task", "read_report", "list_tasks", "list_reports",
    ]);
    allowed = allowed.filter((name) => selfReportTools.has(name));
  }

  const source = [
    ...buildStaticGoogleTools(allowed),
    ...buildGeminiWorkspaceToolDeclarations({
      includeWriteFile: false,
      includeListDir: true,
    }).filter((tool) => ["read_file", "grep_files", "list_dir"].includes(tool.name)),
  ];
  const actualNames = new Map<string, string>();
  const declarations: ArkFunctionTool[] = [];
  for (const tool of source) {
    const wireName = wireToolName(tool.name);
    if (actualNames.has(wireName)) continue;
    actualNames.set(wireName, tool.name);
    declarations.push({
      type: "function",
      name: wireName,
      description: tool.description,
      parameters: normalizeJsonSchema(tool.parameters) as Record<string, unknown>,
    });
  }
  return { declarations, actualNames, ...(mcpConfig ? { mcpConfig } : {}) };
}

export class DoubaoRunHandle implements RunHandle {
  readonly run_id: string;
  readonly session_id: string;
  readonly agent_id: string;
  readonly #startedAt = new Date().toISOString();
  readonly #listeners = new Set<(event: RuntimeEvent) => void>();
  readonly #abortController = new AbortController();
  readonly #settled: Promise<SessionRun>;
  #resolveSettled!: (run: SessionRun) => void;
  #active = true;
  #toolCallsCount = 0;

  constructor(input: { runId: string; sessionId: string; agentId: string }) {
    this.run_id = input.runId;
    this.session_id = input.sessionId;
    this.agent_id = input.agentId;
    this.#settled = new Promise((resolve) => { this.#resolveSettled = resolve; });
  }

  get signal(): AbortSignal { return this.#abortController.signal; }
  isActive(): boolean { return this.#active; }
  whenSettled(): Promise<SessionRun> { return this.#settled; }
  countToolCall(): void { this.#toolCallsCount += 1; }

  async cancel(reason: string): Promise<void> {
    if (!this.#active) return;
    this.#abortController.abort(reason);
    this.emit("runtime.session_cancelled", { reason });
    this.settle("cancelled");
  }

  onEvent(listener: (event: RuntimeEvent) => void): Unsubscribe {
    this.#listeners.add(listener);
    return () => this.#listeners.delete(listener);
  }

  emit(eventType: RuntimeEventType, payload?: unknown): void {
    const event: RuntimeEvent = {
      event_id: `evt-${randomUUID()}`,
      at: new Date().toISOString(),
      event_type: eventType,
      session_id: this.session_id,
      run_id: this.run_id,
      agent_id: this.agent_id,
      payload,
    };
    for (const listener of this.#listeners) {
      try { listener(event); } catch { this.#listeners.delete(listener); }
    }
  }

  settle(status: SessionRun["status"], error?: string): void {
    if (!this.#active && status !== "cancelled") return;
    this.#active = false;
    this.#resolveSettled({
      run_id: this.run_id,
      started_at: this.#startedAt,
      ended_at: new Date().toISOString(),
      status,
      tool_calls_count: this.#toolCallsCount,
      ...(error ? { sdk_error: error } : {}),
    } as SessionRun);
  }
}

/**
 * Volcengine Ark / Doubao adapter using the OpenAI-compatible Responses API.
 * The model proposes function calls; CodeFlowMu validates and executes them
 * through the shared governed tool router. Credentials stay runtime-only.
 */
export class DoubaoArkAdapter implements AgentSdkAdapter {
  readonly #knownAgents = new Set<string>();
  readonly #fetch: typeof fetch;
  readonly options: DoubaoArkAdapterOptions;

  constructor(options: DoubaoArkAdapterOptions = {}) {
    this.options = options;
    this.#fetch = options.fetchImpl ?? fetch;
  }

  #apiKey(): string {
    const value = this.options.apiKey ?? process.env.ARK_API_KEY;
    if (!value) throw new Error("ARK_API_KEY is required for DoubaoArkAdapter");
    return value;
  }

  #baseUrl(): string {
    return (this.options.baseUrl ?? process.env.ARK_BASE_URL ??
      "https://ark.cn-beijing.volces.com/api/v3").replace(/\/$/, "");
  }

  #model(hint?: string): string {
    const requested = hint?.trim() ?? "";
    const providerHint = /^(?:default|auto|auto-smart|inherit)$/i.test(requested)
      ? ""
      : requested;
    const model = providerHint || this.options.defaultModel?.trim() ||
      process.env.ARK_MODEL_ID?.trim();
    if (!model) throw new Error("ARK_MODEL_ID is required for DoubaoArkAdapter");
    return model;
  }

  async #responses(body: Record<string, unknown>, signal: AbortSignal): Promise<ArkResponsesBody> {
    const response = await this.#fetch(`${this.#baseUrl()}/responses`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${this.#apiKey()}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
      signal,
    });
    const raw = await response.text();
    let parsed: ArkResponsesBody;
    try { parsed = JSON.parse(raw) as ArkResponsesBody; }
    catch { parsed = { error: { message: raw.slice(0, 500) } }; }
    if (!response.ok) {
      throw new Error(`Ark HTTP ${response.status}: ${JSON.stringify(parsed.error ?? parsed)}`);
    }
    return parsed;
  }

  async create(spec: AgentCreateSpec): Promise<{ sdk_agent_id: string }> {
    const id = `doubao-agent-${spec.agentId}`;
    this.#knownAgents.add(id);
    return { sdk_agent_id: id };
  }

  async list(): Promise<string[]> { return [...this.#knownAgents]; }

  async resume(sdkAgentId: string): Promise<void> {
    if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(sdkAgentId)) {
      throw new Error(`invalid Doubao logical agent id: ${sdkAgentId}`);
    }
    this.#apiKey();
    this.#knownAgents.add(sdkAgentId);
  }

  async listModels(): Promise<string[]> { return [this.#model()]; }

  async send(spec: AgentSendSpec, sdkAgentId: string): Promise<RunHandle> {
    await this.resume(sdkAgentId);
    const handle = new DoubaoRunHandle({
      runId: `run-doubao-${randomUUID()}`,
      sessionId: spec.sessionId,
      agentId: spec.agentId,
    });

    setImmediate(async () => {
      try {
        const model = this.#model(spec.modelId);
        const projectRoot = spec.workspace ?? process.cwd();
        const tools = buildArkTools(spec);
        handle.emit("runtime.session_started", {
          provider: "volcengine-ark",
          model,
          tool_count: tools.declarations.length,
        });

        let response = await this.#responses({
          model,
          input: spec.text,
          ...(tools.declarations.length > 0 ? { tools: tools.declarations } : {}),
          store: true,
        }, handle.signal);
        const maxRounds = Math.max(1, spec.maxToolRounds ?? 10);
        let rounds = 0;

        while (handle.isActive()) {
          const structuredCalls: ProposedToolCall[] = (response.output ?? [])
            .filter((item) => item.type === "function_call" && item.call_id && item.name)
            .map((item) => ({
              call_id: item.call_id!,
              name: item.name!,
              arguments: item.arguments || "{}",
              legacyText: false,
            }));
          const calls = structuredCalls.length > 0
            ? structuredCalls
            : legacyTextToolCalls(finalText(response), rounds + 1);
          if (calls.length === 0) break;
          if (!response.id) throw new Error("Ark function_call response is missing response id");
          if (rounds >= maxRounds) {
            throw new Error(`Doubao tool round limit exceeded (${maxRounds})`);
          }
          rounds += 1;
          const outputs: Array<Record<string, unknown>> = [];
          const legacyOutputs: Array<Record<string, unknown>> = [];

          for (const call of calls) {
            const callId = call.call_id;
            const actualName = tools.actualNames.get(call.name);
            if (!actualName) {
              const denied = {
                ok: false,
                code: "TOOL_NOT_REGISTERED",
                error: `Tool is not registered for this session: ${call.name}`,
              };
              handle.emit("sdk.tool_call", { call_id: callId, name: call.name, arguments: call.arguments });
              handle.emit("sdk.tool_result", { call_id: callId, name: call.name, ...denied });
              if (call.legacyText) legacyOutputs.push({ name: call.name, ...denied });
              else outputs.push({
                type: "function_call_output",
                call_id: callId,
                output: JSON.stringify(denied),
              });
              continue;
            }
            let args: Record<string, unknown>;
            try {
              const parsed = JSON.parse(call.arguments || "{}");
              if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
                throw new Error("arguments must be a JSON object");
              }
              args = parsed as Record<string, unknown>;
            } catch (error) {
              const message = error instanceof Error ? error.message : String(error);
              handle.emit("sdk.tool_call", { call_id: callId, name: actualName, arguments: call.arguments });
              handle.emit("sdk.tool_result", { call_id: callId, name: actualName, ok: false, error: `Invalid tool arguments: ${message}` });
              outputs.push({
                type: "function_call_output",
                call_id: callId,
                output: JSON.stringify({ ok: false, code: "TOOL_ARGUMENTS_INVALID", error: message }),
              });
              if (call.legacyText) {
                outputs.pop();
                legacyOutputs.push({ name: actualName, ok: false, code: "TOOL_ARGUMENTS_INVALID", error: message });
              }
              continue;
            }

            handle.countToolCall();
            handle.emit("sdk.tool_call", { call_id: callId, name: actualName, arguments: args });
            const routed = await routeGoogleToolCall({
              projectRoot,
              agentId: spec.agentId,
              toolName: actualName,
              args,
              callId,
              useFcopOneShot: Boolean(tools.mcpConfig?.command),
              ...(tools.mcpConfig?.command
                ? { pythonBin: resolveOneShotPythonBin(tools.mcpConfig) }
                : {}),
              sessionId: spec.sessionId,
              taskId: spec.pinnedTaskId,
              threadKey: spec.pinnedThreadKey,
              promptRoutingText: spec.text,
              turnIndex: rounds,
              runId: handle.run_id,
            });
            handle.emit("sdk.tool_result", {
              call_id: callId,
              name: actualName,
              ...routed,
            });
            outputs.push({
              type: "function_call_output",
              call_id: callId,
              output: JSON.stringify(routed.ok
                ? { ok: true, output: routed.output ?? "" }
                : { ok: false, code: routed.code, error: routed.error, approval_id: routed.approval_id }),
            });
            if (call.legacyText) {
              outputs.pop();
              legacyOutputs.push({
                name: actualName,
                ...(routed.ok
                  ? { ok: true, output: routed.output ?? "" }
                  : { ok: false, code: routed.code, error: routed.error, approval_id: routed.approval_id }),
              });
            }
          }

          response = await this.#responses(
            legacyOutputs.length > 0
              ? {
                  model,
                  previous_response_id: response.id,
                  input:
                    "CodeFlowMu 已按权限边界执行你请求的工具。以下是不可伪造的工具结果：\n" +
                    JSON.stringify(legacyOutputs) +
                    "\n请基于结果直接回答用户；不要再次输出 FunctionCall 标记，除非确实还需另一个工具。",
                  ...(tools.declarations.length > 0 ? { tools: tools.declarations } : {}),
                  store: true,
                }
              : {
                  model,
                  previous_response_id: response.id,
                  input: outputs,
                  ...(tools.declarations.length > 0 ? { tools: tools.declarations } : {}),
                  store: true,
                },
            handle.signal,
          );
        }

        const content = finalText(response);
        handle.emit("sdk.assistant", {
          provider: "volcengine-ark",
          response_id: response.id ?? null,
          content,
          usage: response.usage ?? null,
        });
        handle.emit("sdk.result", { status: "completed", response_id: response.id ?? null });
        handle.emit("runtime.session_ended", { status: "completed" });
        handle.settle("finished");
      } catch (error) {
        if (!handle.isActive()) return;
        const message = error instanceof Error ? error.message : String(error);
        handle.emit("sdk.status", { status: "failed", error: message });
        handle.emit("runtime.session_ended", { status: "failed" });
        handle.settle("failed", message);
      }
    });
    return handle;
  }
}
