/**
 * P6.1: Advisory routing scores.
 *
 * Combines three signals per (provider_id, model) into an advisory score and
 * a set of category badges surfaced in the BooChat model picker:
 *   - eval results   (eval_runs.aggregate.avgScore, split by suite kind)
 *   - live latency   (control_requests gen_tps + duration over a recent window)
 *   - host health    (fleet liveness — an unhealthy host can win no badge)
 *
 * Advisory only: this never enforces routing. It powers display badges
 * ("best code model right now") and the P7 gateway candidate ordering.
 *
 * The pure scoring/badge helpers are extracted for unit testing per the
 * turn-guard.ts pattern; the DB read lives in computeRoutingScores().
 */

import type { Sql } from '../db.js';
import type { FleetState } from './fleet-state.js';

/** Recent-activity window for live latency signals. */
const LIVE_WINDOW_HOURS = 24;

export interface ModelScore {
  /** Composite picker id: `${providerId}/${model}` (matches /api/models). */
  compositeId: string;
  providerId: string;
  model: string;
  /** Avg score (0..1) from completed code-suite eval runs, or null. */
  codeScore: number | null;
  /** Avg score (0..1) from completed chat-suite eval runs, or null. */
  chatScore: number | null;
  /** Best eval score across kinds, or null when never evaluated. */
  evalScore: number | null;
  /** Avg gen tok/s over the live window, or null when no recent traffic. */
  avgGenTps: number | null;
  /** Avg request duration (ms) over the live window, or null. */
  avgLatencyMs: number | null;
  /** Recent request count in the live window. */
  sampleCount: number;
  /** Avg gen tok/s over the last 5 minutes from route_dispatch_log, or null. */
  recentGenTps: number | null;
  /** Whether the owning host is currently connected. */
  healthy: boolean;
  /** Category badges this model currently wins. */
  badges: BadgeKind[];
}

export type BadgeKind = 'best-code' | 'best-chat' | 'best-fast';

export const BADGE_LABELS: Record<BadgeKind, string> = {
  'best-code': 'Best code model now',
  'best-chat': 'Best chat model now',
  'best-fast': 'Fastest model now',
};

interface EvalRow {
  provider_id: string;
  model: string;
  suite_kind: string;
  avg_score: number | null;
}

interface LatencyRow {
  provider_id: string;
  model: string;
  avg_gen_tps: number | null;
  avg_duration_ms: number | null;
  sample_count: number;
}

/**
 * Pure badge assignment: given the per-model signals, award one winner per
 * category. Only healthy hosts are eligible; ties broken by first-seen order
 * (callers sort deterministically before passing in).
 */
export function assignBadges(scores: ModelScore[]): void {
  const eligible = scores.filter((s) => s.healthy);

  const award = (
    pick: (s: ModelScore) => number | null,
    badge: BadgeKind,
  ): void => {
    let best: ModelScore | null = null;
    let bestVal = -Infinity;
    for (const s of eligible) {
      const v = pick(s);
      if (v == null) continue;
      if (v > bestVal) {
        bestVal = v;
        best = s;
      }
    }
    if (best && bestVal > -Infinity) {
      best.badges.push(badge);
    }
  };

  award((s) => s.codeScore, 'best-code');
  award((s) => s.chatScore, 'best-chat');
  award((s) => s.avgGenTps, 'best-fast');
}

/**
 * Compute advisory routing scores across all (provider_id, model) pairs that
 * have either eval history or recent live traffic.
 */
