export type ChatSummary = {
  id: string;
  title: string | null;
  parentChatId: string | null;
  titleGenerationPending: boolean;
  createdAt: string;
  updatedAt: string;
  starred: boolean;
  starredAt: string | null;
  initiatedProvider: Provider | null;
  initiatedModel: string | null;
  lastUsedProvider: Provider | null;
  lastUsedModel: string | null;
  additionalSystemPrompt: string | null;
  enabledTools: string[] | null;
};

export type SearchSummary = {
  id: string;
  title: string | null;
  query: string | null;
  createdAt: string;
  updatedAt: string;
  starred: boolean;
  starredAt: string | null;
};

export type ChatWorkspaceItem = ChatSummary & {
  type: "chat";
};

export type SearchWorkspaceItem = SearchSummary & {
  type: "search";
};

export type WorkspaceItem = ChatWorkspaceItem | SearchWorkspaceItem;

export type Message = {
  id: string;
  createdAt: string;
  role: "system" | "user" | "assistant" | "tool";
  content: string;
  name: string | null;
  metadata: unknown | null;
};

export type ToolCallEvent = {
  toolCallId: string;
  name: string;
  status: "initiated" | "completed" | "failed";
  summary: string;
  args: Record<string, unknown>;
  startedAt: string;
  completedAt?: string;
  durationMs?: number;
  error?: string;
  resultPreview?: string;
};

export type ChatDetail = {
  id: string;
  title: string | null;
  parentChatId: string | null;
  titleGenerationPending: boolean;
  createdAt: string;
  updatedAt: string;
  starred: boolean;
  starredAt: string | null;
  initiatedProvider: Provider | null;
  initiatedModel: string | null;
  lastUsedProvider: Provider | null;
  lastUsedModel: string | null;
  additionalSystemPrompt: string | null;
  enabledTools: string[] | null;
  messages: Message[];
};

export type SearchResultItem = {
  id: string;
  createdAt: string;
  rank: number;
  title: string | null;
  url: string;
  publishedDate: string | null;
  author: string | null;
  text: string | null;
  highlights: string[] | null;
  highlightScores: number[] | null;
  score: number | null;
  favicon: string | null;
  image: string | null;
};

export type SearchDetail = {
  id: string;
  title: string | null;
  query: string | null;
  createdAt: string;
  updatedAt: string;
  starred: boolean;
  starredAt: string | null;
  requestId: string | null;
  latencyMs: number | null;
  error: string | null;
  answerText: string | null;
  answerRequestId: string | null;
  answerCitations: Array<{
    id?: string;
    url?: string;
    title?: string | null;
    publishedDate?: string | null;
    author?: string | null;
    text?: string | null;
  }> | null;
  answerError: string | null;
  results: SearchResultItem[];
};

export type ChatImageAttachment = {
  kind: "image";
  id: string;
  filename: string;
  mimeType: "image/png" | "image/jpeg";
  sizeBytes: number;
  dataUrl: string;
};

export type ChatTextAttachment = {
  kind: "text";
  id: string;
  filename: string;
  mimeType: string;
  sizeBytes: number;
  text: string;
  truncated?: boolean;
};

export type ChatAttachment = ChatImageAttachment | ChatTextAttachment;

export type SearchRunRequest = {
  query?: string;
  title?: string;
  type?: "auto" | "fast" | "deep" | "instant";
  numResults?: number;
  includeDomains?: string[];
  excludeDomains?: string[];
};

export type CompletionRequestMessage = {
  role: "system" | "user" | "assistant" | "tool";
  content: string;
  name?: string;
  attachments?: ChatAttachment[];
};

export type Provider = "openai" | "anthropic" | "xai" | "gemini" | "hermes-agent";

export type ProviderModelInfo = {
  models: string[];
  loadedAt: string | null;
  error: string | null;
};

export type ModelCatalogResponse = {
  providers: Partial<Record<Provider, ProviderModelInfo>>;
};

export type ChatToolInfo = {
  name: string;
  description: string;
};

export type ActiveRunsResponse = {
  chats: string[];
  searches: string[];
};

type CompletionResponse = {
  chatId: string | null;
  message: {
    role: "assistant";
    content: string;
  };
};

type CompletionStreamHandlers = {
  onMeta?: (payload: { chatId: string | null; callId: string | null; provider: Provider; model: string }) => void;
  onToolCall?: (payload: ToolCallEvent) => void;
  onDelta?: (payload: { text: string }) => void;
  onDone?: (payload: { text: string; usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number } }) => void;
  onError?: (payload: { message: string }) => void;
};

