/**
 * Effective runtime knobs: data/runtime-config.json > env > built-in defaults.
 * Part S1: per-role overrides under `roles` — getConfig(role) merges role over global.
 * Read at call time so admin changes apply to the next job without a restart.
 */
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';

/** Slots that can own independent knobs (S1). */
export type ConfigRole = 'operator' | 'operator1' | 'supervisor';

export interface RuntimeConfig {
  model: string;
  effort: Effort;
  concurrency: number;
  swarmWidth: number;
  maxPasses: number;
  maxEpisodes: number;
  maxAttempts: number;
  stallLimit: number;
  progressStall: number;
  gateRounds: number;
  autopilotDefault: boolean;
  /** Scales the repair-swarm temperature ladder [0.5, 0.8, 1.0]; not used for encode/IR/build. */
  swarmTempCeiling: number;
  /** Raise SEV1 when IR escalations ≥ this fraction of features (1 = effectively off). */
  sev1IrFraction: number;
  /** Part V1: absolute path to sync deployable artifact into (empty = off). */
  outputDir: string;
  /**
   * Sync mode for outputDir. 'full' (default) = the portable project incl.
   * `.supergit` + git history (re-attachable by any RHA session);
   * 'deployable' = production push, code + frozen tests only.
   */
  syncMode: 'full' | 'deployable';
}

/** Knobs that may differ per role (S1). */
export type RoleKnobKey =
  | 'effort'
  | 'swarmWidth'
  | 'swarmTempCeiling'
  | 'maxPasses'
  | 'maxEpisodes'
  | 'maxAttempts'
  | 'stallLimit'
  | 'gateRounds';

export type RoleKnobs = Partial<Pick<RuntimeConfig, RoleKnobKey>>;

export type RuntimeConfigKey = keyof RuntimeConfig;

const EFFORTS: Effort[] = ['low', 'medium', 'high', 'xhigh', 'max'];
export const CONFIG_ROLES: ConfigRole[] = ['operator', 'operator1', 'supervisor'];
export const ROLE_KNOB_KEYS: RoleKnobKey[] = [
  'effort', 'swarmWidth', 'swarmTempCeiling', 'maxPasses',
  'maxEpisodes', 'maxAttempts', 'stallLimit', 'gateRounds',
];

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
const CONFIG_PATH = path.join(ROOT, 'data', 'runtime-config.json');

const RANGES: Record<Exclude<RuntimeConfigKey, 'model' | 'effort' | 'autopilotDefault' | 'outputDir' | 'syncMode'>, { min: number; max: number }> = {
  concurrency: { min: 1, max: 16 },
  swarmWidth: { min: 1, max: 8 },
  maxPasses: { min: 1, max: 100 },
  maxEpisodes: { min: 1, max: 200 },
  maxAttempts: { min: 1, max: 100 },
  stallLimit: { min: 1, max: 10 },
  progressStall: { min: 1, max: 20 },
  gateRounds: { min: 1, max: 10 },
  swarmTempCeiling: { min: 0.2, max: 1.0 },
  sev1IrFraction: { min: 0.1, max: 1.0 },
};

interface ConfigFile {
  global?: Partial<RuntimeConfig>;
  roles?: Partial<Record<ConfigRole, RoleKnobs>>;
  /** Flat legacy keys live at top level and fold into global. */
  [k: string]: unknown;
}

function envNum(env: NodeJS.ProcessEnv, name: string, fallback: number): number {
  const n = Number(env[name]);
  return Number.isFinite(n) ? n : fallback;
}

function defaultsFromEnv(env: NodeJS.ProcessEnv = process.env): RuntimeConfig {
  const effortRaw = (env.RHA_EFFORT || 'xhigh') as Effort;
  return {
    model: env.RHA_MODEL || 'claude-opus-4-8',
    effort: EFFORTS.includes(effortRaw) ? effortRaw : 'xhigh',
    concurrency: envNum(env, 'RHA_CONCURRENCY', 4),
    swarmWidth: envNum(env, 'RHA_SWARM_WIDTH', 3),
    maxPasses: envNum(env, 'RHA_BOUNDED_MAX_PASSES', 20),
    maxEpisodes: envNum(env, 'RHA_MAX_EPISODES', 50),
    maxAttempts: envNum(env, 'RHA_MAX_ATTEMPTS', 25),
    stallLimit: envNum(env, 'RHA_STALL_LIMIT', 2),
    progressStall: envNum(env, 'RHA_PROGRESS_STALL', 4),
    gateRounds: envNum(env, 'RHA_GATE_ROUNDS', 3),
    autopilotDefault: true,
    swarmTempCeiling: 1.0,
    sev1IrFraction: 0.5,
    outputDir: '',
    syncMode: 'full',
  };
}

/**
 * Canonical algorithm bounds for the published corpus: pure defaults with no
 * env reads and no local data/ overrides, plus the validation ranges.
 */
export function corpusBaseline(): { defaults: RuntimeConfig; ranges: typeof RANGES; roleKnobKeys: RoleKnobKey[] } {
  return { defaults: defaultsFromEnv({}), ranges: RANGES, roleKnobKeys: ROLE_KNOB_KEYS };
}

function readFile(): ConfigFile {
  try {
    if (!fs.existsSync(CONFIG_PATH)) return {};
    return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as ConfigFile;
  } catch {
    return {};
  }
}

function splitFile(raw: ConfigFile): { global: Partial<RuntimeConfig>; roles: Partial<Record<ConfigRole, RoleKnobs>> } {
  const roles = { ...(raw.roles ?? {}) };
  const global: Partial<RuntimeConfig> = { ...(raw.global ?? {}) };
  // Legacy flat keys → global
  for (const [k, v] of Object.entries(raw)) {
    if (k === 'global' || k === 'roles') continue;
    (global as Record<string, unknown>)[k] = v;
  }
  return { global, roles };
}

