// Extracted from src/db/repository.ts (mechanical split — bodies unchanged).
// llm-usage repository domain. Facade: src/db/repository.ts (Repository class).
import Database from 'better-sqlite3';

/** Per-call delta for the per-user daily LLM usage ledger. */
export interface LlmUsageIncrement {
  /** UTC day bucket 'YYYY-MM-DD'. Defaults to today (UTC) when omitted. */
  day?: string;
  /** Owner id, or 'local' (no-auth) / 'system' (ownerless) sentinel. */
  userId: string;
  source: 'gateway' | 'direct';
  /** Real model name (chunk.model), routing-key fallback, or 'unknown'. */
  model: string;
  /** Backend server name (gateway backendId / direct host), or 'unknown'. */
  route: string;
  tokensIn?: number;
  tokensOut?: number;
  requests?: number;
  at?: string;
}


/** Daily-grouped aggregate row (model/route collapsed) for the usage API. */
export interface LlmUsageDailyAgg {
  day: string;
  userId: string;
  source: string;
  tokensIn: number;
  tokensOut: number;
  requests: number;
}


/** Hour-grain UPSERT input for the v2 usage ledger. */
export interface LlmUsageHourlyIncrement {
  /** UTC hour bucket 'YYYY-MM-DDTHH'. Defaults to the current hour (UTC). */
  hour?: string;
  /** Owner id, 'local' / 'system' sentinel, or 'gw:<team>' for downstream gateway consumers. */
  userId: string;
  /**
   * 'direct' / 'gateway' are this orchestrator's own calls. 'gateway_downstream'
   * is traffic the AAO Gateway proxied for an EXTERNAL consumer (recorded from
   * the gateway proxy path, keyed by team) — kept distinct so it never merges
   * with our own usage. See docs/superpowers/specs/2026-06-19-usage-hourly-profile-and-gateway-downstream-design.md
   */
  source: 'gateway' | 'direct' | 'gateway_downstream';
  /** Real model name (chunk.model), routing-key fallback, or 'unknown'. */
  model: string;
  /** Backend server name (gateway backendId / direct host), or 'unknown'. */
  route: string;
  tokensIn?: number;
  tokensOut?: number;
  requests?: number;
  /** ISO timestamp for last_updated_at + hour default. Defaults to now. */
  at?: string;
}


/**
 * Raw hour-grain ledger row (no axis collapsed) for the usage API. The API
 * re-buckets `hour` into the viewer's local calendar period and groups by
 * whichever of source/model/route/user/org the request asked for.
 */
