// Message Adapter — parses provider-specific NDJSON events into common AgentMessage format
// Standalone for Bun process (no Svelte/Tauri deps). Mirrors the Tauri adapter layer.

// ── Type guards ──────────────────────────────────────────────────────────────

function str(v: unknown, fallback = ""): string {
  return typeof v === "string" ? v : fallback;
}

function num(v: unknown, fallback = 0): number {
  return typeof v === "number" ? v : fallback;
}

// ── Types ────────────────────────────────────────────────────────────────────

export type AgentMessageType =
  | "init"
  | "text"
  | "thinking"
  | "tool_call"
  | "tool_result"
  | "status"
  | "compaction"
  | "cost"
  | "error"
  | "unknown";

export interface AgentMessage {
  id: string;
  type: AgentMessageType;
  parentId?: string;
  content: unknown;
  timestamp: number;
}

export type ProviderId = "claude" | "codex" | "ollama";

// ── Public API ───────────────────────────────────────────────────────────────

/** Parse a raw NDJSON event from a sidecar runner into AgentMessage[] */
export function parseMessage(
  provider: ProviderId,
  raw: Record<string, unknown>,
): AgentMessage[] {
  switch (provider) {
    case "claude":
      return adaptClaudeMessage(raw);
    case "codex":
      return adaptCodexMessage(raw);
    case "ollama":
      return adaptOllamaMessage(raw);
    default:
      return adaptClaudeMessage(raw);
  }
}

// ── Claude adapter ───────────────────────────────────────────────────────────

function adaptClaudeMessage(raw: Record<string, unknown>): AgentMessage[] {
  const uuid = str(raw.uuid) || crypto.randomUUID();
  const ts = Date.now();
  const parentId =
    typeof raw.parent_tool_use_id === "string"
      ? raw.parent_tool_use_id
      : undefined;

  switch (raw.type) {
    case "system":
      return adaptClaudeSystem(raw, uuid, ts);
    case "assistant":
      return adaptClaudeAssistant(raw, uuid, ts, parentId);
    case "user":
      return adaptClaudeUser(raw, uuid, ts, parentId);
    case "result":
      return adaptClaudeResult(raw, uuid, ts);
    default:
      return [{ id: uuid, type: "unknown", content: raw, timestamp: ts }];
  }
}

function adaptClaudeSystem(
  raw: Record<string, unknown>,
  uuid: string,
  ts: number,
): AgentMessage[] {
  const subtype = str(raw.subtype);

  if (subtype === "init") {
    return [
      {
        id: uuid,
        type: "init",
        content: {
          sessionId: str(raw.session_id),
          model: str(raw.model),
          cwd: str(raw.cwd),
          tools: Array.isArray(raw.tools)
            ? raw.tools.filter((t): t is string => typeof t === "string")
            : [],
        },
        timestamp: ts,
      },
    ];
  }

  if (subtype === "compact_boundary") {
    const meta =
      typeof raw.compact_metadata === "object" && raw.compact_metadata !== null
        ? (raw.compact_metadata as Record<string, unknown>)
        : {};
    return [
      {
        id: uuid,
        type: "compaction",
        content: {
          trigger: str(meta.trigger, "auto"),
          preTokens: num(meta.pre_tokens),
        },
        timestamp: ts,
      },
    ];
  }

  return [
    {
      id: uuid,
      type: "status",
      content: {
        subtype,
        message: typeof raw.status === "string" ? raw.status : undefined,
      },
      timestamp: ts,
    },
  ];
}

