/**
 * P7.1: auto:* gateway candidate resolution.
 *
 * The gateway exposes OpenAI-compatible virtual models. A completion against
 * `auto:code` (etc.) is resolved to an ordered list of concrete candidate
 * composite ids ('provider/model'), then dispatched with failover.
 *
 * Ordering source:
 *   - An explicit route_policy for the virtual model (admin-curated candidates).
 *   - Otherwise, advisory routing scores ranked by the category metric.
 *
 * Health filtering (only connected hosts are eligible) is applied last so a
 * curated policy never dispatches to a down host.
 *
 * Pure helpers (orderCandidates, parseVirtualModel) are unit-tested; the DB
 * read lives in resolveCandidates().
 */

import type { Sql } from '../db.js';
import type { FleetState } from './fleet-state.js';
import { computeRoutingScores, type ModelScore } from './routing-scores.js';
import { jsonbStringArray } from './jsonb.js';
import { isTripped } from './circuit-breaker.js';
export { isGatewayVirtualModel } from '@boocode/contracts/gateway';

export const VIRTUAL_MODELS = ['auto', 'auto:code', 'auto:fast', 'auto:cheap'] as const;
export type VirtualModel = (typeof VIRTUAL_MODELS)[number];

/**
 * Strip a composite/provider prefix the picker may prepend. The gateway
 * registry provider id is 'auto', so BooChat may send 'auto/auto:code'.
 * Normalize to the bare virtual model token.
 */
export function parseVirtualModel(modelId: string): string {
  // Composite form: '<gatewayProviderId>/<virtual>' — take the part after '/'.
  const slash = modelId.indexOf('/');
  const tail = slash >= 0 ? modelId.slice(slash + 1) : modelId;
  return tail;
}

export interface RoutePolicyRow {
  virtual_model: string;
  candidates: unknown; // jsonb: porsager returns a parsed array (see jsonb.ts)
  fallback: string | null;
  enabled: boolean;
}

/**
 * Order concrete candidates for a virtual model. Pure.
 *
 * When an explicit policy is provided, its candidate list defines the order
 * (with the fallback appended last). Otherwise candidates are derived from
 * advisory scores ranked by the virtual model's category metric.
 *
 * The returned list is health-filtered: only composite ids whose host is
 * connected survive (a curated candidate on a down host is skipped, not
 * dispatched to).
 */
export function orderCandidates(
  virtualModel: string,
  policy: { candidates: string[]; fallback: string | null } | null,
  scores: ModelScore[],
): string[] {
  const healthy = new Set(scores.filter((s) => s.healthy).map((s) => s.compositeId));

  if (policy) {
    const ordered = [...policy.candidates];
    if (policy.fallback && !ordered.includes(policy.fallback)) ordered.push(policy.fallback);
    // Keep curated order; drop unhealthy. If a candidate isn't in the scores
    // set at all (never seen), keep it — health is unknown, let dispatch try.
    return ordered.filter((id) => !scores.some((s) => s.compositeId === id) || (healthy.has(id) && !isTripped(id)));
  }

  // Derive from advisory scores by category metric.
  const metric = (s: ModelScore): number | null => {
    switch (virtualModel) {
      case 'auto:code':
        return s.codeScore;
      case 'auto:fast':
      case 'auto:cheap':
        return s.avgGenTps;
      case 'auto':
      default:
        // Overall: prefer eval score, then throughput.
        return s.evalScore ?? (s.avgGenTps != null ? s.avgGenTps / 1000 : null);
    }
  };

  return scores
    .filter((s) => s.healthy && !isTripped(s.compositeId) && metric(s) != null)
    .sort((a, b) => (metric(b) ?? -Infinity) - (metric(a) ?? -Infinity))
    .map((s) => s.compositeId);
}

export interface ResolvedCandidates {
  virtualModel: string;
  candidates: string[];
  policyName: string | null;
}

/**
 * Resolve the ordered candidate list for a virtual model against the live
 * fleet + policies + advisory scores.
 */
export async function resolveCandidates(
  sql: Sql,
  fleet: FleetState,
  modelId: string,
): Promise<ResolvedCandidates> {
  const virtualModel = parseVirtualModel(modelId);

  const policyRows = await sql<(RoutePolicyRow & { name: string })[]>`
    SELECT name, virtual_model, candidates, fallback, enabled
    FROM route_policies
    WHERE virtual_model = ${virtualModel} AND enabled = true
    LIMIT 1
  `;

  const scores = await computeRoutingScores(sql, fleet);

  let policy: { candidates: string[]; fallback: string | null } | null = null;
  let policyName: string | null = null;
  if (policyRows.length > 0) {
    const row = policyRows[0]!;
    policy = { candidates: jsonbStringArray(row.candidates as unknown), fallback: row.fallback };
    policyName = row.name;
  }

  let candidates = orderCandidates(virtualModel, policy, scores);

  // Cold-start fallback (G2): with no curated policy and no eval/traffic history,
  // advisory scores are empty so orderCandidates returns []. Fall back to the
  // live fleet model map so a healthy host still dispatches instead of 503ing.
  if (candidates.length === 0) {
    candidates = fleetModelCandidates(fleet);
  }

  return { virtualModel, candidates, policyName };
}

/**
 * Candidate composite ids from live fleet state: connected hosts' known models,
 * `ready` models first (already loaded => loadable + likely the chat model).
 * Pure over the fleet snapshot. Used only as the cold-start fallback.
 */
export function fleetModelCandidates(fleet: FleetState): string[] {
  const ready: string[] = [];
  const other: string[] = [];
  for (const host of fleet.hosts.values()) {
    if (host.liveness !== 'connected') continue;
    for (const m of host.models.values()) {
      const id = `${host.providerId}/${m.model}`;
      if (m.state === 'ready') ready.push(id);
      else other.push(id);
    }
  }
  return [...ready, ...other];
}

/** Split a composite id 'provider/model' into parts. */
export function splitComposite(compositeId: string): { providerId: string; model: string } | null {
  const slash = compositeId.indexOf('/');
  if (slash <= 0) return null;
  return { providerId: compositeId.slice(0, slash), model: compositeId.slice(slash + 1) };
}