function readOverrides(): Partial<RuntimeConfig> {
  return splitFile(readFile()).global;
}

function readRoleMap(): Partial<Record<ConfigRole, RoleKnobs>> {
  return splitFile(readFile()).roles;
}

/** Clamp / coerce a partial patch; throws on invalid values. */
export function validateConfigPatch(patch: Record<string, unknown>): Partial<RuntimeConfig> {
  const out: Partial<RuntimeConfig> = {};
  for (const [k, v] of Object.entries(patch)) {
    if (v === undefined) continue;
    switch (k as RuntimeConfigKey) {
      case 'model': {
        if (typeof v !== 'string' || !v.trim()) throw new Error('model must be a non-empty string');
        out.model = v.trim();
        break;
      }
      case 'effort': {
        if (typeof v !== 'string' || !EFFORTS.includes(v as Effort)) {
          throw new Error(`effort must be one of ${EFFORTS.join('|')}`);
        }
        out.effort = v as Effort;
        break;
      }
      case 'autopilotDefault': {
        if (typeof v !== 'boolean') throw new Error('autopilotDefault must be boolean');
        out.autopilotDefault = v;
        break;
      }
      case 'syncMode': {
        if (v !== 'full' && v !== 'deployable') throw new Error("syncMode must be 'full' or 'deployable'");
        out.syncMode = v;
        break;
      }
      // Legacy key from older runtime-config.json files — map onto syncMode.
      case 'syncIncludeLedger' as never: {
        out.syncMode = v ? 'full' : 'deployable';
        break;
      }
      case 'outputDir': {
        if (typeof v !== 'string') throw new Error('outputDir must be a string');
        out.outputDir = v.trim();
        break;
      }
      case 'concurrency':
      case 'swarmWidth':
      case 'maxPasses':
      case 'maxEpisodes':
      case 'maxAttempts':
      case 'stallLimit':
      case 'progressStall':
      case 'gateRounds':
      case 'swarmTempCeiling':
      case 'sev1IrFraction': {
        const n = typeof v === 'number' ? v : Number(v);
        if (!Number.isFinite(n)) throw new Error(`${k} must be a number`);
        const { min, max } = RANGES[k as keyof typeof RANGES];
        if (n < min || n > max) throw new Error(`${k} must be in [${min}, ${max}]`);
        (out as Record<string, number>)[k] = k === 'swarmTempCeiling' || k === 'sev1IrFraction' ? n : Math.round(n);
        break;
      }
      default:
        throw new Error(`unknown config key: ${k}`);
    }
  }
  return out;
}

export function validateRoleKnobs(patch: Record<string, unknown>): RoleKnobs {
  const filtered: Record<string, unknown> = {};
  for (const k of ROLE_KNOB_KEYS) {
    if (patch[k] !== undefined) filtered[k] = patch[k];
  }
  return validateConfigPatch(filtered) as RoleKnobs;
}

export function getConfig(role?: ConfigRole): RuntimeConfig {
  const base = { ...defaultsFromEnv(), ...readOverrides() };
  if (!role) return base;
  const roleOv = readRoleMap()[role] ?? {};
  return { ...base, ...roleOv };
}

export function getConfigStatus(): {
  effective: RuntimeConfig;
  overrides: Partial<RuntimeConfig>;
  defaults: RuntimeConfig;
  roles: Partial<Record<ConfigRole, RoleKnobs>>;
} {
  const defaults = defaultsFromEnv();
  const overrides = readOverrides();
  const roles = readRoleMap();
  return { effective: { ...defaults, ...overrides }, overrides, defaults, roles };
}

export function writeConfigOverrides(patch: Partial<RuntimeConfig>): RuntimeConfig {
  const validated = validateConfigPatch(patch as Record<string, unknown>);
  const { global, roles } = splitFile(readFile());
  const nextGlobal = { ...global, ...validated };
  fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
  fs.writeFileSync(CONFIG_PATH, JSON.stringify({ global: nextGlobal, roles }, null, 2) + '\n');
  return getConfig();
}

export function writeRoleKnobs(role: ConfigRole, patch: RoleKnobs): RuntimeConfig {
  if (!CONFIG_ROLES.includes(role)) throw new Error(`unknown role: ${role}`);
  const validated = validateRoleKnobs(patch as Record<string, unknown>);
  const { global, roles } = splitFile(readFile());
  roles[role] = { ...(roles[role] ?? {}), ...validated };
  fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
  fs.writeFileSync(CONFIG_PATH, JSON.stringify({ global, roles }, null, 2) + '\n');
  return getConfig(role);
}

export function resetConfigOverrides(): RuntimeConfig {
  try { fs.rmSync(CONFIG_PATH, { force: true }); } catch { /* ignore */ }
  return getConfig();
}

/** Swarm temperature ladder scaled so the top rung equals swarmTempCeiling. */
export function swarmTemperatures(role?: ConfigRole): number[] {
  const ceiling = Math.min(1, Math.max(0.2, getConfig(role).swarmTempCeiling));
  const base = [0.5, 0.8, 1.0];
  return base.map(t => Math.min(1, Math.round((t * ceiling) * 1000) / 1000));
}

/**
 * Decorrelation ladder for bounded retries: rung 0 is the provider default
 * (`undefined`), then the swarm temperatures. Walk with
 * `temperatureLadder()[(pass - 1) % ladder.length]`.
 */
export function temperatureLadder(role?: ConfigRole): (number | undefined)[] {
  return [undefined, ...swarmTemperatures(role)];
}