export interface LlmUsageHourlyRow {
  hour: string;
  userId: string;
  source: string;
  model: string;
  route: string;
  tokensIn: number;
  tokensOut: number;
  requests: number;
}


  // ── Per-user daily LLM usage (gateway + direct) ──────────────────────
  //
  // Recorded at the OpenAICompatClient completion boundary for every
  // successful chat completion. UPSERT on the (day, user_id, source,
  // model, route) grain. Separate lens from gateway_key_usage — never
  // summed across the two tables. Spec:
  // docs/superpowers/specs/2026-06-11-llm-usage-aggregation-design.md

  /**
   * UPSERT: bump per-(day, user, source, model, route) counters. Deltas
   * are clamped at zero. `day` defaults to the UTC day of `at` (or now).
   * Called once per successful stream completion; `usage`-less completions
   * still bump `requests` (tokens 0) so a 0-token request is distinct from
   * a failed/aborted one (which is never recorded).
   */
  export function incrementLlmUsage(db: Database.Database, params: LlmUsageIncrement): void {
    const tIn = Math.max(0, Math.floor(params.tokensIn ?? 0));
    const tOut = Math.max(0, Math.floor(params.tokensOut ?? 0));
    const reqs = Math.max(0, Math.floor(params.requests ?? 1));
    const ts = params.at ?? new Date().toISOString();
    const day = params.day ?? ts.slice(0, 10);
    db
      .prepare(
        `INSERT INTO llm_usage_daily
           (day, user_id, source, model, route, tokens_in, tokens_out, requests, last_updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON CONFLICT (day, user_id, source, model, route) DO UPDATE SET
           tokens_in       = tokens_in  + excluded.tokens_in,
           tokens_out      = tokens_out + excluded.tokens_out,
           requests        = requests   + excluded.requests,
           last_updated_at = excluded.last_updated_at`,
      )
      .run(day, params.userId, params.source, params.model, params.route, tIn, tOut, reqs, ts);
  }


  /**
   * Daily time series for the usage dashboard, grouped by (day, user_id,
   * source) with model/route collapsed. `userId` filter scopes a non-admin
   * to their own rows; omit it for the admin all-users view (callers can
   * collapse user_id afterwards). Inclusive `from`/`to` are 'YYYY-MM-DD'.
   */
  export function queryLlmUsageDaily(db: Database.Database, opts: { from: string; to: string; userId?: string }): LlmUsageDailyAgg[] {
    const where = ['day >= ?', 'day <= ?'];
    const args: unknown[] = [opts.from, opts.to];
    if (opts.userId !== undefined) {
      where.push('user_id = ?');
      args.push(opts.userId);
    }
    const rows = db
      .prepare(
        `SELECT day, user_id, source,
                SUM(tokens_in)  AS tokens_in,
                SUM(tokens_out) AS tokens_out,
                SUM(requests)   AS requests
           FROM llm_usage_daily
          WHERE ${where.join(' AND ')}
          GROUP BY day, user_id, source
          ORDER BY day ASC`,
      )
      .all(...args) as Array<{
        day: string;
        user_id: string;
        source: string;
        tokens_in: number;
        tokens_out: number;
        requests: number;
      }>;
    return rows.map((r) => ({
      day: r.day,
      userId: r.user_id,
      source: r.source,
      tokensIn: r.tokens_in,
      tokensOut: r.tokens_out,
      requests: r.requests,
    }));
  }


  /**
   * v2 write path: UPSERT per-(hour, user, source, model, route) counters.
   * `hour` defaults to the current UTC hour 'YYYY-MM-DDTHH'. Same contract as
   * incrementLlmUsage (deltas clamped at zero; usage-less completions still
   * bump `requests`). Supersedes incrementLlmUsage as the recorder target.
   */
  export function incrementLlmUsageHourly(db: Database.Database, params: LlmUsageHourlyIncrement): void {
    const tIn = Math.max(0, Math.floor(params.tokensIn ?? 0));
    const tOut = Math.max(0, Math.floor(params.tokensOut ?? 0));
    const reqs = Math.max(0, Math.floor(params.requests ?? 1));
    const ts = params.at ?? new Date().toISOString();
    const hour = params.hour ?? ts.slice(0, 13);
    db
      .prepare(
        `INSERT INTO llm_usage_hourly
           (hour, user_id, source, model, route, tokens_in, tokens_out, requests, last_updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON CONFLICT (hour, user_id, source, model, route) DO UPDATE SET
           tokens_in       = tokens_in  + excluded.tokens_in,
           tokens_out      = tokens_out + excluded.tokens_out,
           requests        = requests   + excluded.requests,
           last_updated_at = excluded.last_updated_at`,
      )
      .run(hour, params.userId, params.source, params.model, params.route, tIn, tOut, reqs, ts);
  }


  /**
   * Raw hour-grain rows for the usage dashboard v2, no axis collapsed so the
   * API can group by any of source/model/route/user/org and re-bucket into the
   * viewer's local timezone. `userId` scopes a non-admin to their own rows.
   * Inclusive `fromHour`/`toHour` are 'YYYY-MM-DDTHH' (UTC) — callers widen the
   * UTC window by ±1 day before filtering precisely against local days.
   */
  export function queryLlmUsageHourly(db: Database.Database, opts: {
    fromHour: string;
    toHour: string;
    userId?: string;
  }): LlmUsageHourlyRow[] {
    const where = ['hour >= ?', 'hour <= ?'];
    const args: unknown[] = [opts.fromHour, opts.toHour];
    if (opts.userId !== undefined) {
      where.push('user_id = ?');
      args.push(opts.userId);
    }
    const rows = db
      .prepare(
        `SELECT hour, user_id, source, model, route, tokens_in, tokens_out, requests
           FROM llm_usage_hourly
          WHERE ${where.join(' AND ')}
          ORDER BY hour ASC`,
      )
      .all(...args) as Array<{
        hour: string;
        user_id: string;
        source: string;
        model: string;
        route: string;
        tokens_in: number;
        tokens_out: number;
        requests: number;
      }>;
    return rows.map((r) => ({
      hour: r.hour,
      userId: r.user_id,
      source: r.source,
      model: r.model,
      route: r.route,
      tokensIn: r.tokens_in,
      tokensOut: r.tokens_out,
      requests: r.requests,
    }));
  }


  /**
   * Map every user to a single org label for the usage dashboard's "by org"
   * breakdown. Unions Gitea org cache (user_gitea_orgs) and local orgs
   * (local_org_members → local_orgs.name); a multi-org user collapses to the
   * alphabetically-first org name (MIN) so the breakdown is deterministic.
   * Users with no org (and the 'local'/'system' sentinels) are simply absent —
   * the API buckets them under 'no-org'.
   */
  export function getUsageOrgMap(db: Database.Database): Map<string, string> {
    const rows = db
      .prepare(
        `SELECT user_id, MIN(org_name) AS org_name FROM (
           SELECT user_id, org_name FROM user_gitea_orgs
           UNION ALL
           SELECT m.user_id AS user_id, o.name AS org_name
             FROM local_org_members m
             JOIN local_orgs o ON o.id = m.org_id
         )
         GROUP BY user_id`,
      )
      .all() as Array<{ user_id: string; org_name: string }>;
    const map = new Map<string, string>();
    for (const r of rows) map.set(r.user_id, r.org_name);
    return map;
  }