type CreateChatRequest = {
  title?: string;
  provider?: Provider;
  model?: string;
  additionalSystemPrompt?: string;
  enabledTools?: string[];
  messages?: CompletionRequestMessage[];
};

type CreateSearchRequest = {
  title?: string;
  query?: string;
  reuseByQuery?: boolean;
};

type CreateSearchResponse = {
  search: SearchSummary;
  reused: boolean;
  cacheHit: boolean;
};

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api";
const ENV_ADMIN_TOKEN = (import.meta.env.VITE_ADMIN_TOKEN as string | undefined)?.trim() || null;
let authToken: string | null = ENV_ADMIN_TOKEN;

export function getConfiguredToken() {
  return ENV_ADMIN_TOKEN;
}

export function setAuthToken(token: string | null) {
  authToken = token?.trim() || null;
}

async function api<T>(path: string, init?: RequestInit): Promise<T> {
  const headers = new Headers(init?.headers ?? {});
  const hasBody = init?.body !== undefined && init.body !== null;
  if (hasBody && !headers.has("Content-Type")) {
    headers.set("Content-Type", "application/json");
  }
  if (authToken) {
    headers.set("Authorization", `Bearer ${authToken}`);
  }

  const response = await fetch(`${API_BASE_URL}${path}`, {
    ...init,
    headers,
  });

  if (!response.ok) {
    const fallback = `${response.status} ${response.statusText}`;
    let message = fallback;
    try {
      const body = (await response.json()) as { message?: string };
      if (body.message) message = body.message;
    } catch {
      // keep fallback message
    }
    throw new Error(message);
  }

  return (await response.json()) as T;
}

export async function listChats() {
  const data = await api<{ chats: ChatSummary[] }>("/v1/chats");
  return data.chats;
}

export async function listWorkspaceItems() {
  const data = await api<{ items: WorkspaceItem[] }>("/v1/workspace-items");
  return data.items;
}

export async function verifySession() {
  return api<{ authenticated: true; mode: "open" | "token" }>("/v1/auth/session");
}

export async function listModels() {
  return api<ModelCatalogResponse>("/v1/models");
}

export async function listChatTools() {
  const data = await api<{ tools: ChatToolInfo[] }>("/v1/chat-tools");
  return data.tools;
}

export async function getActiveRuns() {
  return api<ActiveRunsResponse>("/v1/active-runs");
}

export async function createChat(input?: string | CreateChatRequest) {
  const body = typeof input === "string" ? { title: input } : input ?? {};
  const data = await api<{ chat: ChatSummary }>("/v1/chats", {
    method: "POST",
    body: JSON.stringify(body),
  });
  return data.chat;
}

export async function getChat(chatId: string) {
  const data = await api<{ chat: ChatDetail }>(`/v1/chats/${chatId}`);
  return data.chat;
}

export async function forkChat(chatId: string, messageId?: string) {
  const data = await api<{ chat: ChatSummary }>(`/v1/chats/${chatId}/fork`, {
    method: "POST",
    body: JSON.stringify(messageId ? { messageId } : {}),
  });
  return data.chat;
}

export async function updateChatTitle(chatId: string, title: string) {
  const data = await api<{ chat: ChatSummary }>(`/v1/chats/${chatId}`, {
    method: "PATCH",
    body: JSON.stringify({ title }),
  });
  return data.chat;
}

export async function updateChatStar(chatId: string, starred: boolean) {
  const data = await api<{ chat: ChatSummary }>(`/v1/chats/${chatId}/star`, {
    method: "PATCH",
    body: JSON.stringify({ starred }),
  });
  return data.chat;
}

export async function updateChatSettings(
  chatId: string,
  body: { title?: string; additionalSystemPrompt?: string | null; enabledTools?: string[] }
) {
  const data = await api<{ chat: ChatSummary }>(`/v1/chats/${chatId}`, {
    method: "PATCH",
    body: JSON.stringify(body),
  });
  return data.chat;
}

export async function suggestChatTitle(body: { chatId: string; content: string }) {
  const data = await api<{ chat: ChatSummary }>("/v1/chats/title/suggest", {
    method: "POST",
    body: JSON.stringify(body),
  });
  return data.chat;
}

export async function deleteChat(chatId: string) {
  await api<{ deleted: true }>(`/v1/chats/${chatId}`, { method: "DELETE" });
}

export async function listSearches() {
  const data = await api<{ searches: SearchSummary[] }>("/v1/searches");
  return data.searches;
}

async function postSearch(body?: CreateSearchRequest) {
  return api<CreateSearchResponse>("/v1/searches", {
    method: "POST",
    body: JSON.stringify(body ?? {}),
  });
}