function adaptClaudeAssistant(
  raw: Record<string, unknown>,
  uuid: string,
  ts: number,
  parentId?: string,
): AgentMessage[] {
  const msg =
    typeof raw.message === "object" && raw.message !== null
      ? (raw.message as Record<string, unknown>)
      : undefined;
  if (!msg) return [];

  const content = Array.isArray(msg.content)
    ? (msg.content as Array<Record<string, unknown>>)
    : undefined;
  if (!content) return [];

  const messages: AgentMessage[] = [];
  for (const block of content) {
    switch (block.type) {
      case "text":
        messages.push({
          id: `${uuid}-text-${messages.length}`,
          type: "text",
          parentId,
          content: { text: str(block.text) },
          timestamp: ts,
        });
        break;
      case "thinking":
        messages.push({
          id: `${uuid}-think-${messages.length}`,
          type: "thinking",
          parentId,
          content: { text: str(block.thinking ?? block.text) },
          timestamp: ts,
        });
        break;
      case "tool_use":
        messages.push({
          id: `${uuid}-tool-${messages.length}`,
          type: "tool_call",
          parentId,
          content: {
            toolUseId: str(block.id),
            name: str(block.name),
            input: block.input,
          },
          timestamp: ts,
        });
        break;
    }
  }
  return messages;
}

function adaptClaudeUser(
  raw: Record<string, unknown>,
  uuid: string,
  ts: number,
  parentId?: string,
): AgentMessage[] {
  const msg =
    typeof raw.message === "object" && raw.message !== null
      ? (raw.message as Record<string, unknown>)
      : undefined;
  if (!msg) return [];

  const content = Array.isArray(msg.content)
    ? (msg.content as Array<Record<string, unknown>>)
    : undefined;
  if (!content) return [];

  const messages: AgentMessage[] = [];
  for (const block of content) {
    if (block.type === "tool_result") {
      messages.push({
        id: `${uuid}-result-${messages.length}`,
        type: "tool_result",
        parentId,
        content: {
          toolUseId: str(block.tool_use_id),
          output: block.content ?? raw.tool_use_result,
        },
        timestamp: ts,
      });
    }
  }
  return messages;
}

function adaptClaudeResult(
  raw: Record<string, unknown>,
  uuid: string,
  ts: number,
): AgentMessage[] {
  const usage =
    typeof raw.usage === "object" && raw.usage !== null
      ? (raw.usage as Record<string, unknown>)
      : undefined;
  return [
    {
      id: uuid,
      type: "cost",
      content: {
        totalCostUsd: num(raw.total_cost_usd),
        durationMs: num(raw.duration_ms),
        inputTokens: num(usage?.input_tokens),
        outputTokens: num(usage?.output_tokens),
        numTurns: num(raw.num_turns),
        isError: raw.is_error === true,
        result: typeof raw.result === "string" ? raw.result : undefined,
      },
      timestamp: ts,
    },
  ];
}

// ── Codex adapter ────────────────────────────────────────────────────────────

function adaptCodexMessage(raw: Record<string, unknown>): AgentMessage[] {
  const ts = Date.now();
  const uuid = crypto.randomUUID();

  switch (raw.type) {
    case "thread.started":
      return [
        {
          id: uuid,
          type: "init",
          content: {
            sessionId: str(raw.thread_id),
            model: "",
            cwd: "",
            tools: [],
          },
          timestamp: ts,
        },
      ];
    case "turn.started":
      return [
        {
          id: uuid,
          type: "status",
          content: { subtype: "turn_started" },
          timestamp: ts,
        },
      ];
    case "turn.completed": {
      const usage =
        typeof raw.usage === "object" && raw.usage !== null
          ? (raw.usage as Record<string, unknown>)
          : {};
      return [
        {
          id: uuid,
          type: "cost",
          content: {
            totalCostUsd: 0,
            durationMs: 0,
            inputTokens: num(usage.input_tokens),
            outputTokens: num(usage.output_tokens),
            numTurns: 1,
            isError: false,
          },
          timestamp: ts,
        },
      ];
    }
    case "turn.failed":
      return [
        {
          id: uuid,
          type: "error",
          content: {
            message: str(
              (raw.error as Record<string, unknown>)?.message,
              "Turn failed",
            ),
          },
          timestamp: ts,
        },
      ];
    case "item.started":
    case "item.updated":
    case "item.completed":
      return adaptCodexItem(raw, uuid, ts);
    case "error":
      return [
        {
          id: uuid,
          type: "error",
          content: { message: str(raw.message, "Unknown error") },
          timestamp: ts,
        },
      ];
    default:
      return [{ id: uuid, type: "unknown", content: raw, timestamp: ts }];
  }
}

