import type {
  ListModelInfoResponse,
  ModelDetailsResponse,
  ModelInfoResponse,
  PoolBudgetResponse,
  CatalogModelsResponse,
  CatalogModelResponse,
  KeysResponse,
  TokenRequest,
  TokenResponse,
  PullResponse,
  AsyncPullResponse,
  VersionResponse,
  ChatRequest,
  ChatStreamResponse,
  VRAMRequest,
  VRAMCalculatorResponse,
  HFLookupResponse,
  ResolveSourceResponse,
  PlaygroundTemplateInfo,
  PlaygroundTemplateListResponse,
  PlaygroundTemplateResponse,
  PlaygroundSessionRequest,
  PlaygroundSessionResponse,
  PlaygroundChatRequest,
  DevicesResponse,
  LibsCombinationsResponse,
  LibsBundleListResponse,
  LibsBundleActionResponse,
  LibsPeerBundleListResponse,
  LibsPeerPullEvent,
  PeerModelListResponse,
  BuckyCatalogResponse,
  BuckyModelsResponse,
  BuckyModelActionResponse,
  BuckyModelDetails,
  TranscriptionResponse,
  AccuracyFunctionsResponse,
  AccuracyResponse,
} from '../types';

class ApiService {
  private baseUrl = '/v1';

  private async parseErrorMessage(response: Response): Promise<string> {
    let message = `HTTP ${response.status}`;
    try {
      const raw = await response.text();
      try {
        const body = JSON.parse(raw);
        message = body?.error?.message ?? message;
      } catch {
        if (raw) message = `${message}: ${raw.slice(0, 200)}`;
      }
    } catch { /* empty */ }
    return message;
  }

  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

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

    if (response.status === 204) {
      return undefined as T;
    }

