/**
 * Douanier — typed client for the nech.pl `audio` API.
 *
 * Zero dependencies: uses the global `fetch` / `FormData` / `Blob` available in
 * Node 18+, Vercel Functions, and the Edge runtime. Every analysis takes an
 * audio clip (a `Blob` — e.g. from `await fetch(url).then(r => r.blob())`, or a
 * `File` from an upload) and returns a typed JSON result. Identical audio is
 * cached server-side: check `result._cache === "hit"`.
 *
 *   const dz = new Douanier({ token: process.env.DOUANIER_TOKEN! });
 *   const mood = await dz.emotion(clip);          // { valence, arousal, top, … }
 *   const spec = await dz.spectrum(clip, { bands: 64, frames: 200 });
 */

export interface DouanierOptions {
  /** Platform bearer token (provisioned by SRE for your tenant). */
  token: string;
  /** Defaults to the public gateway path. */
  baseUrl?: string;
  /** Inject a custom fetch (tests / non-standard runtimes). */
  fetchImpl?: typeof fetch;
}

export type Cache = "hit" | "miss";
type WithMeta<T> = T & { filename: string; content_id: string; _cache: Cache };

export interface Emotion {
  valence: number; arousal: number;
  top: [string, number][]; dist: Record<string, number>;
  confidence: number; engine: string;
}
export interface Features { features: Record<string, number | string>; tiers: Record<string, string[]>; sr: number; engine: string; }
export interface SampleProfile {
  profile: { rms_db: number; peak_db: number; centroid: number; bands: Record<string, number> };
  role: "percs" | "bass" | "melodic" | "tops" | "atmos";
  centroid: number | null; broadband: boolean; time_activity: number;
  name_hint: string | null; sr: number; engine: string;
}
export interface Grade {
  grade: { path: string; kind: "loop" | "one_shot"; grade: number; tier: "S" | "A" | "B" | "C" | "D";
    sub: Record<string, number>; metrics: Record<string, number>; flags: string[] };
}
export interface Onsets { tempo_bpm: number; onsets: number[]; n_onsets: number; onset_rate: number; duration_s: number; sr: number; engine: string; }
export interface Waveform { bins: number; peaks: [number, number][]; rms: number[]; duration_s: number; sr: number; engine: string; }
export interface Analyze { emotion: Emotion; features: Record<string, number | string>; sample: SampleProfile; }
export interface Loudness {
  lufs_integrated: number | null; peak_dbfs: number; true_peak_dbfs: number; rms_dbfs: number;
  crest_db: number; gain_to_target_db: { streaming: number; club: number } | null;
  channels: number; sr: number; engine: string;
}
export interface Spectrum { bands: number; frames: number; mel: boolean; band_hz: number[]; spec: number[][]; duration_s: number; sr: number; engine: string; }
export interface Naming { suggested_name: string; role: string; character: string; stem: string; lint: string[]; }

export class DouanierError extends Error {
  constructor(public status: number, public detail: unknown) {
    super(`Douanier ${status}: ${typeof detail === "string" ? detail : JSON.stringify(detail)}`);
  }
}

export class Douanier {
  private base: string;
  private token: string;
  private f: typeof fetch;

  constructor(opts: DouanierOptions) {
    this.token = opts.token;
    this.base = (opts.baseUrl ?? "https://api.nech.pl/audio/v1").replace(/\/$/, "");
    this.f = opts.fetchImpl ?? fetch;
  }

  private async post<T>(path: string, clip: Blob, query: Record<string, unknown> = {},
                        filename = "clip.wav"): Promise<WithMeta<T>> {
    const qs = Object.entries(query)
      .filter(([, v]) => v !== undefined && v !== null && v !== "")
      .map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
    const form = new FormData();
    form.append("file", clip, filename);
    const res = await this.f(`${this.base}${path}${qs ? `?${qs}` : ""}`, {
      method: "POST",
      headers: { Authorization: `Bearer ${this.token}` },
      body: form,
    });
    const body = await res.json().catch(() => ({}));
    if (!res.ok) throw new DouanierError(res.status, (body as any)?.detail ?? body);
    (body as any)._cache = (res.headers.get("X-Douanier-Cache") as Cache) ?? "miss";
    return body as WithMeta<T>;
  }

  /** Service health + per-engine availability (unauthenticated). */
  async health(): Promise<any> {
    const res = await this.f(`${this.base}/healthz`);
    return res.json();
  }

  emotion(clip: Blob, filename?: string)               { return this.post<Emotion & { emotion: Emotion }>("/analyze/emotion", clip, {}, filename); }
  features(clip: Blob, o: { rhythm?: boolean } = {})   { return this.post<Features>("/features", clip, o); }
  samples(clip: Blob, o: { name?: string } = {})       { return this.post<SampleProfile>("/analyze/samples", clip, o); }
  grade(clip: Blob, o: { role?: string } = {})         { return this.post<Grade>("/grade", clip, o); }
  onsets(clip: Blob)                                   { return this.post<Onsets>("/onsets", clip); }
  waveform(clip: Blob, o: { bins?: number } = {})      { return this.post<Waveform>("/waveform", clip, o); }
  analyze(clip: Blob)                                  { return this.post<Analyze>("/analyze", clip); }
  loudness(clip: Blob)                                 { return this.post<Loudness>("/loudness", clip); }
  spectrum(clip: Blob, o: { bands?: number; frames?: number; mel?: boolean } = {}) { return this.post<Spectrum>("/spectrum", clip, o); }
  naming(clip: Blob, o: { index?: number } = {})       { return this.post<Naming>("/naming", clip, o); }
}

export default Douanier;
