import type { DatabaseAdapter } from "../db.js";
import { ValidationError, assertAllowedKeys } from "../errors.js";
import { SESSION_SOURCES, type AnalyticsSource } from "../sources.js";
import { parseOptionalEnumArray } from "../validation.js";

interface ModelsInput {
  sources?: AnalyticsSource[];
  date_from?: string;
  date_to?: string;
  limit: number;
  offset: number;
  order_by: "sessions" | "observed_tokens";
}

function parseIso(value: unknown): string | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new ValidationError("Invalid date.");
  return new Date(value).toISOString();
}

function parseInput(args: unknown): ModelsInput {
  const obj = (args && typeof args === "object" && !Array.isArray(args) ? args : {}) as Record<string, unknown>;
  assertAllowedKeys(obj, ["sources", "date_from", "date_to", "limit", "offset", "order_by"]);

  const sources = parseOptionalEnumArray(obj.sources, SESSION_SOURCES, "sources");
  if (obj.limit !== undefined && typeof obj.limit !== "number") throw new ValidationError("limit must be a number.");
  if (obj.offset !== undefined && typeof obj.offset !== "number") throw new ValidationError("offset must be a number.");
  const limit = typeof obj.limit === "number" ? Math.max(1, Math.min(1000, Math.floor(obj.limit))) : 100;
  const offset = typeof obj.offset === "number" ? Math.max(0, Math.floor(obj.offset)) : 0;
  if (obj.order_by !== undefined && obj.order_by !== "sessions" && obj.order_by !== "observed_tokens") throw new ValidationError("order_by must be sessions or observed_tokens.");
  return { sources, date_from: parseIso(obj.date_from), date_to: parseIso(obj.date_to), limit, offset, order_by: obj.order_by === "observed_tokens" ? "observed_tokens" : "sessions" };
}

