/**
 * Unified ModelRegistry (#2540).
 *
 * Single source of truth for per-model metadata. Replaces the previous
 * split between:
 *   - `model-capabilities.ts` (canonical hardcoded list; renamed to
 *     `in-tree-data.ts` in #2546 slice E, helpers moved here + into
 *     `model-config-helpers.ts`)
 *   - `model-behavior-profile.ts` (vendor-pattern-matched runtime
 *     behaviour; file deleted in #2540 PR 2)
 *
 * Each `ModelEntry` carries BOTH capability and behaviour fields. The
 * registry's `getEntry()` always returns something — exact match if
 * the modelId is known, derived entry if vendor + family can be
 * inferred, universal default otherwise.
 *
 * Resolution chain (most specific first):
 *
 *   1. modelHints / explicit alias        ← operator override
 *   2. models.dev snapshot                ← seeded externally (PR 4)
 *   3. in-tree authoritative entries      ← measured/validated by us
 *   4. derived from vendor + family       ← pattern-matched fallback
 *   5. universal default                  ← never fails
 *
 * This module ships the type + class + derived-fallback logic. The
 * authoritative entries (PR 1's payload) are populated alongside; the
 * models.dev snapshot loader lands in PR 4.
 *
 * Availability is a SEPARATE concern — see `AvailableModelsCache` in
 * a later PR. The registry tells you "what does this model id mean";
 * `AvailableModelsCache` tells you "is the harness currently able to
 * serve it." Routing decisions consume both.
 *
 * @module config/model-registry
 */

import { resolveModelIdentitySync, type ModelHints, type ModelVendor } from './model-identity.js';
import { buildInTreeEntries } from './in-tree-entries.js';
import { loadManifestOverlay } from './manifest-overlay.js';
import { DEFAULT_ENTRY, deriveEntry } from './model-derivation.js';
import { loadModelsDevSnapshot } from './models-dev-snapshot-loader.js';
import type {
  InputModality,
  OutputModality,
  Pricing,
  QualityScores,
  SpecialFeature,
  ToolCapability,
} from './model-capabilities-types.js';

// ============================================================================
// Per-model behaviour types — were previously split into model-behavior-profile.ts
// (deleted in #2540 PR 2 when AgenticAdapter migrated to ModelRegistry).
// ============================================================================

/**
 * Tool-definition format the model expects in `CompletionRequest.tools`.
 * Each `IModelAdapter` translates from the canonical `ToolDefinition`
 * shape to the provider's native form, so this field is informational
 * for routing/scoring, not request-side.
 */
export type ToolDefinitionFormat = 'openai' | 'anthropic' | 'gemini';

/**
 * Prompt-caching opt-in level. `'ephemeral'` adds Anthropic-style
 * `cache_control` markers; other providers ignore the field.
 */
export type PromptCachingMode = 'none' | 'ephemeral' | 'aggressive';

// ============================================================================
// ModelEntry — the unified shape
// ============================================================================

/**
 * Where this entry came from. Higher-priority sources override lower
 * ones field-by-field; `derived` is always the fallback floor.
 */
export type EntrySource = 'in-tree' | 'models-dev' | 'manifest' | 'derived';

/**
 * One model's full metadata. Combines what was previously split
 * across `ModelCapability` (capability/pricing/quality) and
 * `ModelBehaviorProfile` (runtime behaviour toggles).
 *
 * All capability + pricing + quality fields are optional because
 * derived entries (vendor known but no authoritative data) won't
 * have them. Routing consumers must handle absence gracefully.
 *
 * Behaviour fields always have values (defaulted from vendor/family
 * profile if no exact entry exists).
 */
export interface ModelEntry {
  // ---- Identity ----
  /** Canonical id, e.g. `claude-opus-4-1`, `gpt-5.4`, `meta/llama-3-70b`. */
  readonly id: string;
  /**
   * Alternate strings that should resolve to this entry. Operators
   * extend via the manifest (PR 4) when a gateway exposes a renamed
   * version of a known model.
   */
  readonly aliases?: readonly string[];
  /** Coarse vendor bucket — drives behaviour-profile fallback chains. */
  readonly vendor: ModelVendor;
  /** Family inside a vendor — `claude-opus`, `gpt-4o`, `llama-3`. */
  readonly family: string;
  /** Version string (best-effort; `4-1`, `2024-08-06`, etc). */
  readonly version?: string;
  /** Human-readable display name for UI / logs. */
  readonly displayName?: string;