    return response.json();
  }

  async listModels(): Promise<ListModelInfoResponse> {
    return this.request<ListModelInfoResponse>('/kronk/models');
  }

  async rebuildModelIndex(): Promise<void> {
    const response = await fetch(`${this.baseUrl}/kronk/models/index`, {
      method: 'POST',
    });
    if (!response.ok) {
      throw new Error(await this.parseErrorMessage(response));
    }
  }

  async listRunningModels(): Promise<ModelDetailsResponse> {
    return this.request<ModelDetailsResponse>('/kronk/models/ps');
  }

  async getPoolBudget(): Promise<PoolBudgetResponse> {
    return this.request<PoolBudgetResponse>('/pool/budget');
  }

  async unloadModel(id: string): Promise<void> {
    await this.request('/kronk/models/unload', {
      method: 'POST',
      body: JSON.stringify({ id }),
    });
  }

  async showModel(id: string): Promise<ModelInfoResponse> {
    return this.request<ModelInfoResponse>(`/kronk/models/${encodeURIComponent(id)}`);
  }

  async getLibsVersion(): Promise<VersionResponse> {
    return this.request<VersionResponse>('/kronk/libs');
  }

  async pullModelAsync(modelUrl: string): Promise<AsyncPullResponse> {
    const response = await fetch(`${this.baseUrl}/kronk/models/pull`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ model_url: modelUrl, async: true }),
    });

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

    return response.json();
  }

  streamPullSession(
    sessionId: string,
    onMessage: (data: PullResponse) => void,
    onError: (error: string) => void,
    onComplete: () => void
  ): () => void {
    const controller = new AbortController();

    fetch(`${this.baseUrl}/kronk/models/pull/${encodeURIComponent(sessionId)}`, {
      method: 'GET',
      signal: controller.signal,
    })
      .then(async (response) => {
        if (response.status === 400) {
          onError('Session closed');
          return;
        }
        if (!response.ok) {
          onError(`HTTP ${response.status}`);
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.trim()) continue;
            const jsonStr = line.startsWith('data: ') ? line.slice(6) : line;
            if (!jsonStr.trim()) continue;
            try {
              const data = JSON.parse(jsonStr) as PullResponse;
              onMessage(data);
              if (data.status === 'downloaded' || data.downloaded) {
                onComplete();
                return;
              }
            } catch {
              onError('Failed to parse response');
            }
          }
        }

        onComplete();
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError('Connection error');
        }
      });

    return () => controller.abort();
  }

  pullModel(
    modelUrl: string,
    projUrl: string | undefined,
    onMessage: (data: PullResponse) => void,
    onError: (error: string) => void,
    onComplete: () => void,
    downloadServer?: string
  ): () => void {
    const controller = new AbortController();

    const body: { model_url: string; proj_url?: string; download_server?: string } = { model_url: modelUrl };
    if (projUrl) {
      body.proj_url = projUrl;
    }
    if (downloadServer) {
      body.download_server = downloadServer;
    }

    fetch(`${this.baseUrl}/kronk/models/pull`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          const msg = await this.parseErrorMessage(response);
          onError(msg);
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';
        let receivedSuccess = false;

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.trim()) continue;
            const jsonStr = line.startsWith('data: ') ? line.slice(6) : line;
            if (!jsonStr.trim()) continue;
            try {
              const data = JSON.parse(jsonStr) as PullResponse;
              onMessage(data);
              if (data.status === 'complete' || data.downloaded) {
                receivedSuccess = true;
                onComplete();
                return;
              }
            } catch {
              onError('Failed to parse response');
            }
          }
        }

        if (!receivedSuccess && !controller.signal.aborted) {
          onError('Stream ended before completion');
        }
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError('Connection error');
        }
      });

    return () => controller.abort();
  }

  async removeModel(id: string): Promise<void> {
    await this.request(`/kronk/models/${encodeURIComponent(id)}`, {
      method: 'DELETE',
    });
  }

  async listCatalog(): Promise<CatalogModelsResponse> {
    return this.request<CatalogModelsResponse>('/kronk/catalog');
  }

  async showCatalogModel(id: string): Promise<CatalogModelResponse> {
    return this.request<CatalogModelResponse>(`/kronk/catalog/${encodeURIComponent(id)}`);
  }

  async removeCatalogModel(id: string): Promise<void> {
    await this.request(`/kronk/catalog/${encodeURIComponent(id)}`, {
      method: 'DELETE',
    });
  }

  async reconcileCatalog(): Promise<void> {
    await this.request('/kronk/catalog/reconcile', {
      method: 'POST',
    });
  }

  pullLibs(
    onMessage: (data: VersionResponse) => void,
    onError: (error: string) => void,
    onComplete: () => void,
    opts?: {
      allowUpgrade?: boolean;
      version?: string;
      arch?: string;
      os?: string;
      processor?: string;
    }
  ): () => void {
    const controller = new AbortController();

    const params = new URLSearchParams();
    if (opts?.allowUpgrade) {
      params.set('allow-upgrade', 'true');
    }
    if (opts?.version) {
      params.set('version', opts.version);
    }
    if (opts?.arch) {
      params.set('arch', opts.arch);
    }
    if (opts?.os) {
      params.set('os', opts.os);
    }
    if (opts?.processor) {
      params.set('processor', opts.processor);
    }
    const qs = params.toString();
    const url = qs ? `${this.baseUrl}/kronk/libs/pull?${qs}` : `${this.baseUrl}/kronk/libs/pull`;

    fetch(url, {
      method: 'POST',
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          onError(`HTTP ${response.status}`);
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.trim()) continue;
            const jsonStr = line.startsWith('data: ') ? line.slice(6) : line;
            if (!jsonStr.trim()) continue;
            try {
              const data = JSON.parse(jsonStr) as VersionResponse;
              onMessage(data);
              if (data.status === 'complete') {
                onComplete();
                return;
              }
            } catch {
              onError('Failed to parse response');
            }
          }
        }

        onComplete();
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError('Connection error');
        }
      });

    return () => controller.abort();
  }

  async listKeys(token: string): Promise<KeysResponse> {
    return this.request<KeysResponse>('/security/keys', {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
  }

  async createKey(token: string): Promise<{ id: string }> {
    return this.request<{ id: string }>('/security/keys/add', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
  }

  async deleteKey(token: string, keyId: string): Promise<void> {
    await this.request(`/security/keys/remove/${encodeURIComponent(keyId)}`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
  }

  async createToken(token: string, request: TokenRequest): Promise<TokenResponse> {
    return this.request<TokenResponse>('/security/token/create', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(request),
    });
  }

  streamChat(
    request: ChatRequest,
    onMessage: (data: ChatStreamResponse) => void,
    onError: (error: string) => void,
    onComplete: () => void,
  ): () => void {
    const controller = new AbortController();

    const headers: Record<string, string> = { 'Content-Type': 'application/json' };

    fetch(`${this.baseUrl}/chat/completions`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ ...request, stream: true }),
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          onError(await this.parseErrorMessage(response));
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.trim() || line === 'data: [DONE]') continue;
            const jsonStr = line.startsWith('data: ') ? line.slice(6) : line;
            if (!jsonStr.trim()) continue;
            try {
              const data = JSON.parse(jsonStr) as ChatStreamResponse;
              onMessage(data);
            } catch {
              // Skip malformed JSON
            }
          }
        }

        onComplete();
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError(err.message || 'Connection error');
        }
      });

    return () => controller.abort();
  }

  async lookupHuggingFace(input: string): Promise<HFLookupResponse> {
    return this.request<HFLookupResponse>('/kronk/catalog/lookup', {
      method: 'POST',
      body: JSON.stringify({ input }),
    });
  }

  async resolveSource(source: string): Promise<ResolveSourceResponse> {
    return this.request<ResolveSourceResponse>('/kronk/catalog/resolve', {
      method: 'POST',
      body: JSON.stringify({ source }),
    });
  }

  async calculateVRAM(request: VRAMRequest, token?: string): Promise<VRAMCalculatorResponse> {
    const headers: Record<string, string> = {};
    if (token) {
      headers['Authorization'] = `Bearer ${token}`;
    }
    return this.request<VRAMCalculatorResponse>('/kronk/models/vram', {
      method: 'POST',
      headers,
      body: JSON.stringify(request),
    });
  }

  async listGrammars(): Promise<{ files: string[] }> {
    return this.request<{ files: string[] }>('/grammars');
  }

  async getGrammarContent(name: string): Promise<{ content: string }> {
    return this.request<{ content: string }>(`/grammars/${encodeURIComponent(name)}`);
  }

  async listTemplates(): Promise<{ files: string[] }> {
    return this.request<{ files: string[] }>('/templates');
  }

  async listPlaygroundTemplates(): Promise<PlaygroundTemplateInfo[]> {
    const resp = await this.request<PlaygroundTemplateListResponse>('/playground/templates');
    return resp.templates;
  }

  async getPlaygroundTemplate(name: string): Promise<PlaygroundTemplateResponse> {
    return this.request<PlaygroundTemplateResponse>(`/playground/templates/${encodeURIComponent(name)}`);
  }

  async savePlaygroundTemplate(name: string, script: string): Promise<void> {
    await this.request('/playground/templates/save', {
      method: 'POST',
      body: JSON.stringify({ name, script }),
    });
  }

  async createPlaygroundSession(request: PlaygroundSessionRequest): Promise<PlaygroundSessionResponse> {
    return this.request<PlaygroundSessionResponse>('/playground/sessions', {
      method: 'POST',
      body: JSON.stringify(request),
    });
  }

  async deletePlaygroundSession(id: string): Promise<void> {
    await this.request(`/playground/sessions/${encodeURIComponent(id)}`, {
      method: 'DELETE',
    });
  }

  streamPlaygroundChat(
    request: PlaygroundChatRequest,
    onMessage: (data: ChatStreamResponse) => void,
    onError: (error: string) => void,
    onComplete: () => void
  ): () => void {
    const controller = new AbortController();

    fetch(`${this.baseUrl}/playground/chat/completions`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...request, stream: true }),
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          onError(await this.parseErrorMessage(response));
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';
        let receivedDone = false;

        const processLine = (raw: string) => {
          const trimmed = raw.trim();
          if (!trimmed) return;
          if (trimmed.startsWith('event:')) return;
          if (trimmed.startsWith(':')) return; // SSE comment / keep-alive
          const jsonStr = trimmed.startsWith('data:') ? trimmed.slice(5).trim() : trimmed;
          if (!jsonStr) return;
          if (jsonStr === '[DONE]') { receivedDone = true; return; }
          try {
            onMessage(JSON.parse(jsonStr) as ChatStreamResponse);
          } catch {
            // Skip malformed JSON
          }
        };

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            processLine(line);
          }
        }

        buffer += decoder.decode(); // flush remaining multi-byte sequence
        if (buffer) processLine(buffer);

        if (receivedDone) {
          onComplete();
        } else {
          onError('Stream terminated: server closed connection before completion');
        }
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError(err.message || 'Connection error');
        }
      });

    return () => controller.abort();
  }

  async getDevices(): Promise<DevicesResponse> {
    return this.request<DevicesResponse>('/devices');
  }

  async getLibsCombinations(): Promise<LibsCombinationsResponse> {
    return this.request<LibsCombinationsResponse>('/kronk/libs/combinations');
  }

  async listLibsInstalls(): Promise<LibsBundleListResponse> {
    return this.request<LibsBundleListResponse>('/kronk/libs/installs');
  }

  async removeLibsInstall(arch: string, os: string, processor: string): Promise<LibsBundleActionResponse> {
    const params = new URLSearchParams({ arch, os, processor });
    return this.request<LibsBundleActionResponse>(`/kronk/libs/installs?${params.toString()}`, { method: 'DELETE' });
  }

  async getBuckyLibsVersion(): Promise<VersionResponse> {
    return this.request<VersionResponse>('/bucky/libs');
  }

  async getBuckyLibsCombinations(): Promise<LibsCombinationsResponse> {
    return this.request<LibsCombinationsResponse>('/bucky/libs/combinations');
  }

  async listBuckyLibsInstalls(): Promise<LibsBundleListResponse> {
    return this.request<LibsBundleListResponse>('/bucky/libs/installs');
  }

  async removeBuckyLibsInstall(arch: string, os: string, processor: string): Promise<LibsBundleActionResponse> {
    const params = new URLSearchParams({ arch, os, processor });
    return this.request<LibsBundleActionResponse>(`/bucky/libs/installs?${params.toString()}`, { method: 'DELETE' });
  }

  pullBuckyLibs(
    onMessage: (data: VersionResponse) => void,
    onError: (error: string) => void,
    onComplete: () => void,
    opts?: {
      version?: string;
      arch?: string;
      os?: string;
      processor?: string;
    }
  ): () => void {
    const controller = new AbortController();

    const params = new URLSearchParams();
    if (opts?.version) {
      params.set('version', opts.version);
    }
    if (opts?.arch) {
      params.set('arch', opts.arch);
    }
    if (opts?.os) {
      params.set('os', opts.os);
    }
    if (opts?.processor) {
      params.set('processor', opts.processor);
    }
    const qs = params.toString();
    const url = qs ? `${this.baseUrl}/bucky/libs/pull?${qs}` : `${this.baseUrl}/bucky/libs/pull`;

    fetch(url, {
      method: 'POST',
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          onError(`HTTP ${response.status}`);
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.trim()) continue;
            const jsonStr = line.startsWith('data: ') ? line.slice(6) : line;
            if (!jsonStr.trim()) continue;
            try {
              const data = JSON.parse(jsonStr) as VersionResponse;
              onMessage(data);
              if (data.status === 'complete') {
                onComplete();
                return;
              }
            } catch {
              onError('Failed to parse response');
            }
          }
        }

        onComplete();
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError('Connection error');
        }
      });

    return () => controller.abort();
  }

  async listBuckyModels(): Promise<BuckyModelsResponse> {
    return this.request<BuckyModelsResponse>('/bucky/models');
  }

  async listBuckyCatalog(): Promise<BuckyCatalogResponse> {
    return this.request<BuckyCatalogResponse>('/bucky/models/catalog');
  }

  async getBuckyModelDetails(id: string): Promise<BuckyModelDetails> {
    return this.request<BuckyModelDetails>(`/bucky/models/${encodeURIComponent(id)}/details`);
  }

  async removeBuckyModel(id: string): Promise<BuckyModelActionResponse> {
    return this.request<BuckyModelActionResponse>(`/bucky/models/${encodeURIComponent(id)}`, { method: 'DELETE' });
  }

  pullBuckyModel(
    source: string,
    onMessage: (data: PullResponse) => void,
    onError: (error: string) => void,
    onComplete: () => void,
  ): () => void {
    const controller = new AbortController();

    fetch(`${this.baseUrl}/bucky/models/pull`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ source }),
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          const msg = await this.parseErrorMessage(response);
          onError(msg);
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.trim()) continue;
            const jsonStr = line.startsWith('data: ') ? line.slice(6) : line;
            if (!jsonStr.trim()) continue;
            try {
              const data = JSON.parse(jsonStr) as PullResponse;
              onMessage(data);
              if (data.status && data.status.startsWith('downloaded')) {
                onComplete();
                return;
              }
            } catch {
              onError('Failed to parse response');
            }
          }
        }

        onComplete();
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError('Connection error');
        }
      });

    return () => controller.abort();
  }

  async listPeerLibsBundles(host: string): Promise<LibsPeerBundleListResponse> {
    const params = new URLSearchParams({ host });
    return this.request<LibsPeerBundleListResponse>(`/download/libs/peer-bundles?${params.toString()}`);
  }

  async listPeerModels(host: string): Promise<PeerModelListResponse> {
    const params = new URLSearchParams({ host });
    return this.request<PeerModelListResponse>(`/download/models/peer-models?${params.toString()}`);
  }

  pullLibsFromPeer(
    host: string,
    arch: string,
    os: string,
    processor: string,
    onMessage: (data: LibsPeerPullEvent) => void,
    onError: (error: string) => void,
    onComplete: () => void,
  ): () => void {
    const controller = new AbortController();

    const params = new URLSearchParams({ host, arch, os, processor });
    const url = `${this.baseUrl}/download/libs/pull-from-peer?${params.toString()}`;

    fetch(url, {
      method: 'POST',
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          onError(`HTTP ${response.status}`);
          return;
        }

        const reader = response.body?.getReader();
        if (!reader) {
          onError('Streaming not supported');
          return;
        }

        const decoder = new TextDecoder();
        let buffer = '';
        let receivedComplete = false;

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.trim()) continue;
            const jsonStr = line.startsWith('data: ') ? line.slice(6) : line;
            if (!jsonStr.trim()) continue;
            try {
              const data = JSON.parse(jsonStr) as LibsPeerPullEvent;
              onMessage(data);
              if (data.status === 'error') {
                onError(data.error || 'Peer pull failed');
                return;
              }
              if (data.status === 'complete') {
                receivedComplete = true;
                onComplete();
                return;
              }
            } catch {
              // Ignore malformed lines.
            }
          }
        }

        if (!receivedComplete && !controller.signal.aborted) {
          onError('Stream ended before completion');
        }
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          onError(err.message || 'Connection error');
        }
      });

    return () => controller.abort();
  }

  async transcribe(
    modelID: string,
    file: Blob,
    opts: {
      filename?: string;
      language?: string;
      translate?: boolean;
      prompt?: string;
      token?: string;
    } = {},
  ): Promise<TranscriptionResponse> {
    const form = new FormData();
    form.append('model', modelID);
    form.append('file', file, opts.filename || 'audio');
    form.append('response_format', 'verbose_json');
    if (opts.language) form.append('language', opts.language);
    if (opts.translate) form.append('translate', 'true');
    if (opts.prompt) form.append('prompt', opts.prompt);

    const headers: Record<string, string> = {};
    if (opts.token) headers['Authorization'] = `Bearer ${opts.token}`;

    const response = await fetch(`${this.baseUrl}/audio/transcriptions`, {
      method: 'POST',
      headers,
      body: form,
    });

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

    return response.json();
  }

  // ── Accuracy app ──

  async listAccuracyFunctions(): Promise<AccuracyFunctionsResponse> {
    return this.request<AccuracyFunctionsResponse>('/accuracy/functions');
  }

  async runAccuracy(
    model: string,
    fn: string,
    signal?: AbortSignal,
  ): Promise<AccuracyResponse> {
    return this.request<AccuracyResponse>('/accuracy/test', {
      method: 'POST',
      body: JSON.stringify({ model, function: fn }),
      signal,
    });
  }

}

export const api = new ApiService();
