// @ts-check
// Balance vector computation. Deterministic, no LLM.
// Mirrors the per-axis formulas in
// docs/internal/pro-regional-intelligence-appendix-scoring.md.

import { clip, num, weightedAverage, percentile } from './_helpers.mjs';
import { sanitizeEvidenceString } from './_sanitize.mjs';
import { CII_RISK_SCORE_CACHE_KEYS } from '../_cii-risk-cache-keys.mjs';
// Use scripts/shared mirror (not repo-root shared/): Railway service has
// rootDirectory=scripts so ../../shared/ escapes the deploy root. See #2954.
import {
  getRegionCountries,
  getRegionCorridors,
  countryCriticality,
  REGIONS,
  isSignalInRegion,
} from '../shared/geography.js';
import iso3ToIso2Raw from '../shared/iso3-to-iso2.json' with { type: 'json' };

/** @type {Record<string, string>} */
const ISO3_TO_ISO2 = iso3ToIso2Raw;

// 1.1.0: balance vector now consumes the cross-source-signals, forecasts,
// national-debt, and transit-summaries inputs that were silently dropped while
// they were stored as { _seed, data } envelopes but read flat (coercive_pressure
// scored 0 for every region → flat 'calm'). Fixed at the loader via
// unwrapEnvelope; bumped so the scoring_version in snapshot meta makes the
// resulting score shift traceable.
const SCORING_VERSION = '1.1.0';

export { SCORING_VERSION };

/**
 * @param {string} regionId
 * @param {Record<string, any>} sources - keyed by Redis key, see freshness.mjs
 * @returns {{ vector: import('../../shared/regions.types.js').BalanceVector }}
 */
export function computeBalanceVector(regionId, sources) {
  const region = REGIONS.find((r) => r.id === regionId);
  if (!region) throw new Error(`Unknown region: ${regionId}`);
  const countries = new Set(getRegionCountries(regionId));
  const corridors = getRegionCorridors(regionId);

  const pressuresOut = [];
  const buffersOut = [];

  // ── Pressures ────────────────────────────────────────────────────────────
  const coercive = computeCoercivePressure(region, sources, pressuresOut);
  const fragility = computeDomesticFragility(countries, sources, pressuresOut);
  const capital = computeCapitalStress(countries, sources, pressuresOut);
  const energyVuln = computeEnergyVulnerability(countries, sources, pressuresOut);

  // ── Buffers ──────────────────────────────────────────────────────────────
  const alliance = computeAllianceCohesion(regionId, sources, buffersOut);
  const maritime = computeMaritimeAccess(corridors, sources, buffersOut);
  const energyLev = computeEnergyLeverage(countries, buffersOut);

  const pressureMean = (coercive + fragility + capital + energyVuln) / 4;
  const bufferMean = (alliance + maritime + energyLev) / 3;
  const netBalance = bufferMean - pressureMean;

  return {
    vector: {
      coercive_pressure: round(coercive),
      domestic_fragility: round(fragility),
      capital_stress: round(capital),
      energy_vulnerability: round(energyVuln),
      alliance_cohesion: round(alliance),
      maritime_access: round(maritime),
      energy_leverage: round(energyLev),
      net_balance: round(netBalance),
      pressures: pressuresOut,
      buffers: buffersOut,
    },
  };
}

// ────────────────────────────────────────────────────────────────────────────
// Per-axis computations
// ────────────────────────────────────────────────────────────────────────────