  // ---- Capabilities (carried from ModelCapability) ----
  readonly contextWindow?: number;
  readonly maxOutputTokens?: number;
  readonly inputModalities?: readonly InputModality[];
  readonly outputModalities?: readonly OutputModality[];
  readonly toolCapabilities?: readonly ToolCapability[];
  readonly specialFeatures?: readonly SpecialFeature[];
  readonly pricing?: Pricing;
  readonly qualityScores?: QualityScores;
  readonly notes?: string;

  // ---- CLI routing metadata (in-tree entries only) ----
  /** Which CLI tool this model belongs to (e.g. 'claude', 'gemini'). */
  readonly cliName?: string;
  /** Short alias the CLI accepts (e.g. 'opus' for claude). */
  readonly cliAlias?: string;
  /** Vendor model id the CLI passes upstream (e.g. 'claude-opus-4-6'). */
  readonly cliModelName?: string;

  // ---- Behaviour (carried from ModelBehaviorProfile) ----
  readonly parallelToolCalls: boolean;
  readonly promptCaching: PromptCachingMode;
  readonly toolDefinitionFormat: ToolDefinitionFormat;
  readonly maxRecommendedTurnBudget: number;
  readonly strictJson: boolean;
  readonly quirks: readonly string[];
  readonly profileId: string;

  // ---- Provenance ----
  readonly source: EntrySource;
  /** ISO date when this entry was last validated against the upstream. */
  readonly verifiedAt?: string;
}

// Derivation (DEFAULT_ENTRY, VENDOR_DEFAULTS, FAMILY_DEFAULTS, deriveEntry)
// lives in `./model-derivation.ts` so `in-tree-entries.ts` can import
// `deriveEntry` without creating a runtime cycle back through this file.
// We re-export below for backward compatibility with existing call sites.

// ============================================================================
// Registry
// ============================================================================

export interface ModelRegistryOptions {
  /** Authoritative in-tree entries. Highest priority. */
  readonly inTreeEntries?: readonly ModelEntry[];
  /** models.dev snapshot entries. Lower priority than in-tree. */
  readonly modelsDevEntries?: readonly ModelEntry[];
  /** Operator manifest entries. Higher priority than in-tree. */
  readonly manifestEntries?: readonly ModelEntry[];
}

/**
 * The unified model-metadata registry. Construct once, share across
 * the process. `getEntry(modelId)` always returns something.
 */
export class ModelRegistry {
  private readonly byId = new Map<string, ModelEntry>();
  private readonly byAlias = new Map<string, string>(); // alias → canonical id

  constructor(options: ModelRegistryOptions = {}) {
    // Load order: lowest priority first; later sources overwrite.
    if (options.modelsDevEntries !== undefined) {
      this.loadEntries(options.modelsDevEntries);
    }
    if (options.inTreeEntries !== undefined) {
      this.loadEntries(options.inTreeEntries);
    }
    if (options.manifestEntries !== undefined) {
      this.loadEntries(options.manifestEntries);
    }
  }

  /**
   * Resolve a model id to its full metadata entry. Always returns —
   * unknown models get a derived entry with sensible defaults.
   */
  getEntry(modelId: string, hints?: ModelHints): ModelEntry {
    const direct = this.lookupExact(modelId);
    if (direct !== undefined && direct.source !== 'models-dev') return direct;

    const identity = resolveModelIdentitySync(modelId, augmentHints(hints, direct));
    const derived = deriveEntry(modelId, identity);
    if (direct !== undefined && identity.vendor !== 'unknown') {
      return mergeSnapshotWithDerived(direct, derived);
    }
    return direct ?? derived;
  }

  /**
   * Has the registry got an authoritative entry for this id?
   * Consumers use this to distinguish "we know X" from "we guessed."
   */
  hasAuthoritative(modelId: string): boolean {
    return this.lookupExact(modelId) !== undefined;
  }

