/**
 * KANDIMA SDK — TypeScript
 *
 * Save as kandima.ts. No dependencies (uses fetch). Works in Node 20+, Bun, Deno
 * and the browser. Generated from the OpenAPI spec at:
 *     https://kandima.ai/api/v1/openapi.json
 *
 * Usage:
 *     import { Kandima } from "./kandima";
 *     const k = new Kandima(process.env.KANDIMA_API_KEY!);
 *     const me = await k.me();
 *     const credits = await k.credits();
 *     const customers = await k.customers({ limit: 50 });
 *
 *     // Outbound webhooks
 *     const webhook = await k.webhooks.create({
 *       url: "https://my-app.com/webhooks/kandima",
 *       events: ["purchase.created", "alert.fired"],
 *     });
 *     console.log("Save this secret:", webhook.secret);
 *
 *     // Custom event ingest
 *     await k.track({ event: "checkout_started", props: { plan: "pro" } });
 *
 * For HMAC signature verification on inbound webhooks see verifyWebhook() at
 * the bottom of this file.
 */

export type ApiKey = `ka_live_${string}` | `ka_test_${string}`;

export type KandimaOptions = {
  baseUrl?: string;          // default https://kandima.ai
  fetch?: typeof fetch;      // override (e.g. node-fetch, Cloudflare Workers fetch)
  timeoutMs?: number;        // default 15000
  retries?: number;          // default 0 (no auto-retry)
};

export class KandimaError extends Error {
  constructor(
    public status: number,
    public code: string,
    message: string,
    public details?: unknown,
  ) {
    super(message);
    this.name = "KandimaError";
  }
}

export class Kandima {
  private baseUrl: string;
  private apiKey: string;
  private fetcher: typeof fetch;
  private timeoutMs: number;
  private retries: number;

  webhooks: WebhooksAPI;

  constructor(apiKey: string, opts: KandimaOptions = {}) {
    if (!apiKey || !apiKey.startsWith("ka_")) {
      throw new Error("Kandima: invalid api key (must start with ka_)");
    }
    this.apiKey = apiKey;
    this.baseUrl = (opts.baseUrl ?? "https://kandima.ai").replace(/\/$/, "");
    this.fetcher = opts.fetch ?? globalThis.fetch.bind(globalThis);
    this.timeoutMs = opts.timeoutMs ?? 15_000;
    this.retries = opts.retries ?? 0;
    this.webhooks = new WebhooksAPI(this);
  }

  // Public unauthenticated check
  async status(): Promise<{ status: string; api_version: string }> {
    return this.request("GET", "/api/v1/status", { auth: false });
  }

  async me(): Promise<{ user_id: string; email: string; plan?: string; workspace?: string }> {
    return this.request("GET", "/api/v1/me");
  }

  async credits(): Promise<{
    balance: number;
    total_purchased: number;
    total_spent: number;
    auto_topup_enabled: boolean;
  }> {
    return this.request("GET", "/api/v1/credits");
  }

  async customers(opts: { limit?: number; offset?: number } = {}) {
    return this.request<{ data: Customer[]; total: number }>(
      "GET",
      `/api/v1/customers?limit=${opts.limit ?? 100}&offset=${opts.offset ?? 0}`,
    );
  }

  async integrations() {
    return this.request<{ data: Integration[] }>("GET", "/api/v1/integrations");
  }

  async insights(opts: { status?: "open" | "closed"; severity?: string } = {}) {
    const params = new URLSearchParams();
    if (opts.status) params.set("status", opts.status);
    if (opts.severity) params.set("severity", opts.severity);
    return this.request<{ data: Insight[] }>(
      "GET",
      `/api/v1/insights${params.toString() ? `?${params.toString()}` : ""}`,
    );
  }

  async profit(opts: { days?: number } = {}) {
    return this.request<{ data: ProfitDay[]; totals: ProfitTotals; window_days: number }>(
      "GET",
      `/api/v1/profit?days=${opts.days ?? 30}`,
    );
  }

  async conversations(opts: { channel?: "whatsapp" | "messenger" | "instagram"; status?: string; limit?: number } = {}) {
    const params = new URLSearchParams();
    if (opts.channel) params.set("channel", opts.channel);
    if (opts.status) params.set("status", opts.status);
    if (opts.limit) params.set("limit", String(opts.limit));
    return this.request<{ channel: string; items: unknown[] }>(
      "GET",
      `/api/v1/conversations${params.toString() ? `?${params.toString()}` : ""}`,
    );
  }

