/**
 * Browser-oriented integration example, checked against Klaudija source
 * 86ca4d24b6133d5a907e688b70599a1bd21f1071 on 2026-09-05.
 * No live request has been executed by this example's authors.
 *
 * Supply the complete provisioned /uni-agent URL and a scoped end-user JWT.
 * Your application's browser origin must be registered with Klaudija.
 * Never put a platform API key or provider secret in a browser bundle.
 */
export type JobStatus = 'pending' | 'done' | 'error' | 'cancelled';
export interface Job {
  job_id: string;
  session_id: string;
  status: JobStatus;
  response?: string;
  error?: string;
  file_download_available?: boolean;
  memory_status?: 'attached' | 'degraded' | 'not_applicable' | 'unknown';
  memory_warning?: string;
}
export class KlaudijaHttpError extends Error {
  constructor(
    public readonly status: number,
    public readonly detail: unknown,
  ) {
    super(`Klaudija request failed (${status})`);
    this.name = 'KlaudijaHttpError';
  }
}
async function requireOk(response: Response): Promise<Response> {
  if (response.ok) return response;
  const detail: unknown = await response.json().catch(() => null);
  throw new KlaudijaHttpError(response.status, detail);
}
function parseJob(value: unknown): Job {
  if (typeof value !== 'object' || value === null)
    throw new Error('Invalid job response');
  const job = value as Partial<Job>;
  if (
    typeof job.job_id !== 'string' ||
    typeof job.session_id !== 'string' ||
    !['pending', 'done', 'error', 'cancelled'].includes(String(job.status))
  ) {
    throw new Error('Invalid job response');
  }
  return job as Job;
}
function delay(ms: number, signal: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    signal.throwIfAborted();
    const abort = () => {
      clearTimeout(timer);
      reject(signal.reason);
    };
    const timer = setTimeout(() => {
      signal.removeEventListener('abort', abort);
      resolve();
    }, ms);
    signal.addEventListener('abort', abort, { once: true });
  });
}
export class KlaudijaClient {
  private readonly runBase: string;
  constructor(
    runBase: string,
    private readonly accessToken: () => Promise<string>,
  ) {
    this.runBase = runBase.replace(/\/$/, '');
    const url = new URL(this.runBase);
    if (url.protocol !== 'https:' && url.hostname !== 'localhost') {
      throw new Error('Use HTTPS for a remote Klaudija endpoint');
    }
  }
  private async headers(): Promise<HeadersInit> {
    return { Authorization: `Bearer ${await this.accessToken()}` };
  }
  async submit(
    input: { query: string; sessionId: string; file?: File },
    signal: AbortSignal,
  ): Promise<Job> {
    if (!input.query.trim() && !input.file)
      throw new Error('Provide a query or file');
    const form = new FormData();
    form.set('query', input.query);
    form.set('session_id', input.sessionId);
    form.set('async', '1');
    if (input.file) form.set('file', input.file);
    // Do not retry automatically: plain POST has no idempotency contract.
    const response = await requireOk(
      await fetch(this.runBase, {
        method: 'POST',
        headers: await this.headers(),
        body: form,
        signal,
      }),
    );
    return parseJob(await response.json());
  }
  async status(jobId: string, signal: AbortSignal): Promise<Job> {
    const response = await requireOk(
      await fetch(`${this.runBase}/status/${encodeURIComponent(jobId)}`, {
        headers: await this.headers(),
        signal,
      }),
    );
    return parseJob(await response.json());
  }
  async wait(
    jobId: string,
    signal: AbortSignal,
    timeoutMs = 300_000,
  ): Promise<Job> {
    if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
      throw new Error('Provide a positive timeout');
    const timeoutSignal = AbortSignal.timeout(timeoutMs);
    const boundedSignal = AbortSignal.any([signal, timeoutSignal]);
    while (true) {
      const job = await this.status(jobId, boundedSignal);
      if (job.status === 'error')
        throw new Error(job.error || 'Agent job failed');
      if (job.status === 'done' || job.status === 'cancelled') return job;
      await delay(2000, boundedSignal);
    }
    // Client timeout does not cancel the server job. Preserve jobId to resume.
  }
  async result(
    jobId: string,
    signal: AbortSignal,
    fileId?: string,
  ): Promise<Blob> {
    const url = new URL(`${this.runBase}/result/${encodeURIComponent(jobId)}`);
    if (fileId) url.searchParams.set('file', fileId);
    const response = await requireOk(
      await fetch(url, { headers: await this.headers(), signal }),
    );
    return response.blob();
  }
  async cancel(jobId: string, signal: AbortSignal): Promise<unknown> {
    const response = await requireOk(
      await fetch(`${this.runBase}/cancel/${encodeURIComponent(jobId)}`, {
        method: 'POST',
        headers: await this.headers(),
        signal,
      }),
    );
    return response.json();
  }
}