export async function createSearch(body?: CreateSearchRequest) {
  const data = await postSearch(body);
  return data.search;
}

export async function createReusableSearch(body: Omit<CreateSearchRequest, "reuseByQuery">) {
  return postSearch({ ...body, reuseByQuery: true });
}

export async function getSearch(searchId: string) {
  const data = await api<{ search: SearchDetail }>(`/v1/searches/${searchId}`);
  return data.search;
}

export async function updateSearchStar(searchId: string, starred: boolean) {
  const data = await api<{ search: SearchSummary }>(`/v1/searches/${searchId}/star`, {
    method: "PATCH",
    body: JSON.stringify({ starred }),
  });
  return data.search;
}

export async function createChatFromSearch(searchId: string, body?: { title?: string }) {
  const data = await api<{ chat: ChatSummary }>(`/v1/searches/${searchId}/chat`, {
    method: "POST",
    body: JSON.stringify(body ?? {}),
  });
  return data.chat;
}

export async function deleteSearch(searchId: string) {
  await api<{ deleted: true }>(`/v1/searches/${searchId}`, { method: "DELETE" });
}

export function getMessageAttachments(metadata: unknown): ChatAttachment[] {
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return [];
  const attachments = (metadata as Record<string, unknown>).attachments;
  if (!Array.isArray(attachments)) return [];

  const parsed: ChatAttachment[] = [];
  for (const entry of attachments) {
    if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
    const record = entry as Record<string, unknown>;
    const kind = record.kind;
    const id = typeof record.id === "string" ? record.id : "";
    const filename = typeof record.filename === "string" ? record.filename : "";
    const mimeType = typeof record.mimeType === "string" ? record.mimeType : "";
    const sizeBytes = typeof record.sizeBytes === "number" ? record.sizeBytes : 0;

    if (kind === "image" && typeof record.dataUrl === "string" && (mimeType === "image/png" || mimeType === "image/jpeg")) {
      parsed.push({
        kind,
        id,
        filename,
        mimeType,
        sizeBytes,
        dataUrl: record.dataUrl,
      } satisfies ChatImageAttachment);
      continue;
    }

    if (kind === "text" && typeof record.text === "string") {
      parsed.push({
        kind,
        id,
        filename,
        mimeType,
        sizeBytes,
        text: record.text,
        truncated: record.truncated === true,
      } satisfies ChatTextAttachment);
    }
  }

  return parsed;
}

type RunSearchStreamHandlers = {
  onSearchResults?: (payload: { requestId: string | null; results: SearchResultItem[] }) => void;
  onSearchError?: (payload: { error: string }) => void;
  onAnswer?: (payload: { answerText: string | null; answerRequestId: string | null; answerCitations: SearchDetail["answerCitations"] }) => void;
  onAnswerError?: (payload: { error: string }) => void;
  onDone?: (payload: { search: SearchDetail }) => void;
  onError?: (payload: { message: string }) => void;
};

async function readSseStream(response: Response, dispatch: (eventName: string, payload: any) => void) {
  if (!response.ok) {
    const fallback = `${response.status} ${response.statusText}`;
    let message = fallback;
    try {
      const body = (await response.json()) as { message?: string };
      if (body.message) message = body.message;
    } catch {
      // keep fallback message
    }
    throw new Error(message);
  }

  if (!response.body) {
    throw new Error("No response stream");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let eventName = "message";
  let dataLines: string[] = [];
  let sawTerminalEvent = false;

  const flushEvent = () => {
    if (!dataLines.length) {
      eventName = "message";
      return;
    }

    const dataText = dataLines.join("\n");
    let payload: any = null;
    try {
      payload = JSON.parse(dataText);
    } catch {
      payload = { message: dataText };
    }

    dispatch(eventName, payload);
    if (eventName === "done" || eventName === "error") {
      sawTerminalEvent = true;
    }

    dataLines = [];
    eventName = "message";
  };

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    let newlineIndex = buffer.indexOf("\n");

    while (newlineIndex >= 0) {
      const rawLine = buffer.slice(0, newlineIndex);
      buffer = buffer.slice(newlineIndex + 1);
      const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;

      if (!line) {
        flushEvent();
      } else if (line.startsWith("event:")) {
        eventName = line.slice("event:".length).trim();
      } else if (line.startsWith("data:")) {
        dataLines.push(line.slice("data:".length).trimStart());
      }

      newlineIndex = buffer.indexOf("\n");
    }
  }

  buffer += decoder.decode();
  if (buffer.length) {
    const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
    if (line.startsWith("event:")) {
      eventName = line.slice("event:".length).trim();
    } else if (line.startsWith("data:")) {
      dataLines.push(line.slice("data:".length).trimStart());
    }
  }
  flushEvent();

  if (!sawTerminalEvent) {
    throw new Error("Stream disconnected before completion");
  }
}

