/**
 * Per-run model selection & role-based routing.
 *
 * ADR-0017 consequence: role-based model routing (different run kinds default
 * to different models) is a **product/admin configuration** concern, not a spec
 * invariant. This registry is the seam for that config; the concrete policy
 * (which models are admin-enabled, which role maps to which default) is `OPEN`
 * here — decided by admin settings + ADR, not hard-coded.
 */

/**
 * A named role preset — the full per-run agent bundle. Roles are **data**, not
 * a code enum: admin/teachers define them (ADR-0017: role-based routing is
 * product config, not a spec invariant). `roleId` is an opaque string and
 * is selected through the project console — the registry holds the role set,
 * so new roles are added by configuration, not by editing code.
 *
 * A role bundles everything that distinguishes one agent persona from another:
 * model, system prompt, and the tool surface (files / cph / feishu / skills /
 * mcps). Per-run tool whitelisting is enforced by the Claude SDK runner and
 * the cph_hub MCP server builder; security does not rely on the system prompt.
 */
import { assertSupportedRoleTools } from "./roleTools.js";

export interface RoleEntry {
  readonly id: string;
  /** Human label for the teacher-side switcher UX. */
  readonly label: string;
  /** Default model id for runs under this role, if a routing rule is set. */
  readonly defaultModel: string | undefined;
  /**
   * System prompt seeding the agent's persona/instructions. Prepended to the
   * run's messages only at session start; Claude SDK resume restores later
   * turns from the provider session. `undefined` ⇒ no system prompt (bare run).
   */
  readonly systemPrompt?: string | undefined;
  /**
   * Tool names this role may use (whitelist). `undefined` ⇒ the full registered
   * set (back-compat / unrestricted roles). An empty array ⇒ no tools at all.
   * Invalid names fail fast when settings are loaded or the run is set up.
   */
  readonly tools?: readonly string[] | undefined;
  /** Immutable skill versions selected by this role at runtime. */
  readonly skills?: readonly RoleSkillEntry[] | undefined;
}

export interface RoleSkillEntry {
  readonly name: string;
  readonly version: string;
  readonly contentDigest: string;
}

/** A model the admin has enabled for use by the Hub. */
export interface ModelEntry {
  readonly id: string;
  /** Human label for the teacher-side switcher UX. */
  readonly label: string;
  /** True when the model is known to support tool use reliably. */
  readonly toolCapable: boolean;
}

export interface ModelRegistry {
  /** All models admin-enabled for this Hub instance. */
  listModels(): readonly ModelEntry[];
  /** All role presets currently configured (admin/teacher-defined). */
  listRoles(): readonly RoleEntry[];
  /** The role preset with the given id, if any. */
  role(id: string): RoleEntry | undefined;
  /** Resolve a model id: explicit request → role default → first enabled. */
  resolve(requestedModel: string | undefined, roleId: string): string;
}

/**
 * A simple in-memory registry. Real wiring reads from admin settings / DB; this
 * is the skeleton seam. Both the model list and the role set are pluggable data
 * — adding a role is a config change, not a code change.
 */
export class InMemoryModelRegistry implements ModelRegistry {
  private readonly models: readonly ModelEntry[];
  private readonly roles: Map<string, RoleEntry>;

  constructor(models: readonly ModelEntry[], roles: readonly RoleEntry[] = []) {
    this.models = models;
    for (const role of roles) {
      if (role.tools !== undefined) assertSupportedRoleTools(role.tools);
    }
    this.roles = new Map(roles.map((r) => [r.id, r]));
  }

  listModels(): readonly ModelEntry[] {
    return this.models;
  }

  listRoles(): readonly RoleEntry[] {
    return [...this.roles.values()];
  }

  role(id: string): RoleEntry | undefined {
    return this.roles.get(id);
  }

  /**
   * Resolve a model id for a run. Priority: teacher's explicit request → the
   * role preset's default → the first admin-enabled model. `roleId` is a string
   * so unknown roles degrade gracefully to the fallback rather than throwing.
   */
  resolve(requestedModel: string | undefined, roleId: string): string {
    if (requestedModel !== undefined && this.models.some((m) => m.id === requestedModel)) {
      return requestedModel;
    }
    const role = this.roles.get(roleId);
    if (role?.defaultModel !== undefined) return role.defaultModel;
    const first = this.models[0];
    if (first === undefined) throw new Error("no models enabled for this Hub");
    return first.id;
  }
}