  async conversationsStats(opts: { days?: number } = {}) {
    return this.request<{
      days: number;
      since: string;
      conversations: number;
      messages: { inbound: number; outbound: number; ai_generated: number };
      escalations: number;
      sales: { won_count: number; revenue_usd: number; truncated: boolean };
      automations: { sends_total: number; by_kind: Array<{ kind: string; sends: number }>; truncated: boolean } | null;
    }>("GET", `/api/v1/conversations/stats?days=${opts.days ?? 30}`);
  }

  async anomalies(opts: { days?: number; severity?: string } = {}) {
    const params = new URLSearchParams();
    if (opts.days) params.set("days", String(opts.days));
    if (opts.severity) params.set("severity", opts.severity);
    return this.request<{ data: Anomaly[]; window_days: number }>(
      "GET",
      `/api/v1/anomalies${params.toString() ? `?${params.toString()}` : ""}`,
    );
  }

  async inventory(opts: { status?: "critical" | "low" | "ok" | "overstocked"; limit?: number } = {}) {
    const params = new URLSearchParams();
    if (opts.status) params.set("status", opts.status);
    if (opts.limit) params.set("limit", String(opts.limit));
    return this.request<{ items: InventoryItem[]; summary: InventorySummary }>(
      "GET",
      `/api/v1/inventory${params.toString() ? `?${params.toString()}` : ""}`,
    );
  }

  async reorder(opts: { status?: "pending" | "ordered" | "dismissed"; limit?: number } = {}) {
    const params = new URLSearchParams();
    if (opts.status) params.set("status", opts.status);
    if (opts.limit) params.set("limit", String(opts.limit));
    return this.request<{ items: ReorderSuggestion[] }>(
      "GET",
      `/api/v1/reorder${params.toString() ? `?${params.toString()}` : ""}`,
    );
  }

  async track(payload: TrackEventInput): Promise<{ ok: true; event_id: number }> {
    return this.request("POST", "/api/v1/track", { body: payload });
  }

  // ───────────────────────────── internals ─────────────────────────────
  async request<T>(
    method: "GET" | "POST" | "DELETE",
    path: string,
    opts: { body?: unknown; auth?: boolean } = {},
  ): Promise<T> {
    const headers: Record<string, string> = {};
    if (opts.auth !== false) headers["Authorization"] = `Bearer ${this.apiKey}`;
    if (opts.body !== undefined) headers["Content-Type"] = "application/json";

    let lastError: unknown;
    const attempts = Math.max(1, 1 + this.retries);
    for (let i = 0; i < attempts; i++) {
      const ctrl = new AbortController();
      const t = setTimeout(() => ctrl.abort(), this.timeoutMs);
      try {
        const r = await this.fetcher(`${this.baseUrl}${path}`, {
          method,
          headers,
          body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
          signal: ctrl.signal,
        });
        clearTimeout(t);
        const text = await r.text();
        const data = text ? safeJson(text) : undefined;
        if (!r.ok) {
          const code =
            (data as { error?: string })?.error ?? `http_${r.status}`;
          if (r.status >= 500 && i < attempts - 1) {
            await sleep(200 * Math.pow(2, i));
            continue;
          }
          throw new KandimaError(r.status, code, `KANDIMA ${method} ${path} failed (${r.status})`, data);
        }
        return data as T;
      } catch (err) {
        clearTimeout(t);
        lastError = err;
        if (err instanceof KandimaError) throw err;
        if (i >= attempts - 1) {
          throw new KandimaError(0, "network_error", String(err));
        }
        await sleep(200 * Math.pow(2, i));
      }
    }
    throw lastError;
  }
}

class WebhooksAPI {
  constructor(private k: Kandima) {}

  list() {
    return this.k.request<{ webhooks: Webhook[]; allowed_events: string[] }>(
      "GET",
      "/api/v1/webhooks",
    );
  }

  create(input: { url: string; events: string[] }) {
    return this.k.request<Webhook & { secret: string; warning: string }>(
      "POST",
      "/api/v1/webhooks",
      { body: input },
    );
  }

  delete(id: number) {
    return this.k.request<{ ok: true }>("DELETE", `/api/v1/webhooks?id=${id}`);
  }
}

