import { streamSseResponse } from "../lib/sse";
import type { JsonValue, WorkbenchItem } from "../types";

export interface ManualToolResult {
  tool_name: string;
  content: string;
  parsed: JsonValue | null;
  is_json: boolean;
}

export interface ManualToolDefinition {
  name: string;
  description: string;
  parameters: Record<string, unknown>;
  kind?: string;
}

export interface ManualToolJob {
  job_id: string;
  tool_name: string;
  status: "queued" | "running" | "completed" | "failed";
  created_at: string;
  started_at: string | null;
  finished_at: string | null;
  result: ManualToolResult | null;
  error: string | null;
  error_code: number | null;
}

export interface ManualToolAsyncOptions {
  timeoutMs?: number;
}

function cloneJsonValue(value: JsonValue | undefined): JsonValue | undefined {
  if (value === undefined) return undefined;
  try {
    return JSON.parse(JSON.stringify(value)) as JsonValue;
  } catch {
    return value;
  }
}

export async function decodeJsonError(resp: Response): Promise<string> {
  try {
    const payload = (await resp.json()) as { error?: unknown };
    if (payload && typeof payload === "object" && typeof payload.error === "string") {
      return payload.error;
    }
  } catch {
    // Fall back to status code.
  }
  return `request failed (${resp.status})`;
}

function normalizeManualToolResult(
  payload: Partial<ManualToolResult> | null | undefined,
  fallbackName: string,
): ManualToolResult {
  return {
    tool_name: typeof payload?.tool_name === "string" ? payload.tool_name : fallbackName,
    content: typeof payload?.content === "string" ? payload.content : "",
    parsed:
      payload?.parsed === null || payload?.parsed === undefined
        ? null
        : (cloneJsonValue(payload.parsed as JsonValue) ?? null),
    is_json: payload?.is_json === true,
  };
}

function normalizeManualToolJob(payload: unknown, fallbackName: string): ManualToolJob {
  const record = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
  const toolName = typeof record.tool_name === "string" ? record.tool_name : fallbackName;
  const statusValue = typeof record.status === "string" ? record.status : "queued";
  return {
    job_id: typeof record.job_id === "string" ? record.job_id : "",
    tool_name: toolName,
    status:
      statusValue === "running" || statusValue === "completed" || statusValue === "failed"
        ? statusValue
        : "queued",
    created_at: typeof record.created_at === "string" ? record.created_at : "",
    started_at: typeof record.started_at === "string" ? record.started_at : null,
    finished_at: typeof record.finished_at === "string" ? record.finished_at : null,
    result: normalizeManualToolJobResult(record.result, toolName),
    error: typeof record.error === "string" ? record.error : null,
    error_code: typeof record.error_code === "number" ? record.error_code : null,
  };
}

function normalizeManualToolAsyncOptions(
  options: ManualToolAsyncOptions,
): Required<ManualToolAsyncOptions> {
  const timeoutMs =
    typeof options.timeoutMs === "number" &&
    Number.isFinite(options.timeoutMs) &&
    options.timeoutMs >= 100
      ? Math.round(options.timeoutMs)
      : 30000;
  return { timeoutMs };
}

function normalizeManualToolJobResult(
  payload: unknown,
  fallbackName: string,
): ManualToolResult | null {
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    return null;
  }
  const record = payload as Record<string, unknown>;
  const looksNormalized =
    typeof record.tool_name === "string" ||
    typeof record.content === "string" ||
    record.parsed !== undefined ||
    typeof record.is_json === "boolean";
  if (looksNormalized) {
    return normalizeManualToolResult(record as Partial<ManualToolResult>, fallbackName);
  }
  return {
    tool_name: fallbackName,
    content: JSON.stringify(record),
    parsed: cloneJsonValue(record as JsonValue) ?? null,
    is_json: true,
  };
}

function completedManualToolResult(
  job: ManualToolJob,
  toolName: string,
): ManualToolResult | undefined {
  if (job.status !== "completed") return undefined;
  return job.result ?? normalizeManualToolResult(null, toolName);
}

function assertManualToolJobDidNotFail(job: ManualToolJob, toolName: string): void {
  if (job.status === "failed") {
    throw new Error(job.error || `${toolName} failed`);
  }
}

async function streamManualToolJobUpdates(
  jobId: string,
  toolName: string,
  timeoutMs: number,
): Promise<ManualToolJob | null> {
  let current: ManualToolJob | null = null;
  const controller = new AbortController();
  const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);

  try {
    const resp = await fetch(`/tools/jobs/${encodeURIComponent(jobId)}/stream`, {
      cache: "no-store",
      signal: controller.signal,
    });
    if (!resp.ok) throw new Error(await decodeJsonError(resp));

    await streamSseResponse(resp, (raw) => {
      let payload: unknown = null;
      try {
        payload = JSON.parse(raw);
      } catch {
        return;
      }
      if (!payload || typeof payload !== "object") return;
      const record = payload as Record<string, unknown>;
      if (record.type !== "tool.job") return;
      current = normalizeManualToolJob(record.job, toolName);
    });
  } catch (error) {
    if (controller.signal.aborted) {
      throw new Error(`${toolName} timed out`);
    }
    throw error;
  } finally {
    window.clearTimeout(timeoutId);
  }

  return current;
}