  /** All authoritative entries (in-tree + models.dev + manifest, deduped). */
  allEntries(): readonly ModelEntry[] {
    return [...this.byId.values()];
  }

  /** Snapshot of canonical id → entry mapping. */
  toMap(): ReadonlyMap<string, ModelEntry> {
    return new Map(this.byId);
  }

  private lookupExact(modelId: string): ModelEntry | undefined {
    const direct = this.byId.get(modelId);
    if (direct !== undefined) return direct;
    const canonical = this.byAlias.get(modelId);
    if (canonical !== undefined) return this.byId.get(canonical);
    return undefined;
  }

  private loadEntries(entries: readonly ModelEntry[]): void {
    for (const entry of entries) {
      this.byId.set(entry.id, entry);
      if (entry.aliases !== undefined) {
        for (const alias of entry.aliases) {
          this.byAlias.set(alias, entry.id);
        }
      }
    }
  }
}

// When the modelId alone doesn't carry vendor info (e.g.
// `text-embedding-3-large`), promote the snapshot's vendor/family
// into resolver hints so identity can resolve and derivation can
// supply the correct behaviour fields.
function augmentHints(hints: ModelHints | undefined, direct: ModelEntry | undefined): ModelHints {
  const h = hints ?? {};
  if (direct === undefined) return h;
  const out: { -readonly [K in keyof ModelHints]: ModelHints[K] } = {
    vendor: h.vendor ?? direct.vendor,
    family: h.family ?? direct.family,
  };
  if (h.version !== undefined) out.version = h.version;
  if (h.quirks !== undefined) out.quirks = h.quirks;
  return out;
}

// Snapshot supplies per-version capability data (contextWindow,
// pricing, displayName); derived entries carry richer behaviour
// knowledge (parallelToolCalls, promptCaching, profileId, quirks).
// Merge so each layer wins where it has the better data.
function mergeSnapshotWithDerived(snapshot: ModelEntry, derived: ModelEntry): ModelEntry {
  return {
    ...snapshot,
    parallelToolCalls: derived.parallelToolCalls,
    promptCaching: derived.promptCaching,
    toolDefinitionFormat: derived.toolDefinitionFormat,
    maxRecommendedTurnBudget: derived.maxRecommendedTurnBudget,
    strictJson: derived.strictJson,
    quirks: derived.quirks,
    profileId: derived.profileId,
    source: 'derived',
  };
}

// Re-export the derivation surface so existing consumers keep working
// without needing to update their import paths.
export { DEFAULT_ENTRY, deriveEntry };

// ============================================================================
// Convenience — global default registry, populated lazily
// ============================================================================

let globalRegistry: ModelRegistry | undefined;

/**
 * Lazy global registry. Most consumers should accept a `ModelRegistry`
 * via dependency injection instead, but this is the convenient default
 * for migration from the existing module-level constants.
 *
 * The first call constructs the registry and loads the operator
 * manifest overlay (#2547 4a) from `$NEXUS_MODELS_OVERLAY_PATH` or
 * `$NEXUS_DATA_DIR/models-manifest.yaml`. Missing / malformed manifests
 * never throw — rejections are logged at warn level and dropped.
 */
export function getDefaultRegistry(): ModelRegistry {
  globalRegistry ??= buildDefaultRegistry();
  return globalRegistry;
}

function buildDefaultRegistry(): ModelRegistry {
  const overlay = loadManifestOverlay();
  const snapshot = loadModelsDevSnapshot();
  const inTree = buildInTreeEntries();
  const options: ModelRegistryOptions = { inTreeEntries: inTree };
  if (overlay.status === 'loaded' && overlay.entries.length > 0) {
    (options as { manifestEntries?: readonly ModelEntry[] }).manifestEntries = overlay.entries;
  }
  if (snapshot.status === 'loaded' && snapshot.entries.length > 0) {
    (options as { modelsDevEntries?: readonly ModelEntry[] }).modelsDevEntries = snapshot.entries;
  }
  return new ModelRegistry(options);
}

/** Replace the global registry. Reserved for tests + bootstrap. */
export function setDefaultRegistry(registry: ModelRegistry | undefined): void {
  globalRegistry = registry;
}