function computeCoercivePressure(region, sources, drivers) {
  // Cross-source signals scoped by theater label. Matching handles both
  // fine-grained theater IDs (levant, persian-gulf) and broad display labels
  // the seed emits (Middle East, Sub-Saharan Africa) — see isSignalInRegion.
  const xss = sources['intelligence:cross-source-signals:v1'];
  const signals = Array.isArray(xss?.signals) ? xss.signals : [];
  const inRegion = signals.filter((s) => isSignalInRegion(s?.theater, region));
  const criticalCount = inRegion.filter((s) => /CRITICAL/i.test(String(s?.severity ?? ''))).length;
  const highCount = inRegion.filter((s) => /HIGH/i.test(String(s?.severity ?? ''))).length;

  // ACLED conflict counts (region-scoped via country bbox - approximate via signal theater)
  // For Phase 0 we approximate via cross-source signal counts since ACLED key not directly fetched.
  const cSignal = clip(criticalCount * 0.4 + highCount * 0.15, 0, 1);

  // Forecast 'rising' count for military/conflict in region
  const fc = sources['forecast:predictions:v2'];
  const forecasts = Array.isArray(fc?.predictions) ? fc.predictions : [];
  const risingMilitary = forecasts.filter((f) => {
    const trend = String(f?.trend ?? '').toLowerCase();
    const domain = String(f?.domain ?? '').toLowerCase();
    const fRegion = String(f?.region ?? '').toLowerCase();
    return (trend === 'rising' || trend === 'escalating') &&
           (domain === 'military' || domain === 'conflict') &&
           fRegion.includes(region.forecastLabel.toLowerCase());
  }).length;
  const cForecast = clip(risingMilitary / 5, 0, 1);

  // Vessel surge and conflict event surrogates default to mid-low when no data
  const cVessel = 0;
  const cConflict = clip(inRegion.length / 50, 0, 1);

  const score = 0.30 * cVessel + 0.30 * cSignal + 0.25 * cConflict + 0.15 * cForecast;

  if (cSignal > 0.05) {
    drivers.push({
      axis: 'coercive_pressure',
      description: `${criticalCount} critical, ${highCount} high cross-source signals in region`,
      magnitude: round(cSignal),
      evidence_ids: inRegion.slice(0, 5).map((s) => sanitizeEvidenceString(s?.id ?? `xss:${s?.type ?? 'unknown'}`)),
      orientation: 'pressure',
    });
  }
  if (cForecast > 0) {
    drivers.push({
      axis: 'coercive_pressure',
      description: `${risingMilitary} rising military/conflict forecasts in region`,
      magnitude: round(cForecast),
      evidence_ids: [],
      orientation: 'pressure',
    });
  }

  return clip(score, 0, 1);
}

function computeDomesticFragility(countries, sources, drivers) {
  const cii = sources[CII_RISK_SCORE_CACHE_KEYS.stale];
  const ciiScores = Array.isArray(cii?.ciiScores) ? cii.ciiScores : [];
  const inRegion = ciiScores.filter((s) => countries.has(String(s?.region ?? '')));
  if (!inRegion.length) return 0;

  // Per-country normalized score
  const normPerCountry = inRegion.map((s) => ({
    iso: String(s.region),
    norm: clip(num(s.combinedScore) / 100, 0, 1),
  }));

  // Weighted base average
  const baseAvg = weightedAverage(
    normPerCountry,
    (item) => item.norm,
    (item) => countryCriticality(item.iso),
  );
  // Tail amplification
  const values = normPerCountry.map((c) => c.norm);
  const tailP90 = percentile(values, 90);
  const tailMax = Math.max(...values);

  const score = 0.4 * baseAvg + 0.4 * tailP90 + 0.2 * tailMax;

  // Top driver: the country with the highest weighted contribution
  const top = normPerCountry
    .map((c) => ({ ...c, contribution: c.norm * countryCriticality(c.iso) }))
    .sort((a, b) => b.contribution - a.contribution)[0];
  if (top && top.norm > 0.3) {
    const safeIso = sanitizeEvidenceString(top.iso);
    drivers.push({
      axis: 'domestic_fragility',
      description: `${safeIso} CII ${(top.norm * 100).toFixed(0)} (criticality ${countryCriticality(top.iso).toFixed(1)})`,
      magnitude: round(top.contribution),
      evidence_ids: [`cii:${safeIso}`],
      orientation: 'pressure',
    });
  }

  return clip(score, 0, 1);
}