function adaptCodexItem(
  raw: Record<string, unknown>,
  uuid: string,
  ts: number,
): AgentMessage[] {
  const item =
    typeof raw.item === "object" && raw.item !== null
      ? (raw.item as Record<string, unknown>)
      : {};
  const itemType = str(item.type);
  const eventType = str(raw.type);

  switch (itemType) {
    case "agent_message":
      if (eventType !== "item.completed") return [];
      return [
        {
          id: uuid,
          type: "text",
          content: { text: str(item.text) },
          timestamp: ts,
        },
      ];
    case "reasoning":
      if (eventType !== "item.completed") return [];
      return [
        {
          id: uuid,
          type: "thinking",
          content: { text: str(item.text) },
          timestamp: ts,
        },
      ];
    case "command_execution": {
      // Fix #13: Only emit tool_call on item.started, tool_result on item.completed
      // Prevents duplicate tool_call messages.
      const messages: AgentMessage[] = [];
      const toolUseId = str(item.id, uuid);
      if (eventType === "item.started") {
        messages.push({
          id: `${uuid}-call`,
          type: "tool_call",
          content: {
            toolUseId,
            name: "Bash",
            input: { command: str(item.command) },
          },
          timestamp: ts,
        });
      }
      if (eventType === "item.completed") {
        messages.push({
          id: `${uuid}-result`,
          type: "tool_result",
          content: {
            toolUseId,
            output: str(item.aggregated_output),
          },
          timestamp: ts,
        });
      }
      return messages;
    }
    default:
      return [];
  }
}

// ── Ollama adapter ───────────────────────────────────────────────────────────

function adaptOllamaMessage(raw: Record<string, unknown>): AgentMessage[] {
  const ts = Date.now();
  const uuid = crypto.randomUUID();

  switch (raw.type) {
    case "system": {
      const subtype = str(raw.subtype);
      if (subtype === "init") {
        return [
          {
            id: uuid,
            type: "init",
            content: {
              sessionId: str(raw.session_id),
              model: str(raw.model),
              cwd: str(raw.cwd),
              tools: [],
            },
            timestamp: ts,
          },
        ];
      }
      return [
        {
          id: uuid,
          type: "status",
          content: {
            subtype,
            message: typeof raw.status === "string" ? raw.status : undefined,
          },
          timestamp: ts,
        },
      ];
    }
    case "chunk": {
      const messages: AgentMessage[] = [];
      const msg =
        typeof raw.message === "object" && raw.message !== null
          ? (raw.message as Record<string, unknown>)
          : {};
      const done = raw.done === true;
      const thinking = str(msg.thinking);
      if (thinking) {
        messages.push({
          id: `${uuid}-think`,
          type: "thinking",
          content: { text: thinking },
          timestamp: ts,
        });
      }
      const text = str(msg.content);
      if (text) {
        messages.push({
          id: `${uuid}-text`,
          type: "text",
          content: { text },
          timestamp: ts,
        });
      }
      if (done) {
        const evalDuration = num(raw.eval_duration);
        const durationMs =
          evalDuration > 0 ? Math.round(evalDuration / 1_000_000) : 0;
        messages.push({
          id: `${uuid}-cost`,
          type: "cost",
          content: {
            totalCostUsd: 0,
            durationMs,
            inputTokens: num(raw.prompt_eval_count),
            outputTokens: num(raw.eval_count),
            numTurns: 1,
            isError: str(raw.done_reason) === "error",
          },
          timestamp: ts,
        });
      }
      return messages;
    }
    case "error":
      return [
        {
          id: uuid,
          type: "error",
          content: { message: str(raw.message, "Ollama error") },
          timestamp: ts,
        },
      ];
    default:
      return [{ id: uuid, type: "unknown", content: raw, timestamp: ts }];
  }
}
