// src/provider/anthropic.ts
import type { Provider, ProviderId, RouteMatch, ParsedRequest, ParsedResponse, SseEvent } from "./types.ts";
import type { ToolCall, ToolResult, TokenUsage } from "./canonical.ts";
import { homedir } from "node:os";
import { join, resolve } from "node:path";

const ID: ProviderId = "anthropic" as ProviderId;

function flattenContent(content: unknown): string {
  if (typeof content === "string") return content;
  if (Array.isArray(content)) {
    return content
      .map((b) => {
        if (b && typeof b === "object" && (b as { type?: string }).type === "text") {
          const t = (b as { text?: unknown }).text;
          return typeof t === "string" ? t : "";
        }
        return "";
      })
      .join("");
  }
  return "";
}

function matchesRoute(method: string, urlPath: string, headers: Headers): RouteMatch | null {
  if (method !== "POST") return null;
  if (urlPath === "/v1/messages") {
    const isStreaming = (headers.get("accept") ?? "").includes("text/event-stream");
    return { kind: "messages", isStreaming };
  }
  return null;
}

export const anthropicProvider: Provider = {
  id: ID,
  supportsCacheMarkers: true,
  defaultUpstreamUrl: "https://api.anthropic.com",
  matchesRoute,
  parseRequest(body, route) {
    const b = body as { model?: string; stream?: boolean } | null;
    return {
      raw: body,
      model: b?.model ?? null,
      isStreaming: route.isStreaming || Boolean(b?.stream),
    };
  },
  parseResponse(body) {
    const b = body as { stop_reason?: string } | null;
    return { raw: body, stopReason: b?.stop_reason ?? null };
  },
  parseSseEvent(line: string): SseEvent | null {
    if (!line.startsWith("data:")) return null;
    const json = line.slice(5).trim();
    if (json.length === 0) return null;
    let data: unknown;
    try { data = JSON.parse(json); } catch { return null; }
    const t = (data as { type?: unknown } | null)?.type;
    const eventType = typeof t === "string" ? t : "unknown";
    return { eventType, data };
  },
  extractToolCalls(req): readonly ToolCall[] {
    const body = req.raw as { messages?: unknown } | null;
    const messages = Array.isArray(body?.messages) ? body!.messages : [];
    const out: ToolCall[] = [];
    for (const m of messages) {
      const content = (m as { content?: unknown }).content;
      if (!Array.isArray(content)) continue;
      for (const blk of content) {
        const b = blk as { type?: string; id?: unknown; name?: unknown; input?: unknown };
        if (b.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") {
          out.push({ id: b.id, name: b.name, input: b.input ?? {} });
        }
      }
    }
    return out;
  },
  extractToolResults(req): readonly ToolResult[] {
    const body = req.raw as { messages?: unknown } | null;
    const messages = Array.isArray(body?.messages) ? body!.messages : [];
    const toolUses = new Map<string, { name: string; input: Record<string, unknown> }>();
    for (const m of messages) {
      if ((m as { role?: unknown }).role !== "assistant") continue;
      const content = (m as { content?: unknown }).content;
      if (!Array.isArray(content)) continue;
      for (const blk of content) {
        const b = blk as { type?: string; id?: unknown; name?: unknown; input?: unknown };
        if (b.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") {
          const input = (b.input && typeof b.input === "object")
            ? (b.input as Record<string, unknown>)
            : {};
          toolUses.set(b.id, { name: b.name, input });
        }
      }
    }
    const out: ToolResult[] = [];
    for (const m of messages) {
      const content = (m as { content?: unknown }).content;
      if (!Array.isArray(content)) continue;
      for (const blk of content) {
        const b = blk as {
          type?: string; tool_use_id?: unknown; content?: unknown; is_error?: unknown;
        };
        if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
          const tu = toolUses.get(b.tool_use_id);
          out.push({
            toolCallId: b.tool_use_id,
            content: flattenContent(b.content),
            isError: b.is_error === true,
            ...(tu ? { toolName: tu.name, toolInput: tu.input } : {}),
          });
        }
      }
    }
    return out;
  },
  extractLastTurnToolResults(req): readonly ToolResult[] {
    const body = req.raw as { messages?: unknown } | null;
    const messages = Array.isArray(body?.messages) ? body!.messages : [];
    const toolUses = new Map<string, { name: string; input: Record<string, unknown> }>();
    for (const m of messages) {
      if ((m as any).role !== "assistant") continue;
      const content = (m as any).content;
      if (!Array.isArray(content)) continue;
      for (const blk of content) {
        const b = blk as any;
        if (b.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") {
          toolUses.set(b.id, { name: b.name, input: b.input ?? {} });
        }
      }
    }
    let lastUser: unknown = null;
    for (let i = messages.length - 1; i >= 0; i--) {
      if ((messages[i] as any).role === "user") { lastUser = messages[i]; break; }
    }
    if (!lastUser) return [];
    const out: ToolResult[] = [];
    const content = (lastUser as any).content;
    if (!Array.isArray(content)) return [];
    for (const blk of content) {
      const b = blk as any;
      if (b.type !== "tool_result" || typeof b.tool_use_id !== "string") continue;
      const use = toolUses.get(b.tool_use_id);
      const contentStr = flattenContent(b.content);
      out.push({
        toolCallId: b.tool_use_id,
        content: contentStr,
        toolName: use?.name ?? "unknown",
        toolInput: use?.input ?? {},
        isError: b.is_error === true,
      });
    }
    return out;
  },
  extractTokenUsage(res): TokenUsage | null {
    const u = (res.raw as { usage?: unknown } | null)?.usage as Record<string, unknown> | undefined;
    if (!u) return null;
    const num = (k: string): number => {
      const v = u[k];
      return typeof v === "number" ? v : 0;
    };
    return {
      inputTokens: num("input_tokens"),
      outputTokens: num("output_tokens"),
      cacheHitInputTokens: num("cache_read_input_tokens"),
      cacheCreationInputTokens: num("cache_creation_input_tokens"),
    };
  },
  extractModel(req): string | null { return req.model; },
  extractSessionId(_req, headers): string | null {
    return headers.get("x-claude-code-session-id");
  },
  rewriteToolResult(body, toolCallId, newContent): unknown {
    if (body === null || typeof body !== "object") return body;
    if (Array.isArray(body)) {
      return body.map((item) =>
        (anthropicProvider.rewriteToolResult as (b: unknown, id: string, c: string) => unknown)(item, toolCallId, newContent),
      );
    }
    const o = { ...(body as Record<string, unknown>) };
    if (o.type === "tool_result" && o.tool_use_id === toolCallId) {
      o.content = newContent;
      return o;
    }
    for (const [k, v] of Object.entries(o)) {
      if (v !== null && typeof v === "object") {
        o[k] = (anthropicProvider.rewriteToolResult as (b: unknown, id: string, c: string) => unknown)(v, toolCallId, newContent);
      }
    }
    return o;
  },
  hintSurfacePath(_projectRoot: string): string | null {
    // Honor CLAUDE_CONFIG_DIR so a proxy launched against an alternate config
    // dir (e.g. test isolation via ~/.claude2) writes its hint legend THERE,
    // not into the global ~/.claude/CLAUDE.md that leaks into every plain
    // `claude` session. Unset => byte-identical to the legacy global path.
    const cfg = process.env.CLAUDE_CONFIG_DIR;
    const base = cfg ? resolve(cfg) : join(homedir(), ".claude");
    return join(base, "CLAUDE.md");
  },
  listModels(): readonly string[] {
    return [
      "claude-opus-4-7",
      "claude-sonnet-4-6",
      "claude-haiku-4-5-20251001",
    ];
  },
};