export async function queryModels(db: DatabaseAdapter, args: unknown): Promise<Record<string, unknown>> {
  const input = parseInput(args);
  const where: string[] = [`s.source IN (${SESSION_SOURCES.map(() => "?").join(",")})`];
  const params: unknown[] = [...SESSION_SOURCES];
  if (input.sources) {
    where.push(`s.source IN (${input.sources.map(() => "?").join(",")})`);
    params.push(...input.sources);
  }
  if (input.date_from) {
    where.push("s.updated_at >= ?");
    params.push(input.date_from);
  }
  if (input.date_to) {
    where.push("s.updated_at <= ?");
    params.push(input.date_to);
  }
  where.push("COALESCE(s.session_kind, 'main') = 'main'");
  where.push("s.user_message_count > 0");

  const rows = await db.all<{
    model: string | null;
    unknown_source: AnalyticsSource | null;
    model_reason: string;
    reason_count: number;
    sessions: number;
    messages: number;
    input_tokens: number;
    output_tokens: number;
    reasoning_tokens: number;
    token_available_count: number;
    token_missing_count: number;
    partial_token_data_count: number;
  }>(
    `
    SELECT
      COALESCE(NULLIF(mm.model, ''), NULLIF(s.model_primary,''), 'unknown') as model,
      CASE WHEN NULLIF(mm.model,'') IS NULL AND NULLIF(s.model_primary,'') IS NULL THEN s.source ELSE NULL END as unknown_source,
      CASE WHEN NULLIF(mm.model,'') IS NULL AND NULLIF(s.model_primary,'') IS NULL
        THEN COALESCE(CASE WHEN json_valid(mm.metadata_json) THEN json_extract(mm.metadata_json, '$.model_reason') END, 'MODEL_REASON_UNDETERMINED') ELSE '' END as model_reason,
      COUNT(*) as reason_count,
      COUNT(DISTINCT s.id) as sessions,
      COALESCE(SUM(CASE WHEN mm.role = 'user' THEN 1 ELSE 0 END),0) as messages,
      COALESCE(SUM(mm.input_tokens),0) as input_tokens,
      COALESCE(SUM(mm.output_tokens),0) as output_tokens,
      COALESCE(SUM(mm.reasoning_tokens),0) as reasoning_tokens,
      COALESCE(SUM(CASE WHEN mm.token_available = 1 THEN 1 ELSE 0 END),0) as token_available_count,
      COALESCE(SUM(CASE WHEN mm.token_available = 0 THEN 1 ELSE 0 END),0) as token_missing_count,
      COALESCE(SUM(CASE WHEN mm.partial_token_data = 1 THEN 1 ELSE 0 END),0) as partial_token_data_count
    FROM sessions s
    LEFT JOIN message_metrics mm ON mm.session_id = s.id
    WHERE ${where.join(" AND ")}
    GROUP BY COALESCE(NULLIF(mm.model, ''), NULLIF(s.model_primary,''), 'unknown'),
      CASE WHEN NULLIF(mm.model,'') IS NULL AND NULLIF(s.model_primary,'') IS NULL THEN s.source ELSE NULL END,
      CASE WHEN NULLIF(mm.model,'') IS NULL AND NULLIF(s.model_primary,'') IS NULL THEN COALESCE(CASE WHEN json_valid(mm.metadata_json) THEN json_extract(mm.metadata_json, '$.model_reason') END, 'MODEL_REASON_UNDETERMINED') ELSE '' END
    `,
    params
  );

  // `rows` is split by reason only to build diagnostics. Totals are merged by model/source below,
  // so a session carrying multiple reasons is never counted twice in the public row.
  const sourceTokenRows = await db.all<{ source: AnalyticsSource; observed_tokens: number }>(`SELECT s.source, COALESCE(SUM(COALESCE(mm.input_tokens,0)+COALESCE(mm.output_tokens,0)),0) observed_tokens FROM sessions s LEFT JOIN message_metrics mm ON mm.session_id=s.id WHERE ${where.join(" AND ")} GROUP BY s.source`, params);
  const sourceObserved = new Map<AnalyticsSource, number>(sourceTokenRows.map((row) => [row.source, row.observed_tokens]));
  const totalObserved = sourceTokenRows.reduce((total, row) => total + (row.observed_tokens ?? 0), 0);
  const unknownTotals = await db.all<{ source: AnalyticsSource; sessions: number; messages: number; input_tokens: number; output_tokens: number; reasoning_tokens: number; token_available_count: number; token_missing_count: number; partial_token_data_count: number }>(
    `SELECT s.source, COUNT(DISTINCT s.id) sessions, COALESCE(SUM(CASE WHEN mm.role='user' THEN 1 ELSE 0 END),0) messages, COALESCE(SUM(COALESCE(mm.input_tokens,0)),0) input_tokens, COALESCE(SUM(COALESCE(mm.output_tokens,0)),0) output_tokens, COALESCE(SUM(COALESCE(mm.reasoning_tokens,0)),0) reasoning_tokens, COALESCE(SUM(CASE WHEN mm.token_available=1 THEN 1 ELSE 0 END),0) token_available_count, COALESCE(SUM(CASE WHEN mm.token_available=0 THEN 1 ELSE 0 END),0) token_missing_count, COALESCE(SUM(CASE WHEN mm.partial_token_data=1 THEN 1 ELSE 0 END),0) partial_token_data_count FROM sessions s LEFT JOIN message_metrics mm ON mm.session_id=s.id WHERE ${where.join(" AND ")} AND NULLIF(mm.model,'') IS NULL AND NULLIF(s.model_primary,'') IS NULL GROUP BY s.source`, params
  );
  const unknownBySource = new Map(unknownTotals.map((row) => [row.source, row]));
  const grouped = new Map<string, Record<string, unknown>>();
  for (const row of rows) {
    const unknown = row.unknown_source !== null;
    const key = unknown ? `unknown:${row.unknown_source}` : `known:${row.model}`;
    const sourceTotals = unknown ? unknownBySource.get(row.unknown_source as AnalyticsSource) : undefined;
    const current = grouped.get(key) ?? {
      model: unknown ? `Unknown — ${row.unknown_source}` : row.model, source: unknown ? row.unknown_source : undefined,
      is_unknown: unknown, adapter: unknown ? row.unknown_source : undefined, sessions: sourceTotals?.sessions ?? 0, messages: sourceTotals?.messages ?? 0, input_tokens: sourceTotals?.input_tokens ?? 0, output_tokens: sourceTotals?.output_tokens ?? 0,
      reasoning_tokens: sourceTotals?.reasoning_tokens ?? 0, token_available_count: sourceTotals?.token_available_count ?? 0, token_missing_count: sourceTotals?.token_missing_count ?? 0, partial_token_data: (sourceTotals?.partial_token_data_count ?? 0) > 0,
      reason_breakdown: [] as Array<{ code: string; count: number }>
    };
    // For unknown rows the grouped SQL rows overlap only by reason: keep the first row's totals
    // and aggregate reasons independently. Known rows have a single reason-less group.
    if (!(unknown && grouped.has(key)) && !unknown) {
      for (const field of ['sessions','messages','input_tokens','output_tokens','reasoning_tokens','token_available_count','token_missing_count'] as const) current[field] = Number(current[field]) + Number(row[field] ?? 0);
      current.partial_token_data = Boolean(current.partial_token_data) || row.partial_token_data_count > 0;
    }
    if (unknown) {
      const reasons = current.reason_breakdown as Array<{ code: string; count: number }>;
      const reason = ['MODEL_NOT_EXPOSED','MODEL_FIELD_MISSING','MODEL_FIELD_INVALID','MODEL_SCHEMA_UNSUPPORTED','MODEL_PARTIAL_IMPORT','MODEL_REASON_UNDETERMINED'].includes(row.model_reason) ? row.model_reason : 'MODEL_REASON_UNDETERMINED';
      const found = reasons.find((entry) => entry.code === reason);
      if (found) found.count += row.reason_count; else reasons.push({ code: reason, count: row.reason_count });
    }
    if (unknown) (current.reason_breakdown as Array<{ code: string; count: number }>).sort((a, b) => a.code.localeCompare(b.code));
    grouped.set(key, current);
  }

  return {
    ok: true,
    limit: input.limit,
    offset: input.offset,
    order_by: input.order_by,
    models: [...grouped.values()].map((row): Record<string, unknown> => ({ ...row, observed_tokens: Number(row.input_tokens) + Number(row.output_tokens) })).sort((a, b) => Number(b[input.order_by]) - Number(a[input.order_by]) || String(a.model).localeCompare(String(b.model))).slice(input.offset, input.offset + input.limit).map((row) => {
      if (row.is_unknown) {
        const observed_tokens = Number(row.observed_tokens);
        return { ...row, observed_tokens, source_observed_percentage: (sourceObserved.get(row.source as AnalyticsSource) ?? 0) > 0 ? observed_tokens * 100 / (sourceObserved.get(row.source as AnalyticsSource) ?? 1) : 0, total_observed_percentage: totalObserved > 0 ? observed_tokens * 100 / totalObserved : 0, no_model_deduced: true };
      }
      const { is_unknown, adapter, source, reason_breakdown, ...known } = row; return known;
    })
  };
}