function computeCapitalStress(countries, sources, drivers) {
  // economic:macro-signals:v1 — seed-economy.mjs emits verdict: 'BUY' | 'CASH' | 'UNKNOWN'
  //   BUY  = bullish signals dominate (low stress)
  //   CASH = bearish, rotate to cash (high stress)
  //   UNKNOWN = missing/stale (treat as neutral)
  const macro = sources['economic:macro-signals:v1'];
  const verdict = String(macro?.verdict ?? '').toUpperCase();
  const cMacro = verdict === 'CASH' ? 1 : verdict === 'BUY' ? 0 : 0.5;

  // economic:national-debt:v1 shape: { entries: [{ iso3, debtToGdp, ... }] }
  // debtToGdp is a PERCENTAGE (e.g., 110 for 110% of GDP), not a 0-1 fraction.
  // Filter to region via iso3 -> iso2 lookup, then compute average debt percentage.
  const debt = sources['economic:national-debt:v1'];
  const debtEntries = Array.isArray(debt?.entries) ? debt.entries : [];
  const inRegionDebt = debtEntries.filter((e) => {
    const iso2 = ISO3_TO_ISO2[String(e?.iso3 ?? '')];
    return iso2 && countries.has(iso2);
  });
  // Neutral baseline: 60%. Saturate at 140%+ (80 pct points above neutral).
  const avgDebtPct = inRegionDebt.length
    ? inRegionDebt.reduce((sum, e) => sum + num(e.debtToGdp), 0) / inRegionDebt.length
    : 60;
  const cDebt = clip((avgDebtPct - 60) / 80, 0, 1);

  // economic:stress-index:v1 shape: { compositeScore, label, components, ... }
  // Single global object (US-based FRED composite on 0-100 scale), NOT per-country.
  // Apply as a global overlay that scales every region's capital_stress equally.
  const stress = sources['economic:stress-index:v1'];
  const stressComposite = num(stress?.compositeScore);
  const cStress = clip(stressComposite / 100, 0, 1);

  // Sanctions count proxy: not in default sources, leave at 0 for Phase 0.
  const cSanctions = 0;

  const score = 0.25 * cMacro + 0.20 * cDebt + 0.30 * cStress + 0.25 * cSanctions;

  if (cMacro > 0.6) {
    drivers.push({
      axis: 'capital_stress',
      description: `Macro signals verdict: ${sanitizeEvidenceString(verdict)}`,
      magnitude: round(cMacro),
      evidence_ids: ['macro:verdict'],
      orientation: 'pressure',
    });
  }
  if (cDebt > 0.4) {
    drivers.push({
      axis: 'capital_stress',
      description: `Regional debt/GDP average: ${avgDebtPct.toFixed(0)}% across ${inRegionDebt.length} countries`,
      magnitude: round(cDebt),
      evidence_ids: ['debt:region-avg'],
      orientation: 'pressure',
    });
  }
  if (cStress > 0.5) {
    drivers.push({
      axis: 'capital_stress',
      description: `Global stress index: ${stressComposite.toFixed(0)} (${sanitizeEvidenceString(stress?.label ?? 'n/a')})`,
      magnitude: round(cStress),
      evidence_ids: ['stress:composite'],
      orientation: 'pressure',
    });
  }

  return clip(score, 0, 1);
}

function computeEnergyVulnerability(countries, sources, drivers) {
  // energy:mix:v1:_all shape: Record<ISO2, {
  //   year, coalShare, gasShare, oilShare, nuclearShare, renewShare,
  //   windShare, solarShare, hydroShare, importShare: number|null
  // }>
  // Values are OWID PERCENTAGES (0-100), not 0-1 fractions. Field is
  // `importShare`, not `imported`. Countries with null importShare are
  // excluded from the average (not treated as zero).
  const mix = sources['energy:mix:v1:_all'];
  if (!mix || typeof mix !== 'object') return 0;
  const entries = Object.entries(mix).filter(([iso]) => countries.has(iso));
  if (!entries.length) return 0;

  // Vulnerability = 0.5 * import share + 0.25 * (1 - storage proxy) + 0.25 * (1 - SPR proxy)
  // Phase 0: only import share is reliably present per-country.
  let totalImport = 0;
  let validCount = 0;
  for (const [, m] of entries) {
    if (m == null || m.importShare == null) continue;
    totalImport += clip(num(m.importShare) / 100, 0, 1);
    validCount += 1;
  }
  const avgImport = validCount > 0 ? totalImport / validCount : 0;

  // Storage proxy from EU gas storage (single number for EU region)
  const euGas = sources['economic:eu-gas-storage:v1'];
  const months = Array.isArray(euGas?.months) ? euGas.months : [];
  const latestFill = months.length ? num(months[months.length - 1]?.fillPct) / 100 : null;
  const cStorage = latestFill !== null ? 1 - clip(latestFill / 0.8, 0, 1) : 0.5;

  // SPR proxy
  const spr = sources['economic:spr:v1'];
  const sprDays = num(spr?.daysOfCover, 90);
  const cSpr = 1 - clip(sprDays / 90, 0, 1);

  const score = 0.5 * avgImport + 0.25 * cStorage + 0.25 * cSpr;

  if (avgImport > 0.4 && validCount > 0) {
    drivers.push({
      axis: 'energy_vulnerability',
      description: `Average import dependency ${(avgImport * 100).toFixed(0)}% across ${validCount} countries`,
      magnitude: round(avgImport),
      evidence_ids: ['energy:mix'],
      orientation: 'pressure',
    });
  }

  return clip(score, 0, 1);
}

