import type {
  ChatDetail,
  ChatSummary,
  CompletionRequestMessage,
  CompletionStreamHandlers,
  ModelCatalogResponse,
  Provider,
  SearchDetail,
  SearchRunRequest,
  SearchStreamHandlers,
  SearchSummary,
  SessionStatus,
  WorkspaceItem,
} from "./types.js";

type RequestOptions = {
  method?: "GET" | "POST" | "PATCH" | "DELETE";
  body?: unknown;
  signal?: AbortSignal;
  headers?: Record<string, string>;
};

export class SybilApiClient {
  private readonly baseUrl: string;
  private readonly token: string | null;

  constructor(baseUrl: string, token: string | null) {
    this.baseUrl = baseUrl;
    this.token = token;
  }

  async verifySession() {
    return this.request<SessionStatus>("/v1/auth/session");
  }

  async listModels() {
    return this.request<ModelCatalogResponse>("/v1/models");
  }

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

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

  async createChat(title?: string) {
    const data = await this.request<{ chat: ChatSummary }>("/v1/chats", {
      method: "POST",
      body: { title },
    });
    return data.chat;
  }

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

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

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

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

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

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

  async createSearch(body?: { title?: string; query?: string }) {
    const data = await this.request<{ search: SearchSummary }>("/v1/searches", {
      method: "POST",
      body: body ?? {},
    });
    return data.search;
  }

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

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

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

  async runCompletionStream(
    body: {
      chatId: string;
      provider: Provider;
      model: string;
      messages: CompletionRequestMessage[];
      userLocation?: string;
    },
    handlers: CompletionStreamHandlers,
    options?: { signal?: AbortSignal }
  ) {
    await this.runSse(
      "/v1/chat-completions/stream",
      body,
      {
        meta: handlers.onMeta,
        tool_call: handlers.onToolCall,
        delta: handlers.onDelta,
        done: handlers.onDone,
        error: handlers.onError,
      },
      options
    );
  }

  async runSearchStream(
    searchId: string,
    body: SearchRunRequest,
    handlers: SearchStreamHandlers,
    options?: { signal?: AbortSignal }
  ) {
    await this.runSse(
      `/v1/searches/${searchId}/run/stream`,
      body,
      {
        search_results: handlers.onSearchResults,
        search_error: handlers.onSearchError,
        answer: handlers.onAnswer,
        answer_error: handlers.onAnswerError,
        done: handlers.onDone,
        error: handlers.onError,
      },
      options
    );
  }

  private async request<T>(path: string, options?: RequestOptions): Promise<T> {
    const headers = new Headers(options?.headers ?? {});
    const hasBody = options?.body !== undefined;
    if (hasBody && !headers.has("Content-Type")) {
      headers.set("Content-Type", "application/json");
    }
    if (this.token) {
      headers.set("Authorization", `Bearer ${this.token}`);
    }

    const init: RequestInit = {
      method: options?.method ?? "GET",
      headers,
    };
    if (hasBody) {
      init.body = JSON.stringify(options?.body);
    }
    if (options?.signal) {
      init.signal = options.signal;
    }

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

    if (!response.ok) {
      throw new Error(await this.readErrorMessage(response));
    }

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

  private async runSse(
    path: string,
    body: unknown,
    handlers: Record<string, ((payload: any) => void) | undefined>,
    options?: { signal?: AbortSignal }
  ) {
    const headers = new Headers({
      Accept: "text/event-stream",
      "Content-Type": "application/json",
    });
    if (this.token) {
      headers.set("Authorization", `Bearer ${this.token}`);
    }

    const init: RequestInit = {
      method: "POST",
      headers,
      body: JSON.stringify(body),
    };
    if (options?.signal) {
      init.signal = options.signal;
    }

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

    if (!response.ok) {
      throw new Error(await this.readErrorMessage(response));
    }

    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[] = [];

    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 };
      }

      handlers[eventName]?.(payload);
      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();
  }

  private async readErrorMessage(response: Response) {
    const fallback = `${response.status} ${response.statusText}`;
    try {
      const body = (await response.json()) as { message?: string };
      if (typeof body.message === "string" && body.message.trim()) {
        return body.message;
      }
      return fallback;
    } catch {
      return fallback;
    }
  }
}