async function waitForManualToolJob(
  job: ManualToolJob,
  toolName: string,
  timeoutMs: number,
): Promise<ManualToolResult> {
  const immediate = completedManualToolResult(job, toolName);
  if (immediate) return immediate;
  assertManualToolJobDidNotFail(job, toolName);

  const streamed = await streamManualToolJobUpdates(job.job_id, toolName, timeoutMs);
  const streamedResult = streamed ? completedManualToolResult(streamed, toolName) : undefined;
  if (streamedResult) return streamedResult;
  if (streamed) assertManualToolJobDidNotFail(streamed, toolName);

  const current = await getManualToolJob(job.job_id);
  const finalResult = completedManualToolResult(current, toolName);
  if (finalResult) return finalResult;
  assertManualToolJobDidNotFail(current, toolName);
  throw new Error(`${toolName} ended before completion`);
}

export async function callManualTool(
  toolName: string,
  argumentsValue: Record<string, JsonValue> = {},
): Promise<ManualToolResult> {
  const name = toolName.trim();
  if (!name) throw new Error("tool name is required");

  const cloned = cloneJsonValue(argumentsValue as JsonValue);
  const safeArguments =
    cloned && typeof cloned === "object" && !Array.isArray(cloned)
      ? (cloned as Record<string, JsonValue>)
      : {};

  const resp = await fetch("/tools/call", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ tool_name: name, arguments: safeArguments }),
  });
  if (!resp.ok) throw new Error(await decodeJsonError(resp));

  const payload = await resp.json();
  if (!payload || typeof payload !== "object") {
    throw new Error("invalid tool response");
  }

  return normalizeManualToolResult(payload as Partial<ManualToolResult>, name);
}

export async function startManualToolCall(
  toolName: string,
  argumentsValue: Record<string, JsonValue> = {},
): Promise<ManualToolJob> {
  const name = toolName.trim();
  if (!name) throw new Error("tool name is required");

  const cloned = cloneJsonValue(argumentsValue as JsonValue);
  const safeArguments =
    cloned && typeof cloned === "object" && !Array.isArray(cloned)
      ? (cloned as Record<string, JsonValue>)
      : {};

  const resp = await fetch("/tools/call", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ tool_name: name, arguments: safeArguments, async: true }),
  });
  if (!resp.ok) throw new Error(await decodeJsonError(resp));

  const payload = await resp.json();
  if (!payload || typeof payload !== "object") {
    throw new Error("invalid tool job response");
  }

  const job = normalizeManualToolJob(payload, name);
  if (!job.job_id) throw new Error("tool job id is required");
  return job;
}

export async function getManualToolJob(jobId: string): Promise<ManualToolJob> {
  const key = jobId.trim();
  if (!key) throw new Error("tool job id is required");

  const resp = await fetch(`/tools/jobs/${encodeURIComponent(key)}`, { cache: "no-store" });
  if (!resp.ok) throw new Error(await decodeJsonError(resp));

  const payload = await resp.json();
  if (!payload || typeof payload !== "object") {
    throw new Error("invalid tool job response");
  }

  return normalizeManualToolJob(payload, "");
}

export async function callManualToolAsync(
  toolName: string,
  argumentsValue: Record<string, JsonValue> = {},
  options: ManualToolAsyncOptions = {},
): Promise<ManualToolResult> {
  const { timeoutMs } = normalizeManualToolAsyncOptions(options);
  const job = await startManualToolCall(toolName, argumentsValue);
  return waitForManualToolJob(job, toolName, timeoutMs);
}

export async function listManualTools(): Promise<ManualToolDefinition[]> {
  const resp = await fetch("/tools", { cache: "no-store" });
  if (!resp.ok) throw new Error(await decodeJsonError(resp));

  const payload = (await resp.json()) as { tools?: unknown };
  const tools = Array.isArray(payload?.tools) ? payload.tools : [];
  return tools
    .filter((tool): tool is Record<string, unknown> => !!tool && typeof tool === "object")
    .map((tool) => ({
      name: typeof tool.name === "string" ? tool.name : "",
      description: typeof tool.description === "string" ? tool.description : "",
      parameters:
        tool.parameters && typeof tool.parameters === "object" && !Array.isArray(tool.parameters)
          ? (tool.parameters as Record<string, unknown>)
          : {},
      kind: typeof tool.kind === "string" ? tool.kind : undefined,
    }))
    .filter((tool) => tool.name);
}

export async function updateCardTemplateState(
  cardId: string,
  templateState: Record<string, JsonValue>,
): Promise<void> {
  const key = cardId.trim();
  if (!key) throw new Error("card id is required");
  const resp = await fetch(`/cards/${encodeURIComponent(key)}/state`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ template_state: templateState }),
  });
  if (!resp.ok) throw new Error(await decodeJsonError(resp));
}

export async function updateWorkbenchTemplateState(
  item: WorkbenchItem,
  templateState: Record<string, JsonValue>,
): Promise<void> {
  const resp = await fetch("/workbench", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      id: item.id,
      chat_id: item.chatId,
      kind: item.kind,
      title: item.title,
      content: item.content,
      question: item.question || "",
      choices: item.choices || [],
      response_value: item.responseValue || "",
      slot: item.slot || "",
      template_key: item.templateKey || "",
      template_state: templateState,
      context_summary: item.contextSummary || "",
      promotable: item.promotable,
      source_card_id: item.sourceCardId || "",
    }),
  });
  if (!resp.ok) throw new Error(await decodeJsonError(resp));
}

export async function copyTextToClipboard(text: string): Promise<void> {
  if (navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(text);
    return;
  }
  const textarea = document.createElement("textarea");
  textarea.value = text;
  textarea.setAttribute("readonly", "true");
  textarea.style.position = "fixed";
  textarea.style.opacity = "0";
  document.body.appendChild(textarea);
  textarea.focus();
  textarea.select();
  try {
    document.execCommand("copy");
  } finally {
    textarea.remove();
  }
}