function computeAllianceCohesion(regionId, sources, drivers) {
  // Phase 0: rough alliance signal from forecast actor lenses.
  // No headline classification yet (deferred to Phase 1 LLM batch tagging).
  const fc = sources['forecast:predictions:v2'];
  const forecasts = Array.isArray(fc?.predictions) ? fc.predictions : [];
  const region = REGIONS.find((r) => r.id === regionId);
  const inRegion = forecasts.filter((f) => {
    const fRegion = String(f?.region ?? '').toLowerCase();
    return fRegion.includes(String(region?.forecastLabel ?? '').toLowerCase());
  });
  const allianceRefs = inRegion.filter((f) => {
    const cf = JSON.stringify(f?.caseFile ?? {}).toLowerCase();
    return /alliance|treaty|coordination|coalition|nato|gcc/.test(cf);
  }).length;

  const cActor = clip(allianceRefs / 5, 0, 1);
  // Baseline: assume neutral cohesion if no data
  const score = 0.4 * cActor + 0.6 * 0.5;

  if (cActor > 0.2) {
    drivers.push({
      axis: 'alliance_cohesion',
      description: `${allianceRefs} forecast actor lenses reference alliance dynamics`,
      magnitude: round(cActor),
      evidence_ids: [],
      orientation: 'buffer',
    });
  }

  return clip(score, 0, 1);
}

function computeMaritimeAccess(corridors, sources, drivers) {
  if (!corridors.length) return 0.7; // Inland regions: assume neutral-good

  const chokepointData = sources['supply_chain:chokepoints:v4'];
  const allCps = Array.isArray(chokepointData?.chokepoints) ? chokepointData.chokepoints : [];
  const cpById = new Map();
  for (const cp of allCps) cpById.set(String(cp?.id ?? ''), cp);

  const transitData = sources['supply_chain:transit-summaries:v1'];
  const summaries = transitData?.summaries ?? {};

  let weightedSum = 0;
  let totalWeight = 0;

  for (const corridor of corridors) {
    if (!corridor.chokepointId) continue;
    const cp = cpById.get(corridor.chokepointId);
    if (!cp) continue;

    const threatLevel = String(cp?.threatLevel ?? cp?.status ?? 'normal').toLowerCase();
    const threatMap = { war_zone: 0.0, critical: 0.2, high: 0.4, elevated: 0.6, normal: 1.0 };
    const mThreat = threatMap[threatLevel] ?? 0.7;

    const summary = summaries[corridor.chokepointId];
    const wow = num(summary?.wowChangePct, 0);
    const mThroughput = clip((100 + wow) / 120, 0, 1); // 0% change -> 0.83, +20% -> 1.0

    const mCorridor = 0.6 * mThreat + 0.4 * mThroughput;
    weightedSum += mCorridor * corridor.weight;
    totalWeight += corridor.weight;

    if (mThreat < 0.6) {
      drivers.push({
        axis: 'maritime_access',
        description: `${corridor.label} threat level: ${sanitizeEvidenceString(threatLevel)}`,
        magnitude: round(1 - mThreat),
        evidence_ids: [`chokepoint:${corridor.chokepointId}`],
        orientation: 'buffer',
      });
    }
  }

  return totalWeight > 0 ? clip(weightedSum / totalWeight, 0, 1) : 0.7;
}

function computeEnergyLeverage(countries, drivers) {
  // Producer leverage = max across region's producers
  const TOP_PRODUCERS = new Set(['SA', 'RU', 'US', 'IR', 'IQ', 'AE', 'CA', 'KW', 'QA', 'NG', 'NO', 'BR', 'MX', 'VE', 'AU']);
  const inRegion = [...countries].filter((c) => TOP_PRODUCERS.has(c));
  if (!inRegion.length) return 0;

  // Phase 0: simple presence-based leverage. Sophistication arrives in Phase 1.
  const baseLeverage = 0.6;
  const score = inRegion.length >= 3 ? Math.min(1, baseLeverage + 0.1 * inRegion.length) : baseLeverage;

  drivers.push({
    axis: 'energy_leverage',
    description: `${inRegion.length} top-15 producers in region: ${inRegion.join(', ')}`,
    magnitude: round(score),
    evidence_ids: inRegion.map((iso) => `producer:${iso}`),
    orientation: 'buffer',
  });

  return clip(score, 0, 1);
}

function round(n) {
  return Math.round(n * 1000) / 1000;
}