export async function runSearchStream(
  searchId: string,
  body: SearchRunRequest,
  handlers: RunSearchStreamHandlers,
  options?: { signal?: AbortSignal }
) {
  const headers = new Headers({
    Accept: "text/event-stream",
    "Content-Type": "application/json",
  });
  if (authToken) {
    headers.set("Authorization", `Bearer ${authToken}`);
  }

  const response = await fetch(`${API_BASE_URL}/v1/searches/${searchId}/run/stream`, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
    signal: options?.signal,
  });

  await readSseStream(response, (eventName, payload) => {
    if (eventName === "search_results") handlers.onSearchResults?.(payload);
    else if (eventName === "search_error") handlers.onSearchError?.(payload);
    else if (eventName === "answer") handlers.onAnswer?.(payload);
    else if (eventName === "answer_error") handlers.onAnswerError?.(payload);
    else if (eventName === "done") handlers.onDone?.(payload);
    else if (eventName === "error") handlers.onError?.(payload);
  });
}

export async function attachSearchStream(searchId: string, handlers: RunSearchStreamHandlers, options?: { signal?: AbortSignal }) {
  const headers = new Headers({
    Accept: "text/event-stream",
  });
  if (authToken) {
    headers.set("Authorization", `Bearer ${authToken}`);
  }

  const response = await fetch(`${API_BASE_URL}/v1/searches/${searchId}/run/stream/attach`, {
    method: "POST",
    headers,
    signal: options?.signal,
  });

  await readSseStream(response, (eventName, payload) => {
    if (eventName === "search_results") handlers.onSearchResults?.(payload);
    else if (eventName === "search_error") handlers.onSearchError?.(payload);
    else if (eventName === "answer") handlers.onAnswer?.(payload);
    else if (eventName === "answer_error") handlers.onAnswerError?.(payload);
    else if (eventName === "done") handlers.onDone?.(payload);
    else if (eventName === "error") handlers.onError?.(payload);
  });
}

export async function runCompletion(body: {
  chatId: string;
  provider: Provider;
  model: string;
  messages: CompletionRequestMessage[];
  additionalSystemPrompt?: string;
  enabledTools?: string[];
  userLocation?: string;
}) {
  return api<CompletionResponse>("/v1/chat-completions", {
    method: "POST",
    body: JSON.stringify(body),
  });
}

export async function runCompletionStream(
  body: {
    chatId?: string | null;
    persist?: boolean;
    clientRequestId?: string;
    provider: Provider;
    model: string;
    messages: CompletionRequestMessage[];
    additionalSystemPrompt?: string;
    enabledTools?: string[];
    userLocation?: string;
  },
  handlers: CompletionStreamHandlers,
  options?: { signal?: AbortSignal }
) {
  const headers = new Headers({
    Accept: "text/event-stream",
    "Content-Type": "application/json",
  });
  if (authToken) {
    headers.set("Authorization", `Bearer ${authToken}`);
  }

  const response = await fetch(`${API_BASE_URL}/v1/chat-completions/stream`, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
    signal: options?.signal,
  });

  await readSseStream(response, (eventName, payload) => {
    if (eventName === "meta") handlers.onMeta?.(payload);
    else if (eventName === "tool_call") handlers.onToolCall?.(payload);
    else if (eventName === "delta") handlers.onDelta?.(payload);
    else if (eventName === "done") handlers.onDone?.(payload);
    else if (eventName === "error") handlers.onError?.(payload);
  });
}

export async function attachCompletionStream(chatId: string, handlers: CompletionStreamHandlers, options?: { signal?: AbortSignal }) {
  const headers = new Headers({
    Accept: "text/event-stream",
  });
  if (authToken) {
    headers.set("Authorization", `Bearer ${authToken}`);
  }

  const response = await fetch(`${API_BASE_URL}/v1/chats/${chatId}/stream/attach`, {
    method: "POST",
    headers,
    signal: options?.signal,
  });

  await readSseStream(response, (eventName, payload) => {
    if (eventName === "meta") handlers.onMeta?.(payload);
    else if (eventName === "tool_call") handlers.onToolCall?.(payload);
    else if (eventName === "delta") handlers.onDelta?.(payload);
    else if (eventName === "done") handlers.onDone?.(payload);
    else if (eventName === "error") handlers.onError?.(payload);
  });
}