export async function computeRoutingScores(
  sql: Sql,
  fleet: FleetState,
): Promise<ModelScore[]> {
  // 1. Eval scores — latest completed run per (provider, model, kind).
  //    Take the most recent finished run's aggregate avgScore per kind so a
  //    fresh run supersedes stale numbers.
  const evalRows = await sql<EvalRow[]>`
    SELECT er.provider_id,
           er.model,
           es.kind AS suite_kind,
           (er.aggregate::jsonb ->> 'avgScore')::float AS avg_score
    FROM eval_runs er
    JOIN eval_suites es ON er.suite_id = es.id
    WHERE er.status = 'completed'
      AND er.aggregate IS NOT NULL
      AND er.finished_at = (
        SELECT MAX(er2.finished_at)
        FROM eval_runs er2
        JOIN eval_suites es2 ON er2.suite_id = es2.id
        WHERE er2.provider_id = er.provider_id
          AND er2.model = er.model
          AND es2.kind = es.kind
          AND er2.status = 'completed'
      )
  `;

  // 2. Live latency/throughput — recent control_requests per (provider, model).
  const cutoff = new Date(Date.now() - LIVE_WINDOW_HOURS * 3600_000).toISOString();
  const latencyRows = await sql<LatencyRow[]>`
    SELECT provider_id,
           model,
           AVG(gen_tps) FILTER (WHERE gen_tps > 0) AS avg_gen_tps,
           AVG(duration_ms) FILTER (WHERE duration_ms > 0) AS avg_duration_ms,
           COUNT(*)::int AS sample_count
    FROM control_requests
    WHERE ts >= ${cutoff}
      AND model IS NOT NULL
    GROUP BY provider_id, model
  `;

  // 2.5. Recent latency — control_requests last 5 minutes for EMA blend.
  const recentCutoff = new Date(Date.now() - 5 * 60_000).toISOString();
  const recentLatencyRows = await sql<{ provider_id: string; model: string; recent_tps: number | null }[]>`
    SELECT provider_id,
           model,
           AVG(gen_tps) FILTER (WHERE gen_tps > 0) AS recent_tps
    FROM control_requests
    WHERE ts >= ${recentCutoff}
      AND model IS NOT NULL
    GROUP BY provider_id, model
  `;

  // 3. Merge signals keyed by compositeId.
  const byKey = new Map<string, ModelScore>();
  const keyOf = (providerId: string, model: string) => `${providerId}/${model}`;

  const ensure = (providerId: string, model: string): ModelScore => {
    const compositeId = keyOf(providerId, model);
    let s = byKey.get(compositeId);
    if (!s) {
      s = {
        compositeId,
        providerId,
        model,
        codeScore: null,
        chatScore: null,
        evalScore: null,
        avgGenTps: null,
        avgLatencyMs: null,
        recentGenTps: null,
        sampleCount: 0,
        healthy: fleet.hosts.get(providerId)?.liveness === 'connected',
        badges: [],
      };
      byKey.set(compositeId, s);
    }
    return s;
  };

  for (const row of evalRows) {
    const s = ensure(row.provider_id, row.model);
    if (row.suite_kind === 'code') s.codeScore = row.avg_score;
    else if (row.suite_kind === 'chat') s.chatScore = row.avg_score;
    const best = Math.max(s.codeScore ?? -Infinity, s.chatScore ?? -Infinity);
    s.evalScore = best > -Infinity ? best : null;
  }

  for (const row of latencyRows) {
    const s = ensure(row.provider_id, row.model);
    s.avgGenTps = row.avg_gen_tps;
    s.avgLatencyMs = row.avg_duration_ms;
    s.sampleCount = row.sample_count;
  }

  for (const row of recentLatencyRows) {
    const s = ensure(row.provider_id, row.model);
    s.recentGenTps = row.recent_tps;
  }

  // 4. EMA blend: effective gen_tps = 0.7 * recent + 0.3 * history.
  // Fall through to history-only when recent is null.
  for (const s of byKey.values()) {
    if (s.recentGenTps != null && s.avgGenTps != null) {
      s.avgGenTps = 0.7 * s.recentGenTps + 0.3 * s.avgGenTps;
    }
  }

  // Deterministic order before badge assignment so ties are stable.
  const scores = Array.from(byKey.values()).sort((a, b) =>
    a.compositeId < b.compositeId ? -1 : a.compositeId > b.compositeId ? 1 : 0,
  );

  assignBadges(scores);
  return scores;
}
