/**
 * Multi-role provider registry (Part N Advanced Mode).
 * Solo (advancedMode off) = single operator — identical to today's path.
 * Persists to data/providers.json (gitignored).
 *
 * Valid topologies: 1 op; 1 op + 1 sup; 2 ops; 2 ops + 1 sup.
 * Dual-operator work-stealing is algorithm-only (workQueue) — not a role.
 */
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { isPrivateHost } from './netHost';

export type ProviderType = 'claude' | 'openai-compat' | 'mock';
export type ProviderRole = 'operator' | 'supervisor' | 'pool';

export interface RoleProvider {
  type: ProviderType;
  baseURL?: string;
  model?: string;
  apiKey?: string;
  label?: string;
  verifiedAt?: string;
  connected: boolean;
  kind: 'mock' | 'local' | 'cloud';
}

/** Combinable Supervisor behavior toggles (N4). */
export interface SupervisorModes {
  /** Supervisor runs encode/IR front-end, freezes, hands build+repair to Operator. */
  resolveIr: boolean;
  /** Supervisor steps in on any failed local recheck (⊇ escalations). */
  resolveAllFailures: boolean;
  /** Supervisor engages only on Operator stall/ceiling. */
  resolveEscalationsOnly: boolean;
}

export interface ProvidersFile {
  advancedMode: boolean;
  /** 1–2 operators; index 0 is the primary / solo agent. */
  operators: RoleProvider[];
  supervisor: RoleProvider | null;
  supervisorModes: SupervisorModes;
  /** @deprecated kept for migration; prefer advancedMode + supervisorModes */
  supervisorEnabled?: boolean;
  /** @deprecated pool folded into operators[1] / unused */
  pool?: RoleProvider[];
}

export type TopologyKind = 'solo' | 'failsafe' | 'parallel' | 'full' | 'invalid';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
const FILE = path.join(ROOT, 'data', 'providers.json');

const DEFAULT_MODES: SupervisorModes = {
  resolveIr: true,
  resolveAllFailures: false,
  resolveEscalationsOnly: true,
};

const EMPTY: ProvidersFile = {
  advancedMode: false,
  operators: [],
  supervisor: null,
  supervisorModes: { ...DEFAULT_MODES },
};

let mem: ProvidersFile = load();

function migrate(raw: Record<string, unknown>): ProvidersFile {
  // Ignore legacy orchestrator role — algorithm engages with 2 ops automatically.
  if (raw.orchestrator) {
    // eslint-disable-next-line no-console
    console.warn('[providers] ignoring stored orchestrator role (removed — use 2 operators)');
  }
  if (Array.isArray(raw.operators) || typeof raw.advancedMode === 'boolean') {
    const operators = Array.isArray(raw.operators)
      ? (raw.operators as RoleProvider[]).filter(Boolean).slice(0, 2)
      : [];
    if (!operators.length && raw.operator) operators.push(raw.operator as RoleProvider);
    return {
      advancedMode: Boolean(raw.advancedMode),
      operators,
      supervisor: (raw.supervisor as RoleProvider) ?? null,
      supervisorModes: {
        ...DEFAULT_MODES,
        ...((raw.supervisorModes as SupervisorModes) ?? {}),
        ...(raw.supervisorEnabled && !(raw.supervisorModes as SupervisorModes)?.resolveIr
          ? { resolveIr: true, resolveEscalationsOnly: true }
          : {}),
      },
    };
  }
  const operators: RoleProvider[] = [];
  if (raw.operator) operators.push(raw.operator as RoleProvider);
  const pool = Array.isArray(raw.pool) ? (raw.pool as RoleProvider[]) : [];
  if (pool[0] && operators.length < 2) operators.push(pool[0]);
  return {
    advancedMode: Boolean(raw.supervisorEnabled) || operators.length > 1,
    operators,
    supervisor: (raw.supervisor as RoleProvider) ?? null,
    supervisorModes: {
      ...DEFAULT_MODES,
      resolveIr: Boolean(raw.supervisorEnabled),
      resolveEscalationsOnly: Boolean(raw.supervisorEnabled),
    },
  };
}

/**
 * Stored `kind` is a snapshot from assignment time and may predate the
 * private-host classifier — never trust it. Recompute from baseURL on load
 * so a stale "cloud" label on a LAN endpoint (e.g. 192.168.x) self-heals.
 */
function normalizeKinds(p: ProvidersFile): ProvidersFile {
  const fix = (r: RoleProvider | null): RoleProvider | null => {
    if (!r) return r;
    if (r.type === 'openai-compat' && r.baseURL) {
      // Follow-the-loaded-model: an unset model is the DEFAULT (the user's LM
      // Studio load state decides, per job). Only an explicit vault override
      // pins. Never back-fill a pin from stale labels.
      return { ...r, kind: isPrivateHost(r.baseURL) ? 'local' : 'cloud' };
    }
    return r;
  };
  return {
    ...p,
    operators: p.operators.map(o => fix(o)!),
    supervisor: fix(p.supervisor),
  };
}

function load(): ProvidersFile {
  try {
    if (!fs.existsSync(FILE)) return { ...EMPTY, operators: [], supervisorModes: { ...DEFAULT_MODES } };
    const raw = JSON.parse(fs.readFileSync(FILE, 'utf8')) as Record<string, unknown>;
    return normalizeKinds(migrate(raw));
  } catch {
    return { ...EMPTY, operators: [], supervisorModes: { ...DEFAULT_MODES } };
  }
}