// ───────────────────────────── HMAC verify ─────────────────────────────
/**
 * Verify an inbound KANDIMA webhook signature.
 * Pass the raw request body (string) and the secret you saved when creating
 * the webhook. Throws KandimaError if invalid.
 *
 * Compatible with Node 20+, Bun, Deno, Cloudflare Workers (Web Crypto).
 */
export async function verifyWebhook(
  rawBody: string,
  headers: { signature: string; timestamp: string },
  secret: string,
): Promise<void> {
  const cleanSecret = secret.replace(/^whsec_/, "");
  const ts = Number(headers.timestamp);
  if (!Number.isFinite(ts)) {
    throw new KandimaError(400, "invalid_timestamp", "x-kandima-timestamp not a number");
  }
  if (Math.abs(Date.now() / 1000 - ts) > 300) {
    throw new KandimaError(400, "timestamp_skew", "Signature timestamp >5min skew");
  }
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    enc.encode(cleanSecret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const sig = await crypto.subtle.sign("HMAC", key, enc.encode(`${ts}.${rawBody}`));
  const expected = Array.from(new Uint8Array(sig))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  if (!timingSafeEqual(headers.signature, expected)) {
    throw new KandimaError(401, "invalid_signature", "HMAC signature does not match");
  }
}

function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let mismatch = 0;
  for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return mismatch === 0;
}

function safeJson(text: string): unknown {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

function sleep(ms: number): Promise<void> {
  return new Promise((r) => setTimeout(r, ms));
}

// ───────────────────────────── types ─────────────────────────────
export type Customer = {
  id: number;
  email: string | null;
  name: string | null;
  country: string | null;
  orders_count: number;
  ltv_estimate: number;
  segment: string | null;
  churn_score: number | null;
  last_seen_at: string | null;
};

export type Integration = {
  provider: string;
  account_label: string | null;
  status: string;
  scopes: string[];
  connected_at: string;
  last_sync_at: string | null;
};

export type Insight = {
  id: number;
  title: string;
  body: string;
  severity: "info" | "warning" | "critical";
  status: "open" | "closed";
  action: string | null;
  last_seen: string;
};

export type Anomaly = {
  id: number;
  metric: string;
  observed_value: number;
  expected_value: number;
  z_score: number;
  direction: "spike" | "drop" | "shift";
  severity: "low" | "medium" | "high" | "critical";
  inferred_cause: string | null;
  confidence: number | null;
  detected_at: string;
};

export type ProfitDay = {
  snapshot_date: string;
  gross_revenue: number;
  refunds: number;
  ad_spend: number;
  cogs_estimate: number;
  fees_estimate: number;
  shipping_estimate: number;
  extra_expenses: number;
  net_revenue: number;
  operational_profit: number;
  final_profit: number;
  margin_pct: number;
  purchase_count: number;
};

export type ProfitTotals = {
  days_returned: number;
  gross_revenue: number;
  ad_spend: number;
  final_profit: number;
  purchase_count: number;
  avg_margin_pct: number;
};

export type InventoryItem = {
  sku: string;
  product_name: string;
  current_stock: number;
  units_sold_30d: number;
  units_sold_7d: number;
  avg_daily_units_30d: number;
  avg_daily_units_7d: number;
  days_of_stock: number;
  trend: "accelerating" | "decelerating" | "stable";
  status: "critical" | "low" | "ok" | "overstocked";
  estimated_stockout_at: string | null;
  computed_at: string;
};

export type InventorySummary = {
  total: number;
  critical: number;
  low: number;
  ok: number;
  overstocked: number;
};

export type ReorderSuggestion = {
  id: number;
  inventory_item_id: number;
  qty_suggested: number;
  reason: string;
  confidence: number | null;
  forecast_days: number;
  computed_at: string;
  status: "pending" | "ordered" | "dismissed";
};

export type Webhook = {
  id: number;
  url: string;
  events: string[];
  active: boolean;
  last_fired_at: string | null;
  last_status: number | null;
  fail_count: number;
  total_fired: number;
  total_failed: number;
};

export type TrackEventInput = {
  event: string;
  props?: Record<string, unknown>;
  url?: string;
  path?: string;
  title?: string;
  referrer?: string;
  visitor_id?: string;
  session_id?: string;
  client_ts?: string;
};