function persist(): void {
  try {
    fs.mkdirSync(path.dirname(FILE), { recursive: true });
    fs.writeFileSync(FILE, JSON.stringify(mem, null, 2));
  } catch { /* best-effort */ }
}

export function getProviders(): ProvidersFile {
  return {
    advancedMode: mem.advancedMode,
    operators: mem.operators.map(p => ({ ...p })),
    supervisor: mem.supervisor ? { ...mem.supervisor } : null,
    supervisorModes: { ...mem.supervisorModes },
  };
}

function strip(p: RoleProvider | null): Omit<RoleProvider, 'apiKey'> | null {
  if (!p) return null;
  const { apiKey: _, ...rest } = p;
  return rest;
}

export function getProvidersPublic() {
  const p = getProviders();
  return {
    advancedMode: p.advancedMode,
    operators: p.operators.map(o => strip(o)!),
    supervisor: strip(p.supervisor),
    supervisorModes: p.supervisorModes,
    operator: strip(p.operators[0] ?? null),
    pool: [] as Omit<RoleProvider, 'apiKey'>[],
    supervisorEnabled: p.advancedMode && !!p.supervisor?.connected
      && (p.supervisorModes.resolveIr || p.supervisorModes.resolveEscalationsOnly || p.supervisorModes.resolveAllFailures),
  };
}

/** Classify stack; four valid topologies — no orchestrator role. */
export function classifyTopology(p: ProvidersFile = mem): { kind: TopologyKind; error?: string } {
  if (!p.advancedMode) return { kind: 'solo' };
  const ops = p.operators.filter(o => o?.connected).length;
  const sup = !!(p.supervisor?.connected);
  if (ops === 0) return { kind: 'invalid', error: 'at least one Operator is required' };
  if (ops > 2) return { kind: 'invalid', error: 'Operators capped at 2' };
  if (ops === 1 && !sup) return { kind: 'solo' };
  if (ops === 1 && sup) return { kind: 'failsafe' };
  if (ops === 2 && !sup) return { kind: 'parallel' };
  if (ops === 2 && sup) return { kind: 'full' };
  return { kind: 'invalid', error: 'invalid agent stack' };
}

export function setAdvancedMode(on: boolean): void {
  mem.advancedMode = on;
  if (!on) {
    mem.operators = mem.operators.slice(0, 1);
    mem.supervisor = null;
    mem.supervisorModes = { ...DEFAULT_MODES };
  }
  persist();
}

export function setSupervisorModes(modes: Partial<SupervisorModes>): void {
  mem.supervisorModes = { ...mem.supervisorModes, ...modes };
  if (mem.supervisorModes.resolveAllFailures) {
    mem.supervisorModes.resolveEscalationsOnly = true;
  }
  persist();
}

export function setSupervisorEnabled(on: boolean): void {
  if (on) {
    mem.advancedMode = true;
    mem.supervisorModes = { ...mem.supervisorModes, resolveIr: true, resolveEscalationsOnly: true };
  } else {
    mem.supervisorModes = { ...DEFAULT_MODES, resolveIr: false, resolveEscalationsOnly: false, resolveAllFailures: false };
  }
  persist();
}

export function setRoleProvider(
  role: ProviderRole | 'orchestrator',
  provider: RoleProvider | null,
  index = 0,
): void {
  if (role === 'orchestrator') {
    // eslint-disable-next-line no-console
    console.warn('[providers] orchestrator role ignored — work-stealing engages with 2 operators');
    return;
  }
  if (role === 'operator' || role === 'pool') {
    const i = role === 'pool' ? Math.max(1, index) : Math.max(0, Math.min(1, index));
    while (mem.operators.length <= i) mem.operators.push(null as unknown as RoleProvider);
    if (provider === null) {
      mem.operators.splice(i, 1);
    } else {
      mem.operators[i] = provider;
    }
    mem.operators = mem.operators.filter(Boolean).slice(0, 2);
  } else if (role === 'supervisor') {
    mem.supervisor = provider;
  }
  persist();
}

export function clearAllProviders(): void {
  mem = { ...EMPTY, operators: [], supervisorModes: { ...DEFAULT_MODES } };
  persist();
}

export function resetToSolo(): void {
  const op0 = mem.operators[0] ?? null;
  mem = {
    advancedMode: false,
    operators: op0 ? [op0] : [],
    supervisor: null,
    supervisorModes: { ...DEFAULT_MODES },
  };
  persist();
}

export function applyProviderEnv(p: RoleProvider | null): void {
  delete process.env.MOCK_LLM;
  delete process.env.LOCAL_LLM;
  delete process.env.LOCAL_LLM_URL;
  delete process.env.LOCAL_LLM_MODEL;
  delete process.env.LLM_API_KEY;
  delete process.env.RHA_MODEL;
  if (!p || p.type !== 'claude') delete process.env.ANTHROPIC_API_KEY;

  if (!p || !p.connected) return;
  switch (p.type) {
    case 'mock':
      process.env.MOCK_LLM = '1';
      break;
    case 'openai-compat':
      process.env.LOCAL_LLM = '1';
      if (p.baseURL) process.env.LOCAL_LLM_URL = p.baseURL;
      if (p.model) process.env.LOCAL_LLM_MODEL = p.model;
      if (p.apiKey) process.env.LLM_API_KEY = p.apiKey;
      break;
    case 'claude':
      if (p.apiKey) process.env.ANTHROPIC_API_KEY = p.apiKey;
      if (p.model) process.env.RHA_MODEL = p.model;
      break;
  }
}

/** Primary operator for env / badge (operators[0]). */
export function primaryOperator(): RoleProvider | null {
  return mem.operators[0] ?? null;
}
