/**
 * Pareto strategy exploration and trade-off explanation (§34, §35, §36, §37, §38, §39).
 *
 * The 3.0 optimiser collapses nine objectives into one weighted score and names a winner.
 * That is the right answer to "what should I do", and the wrong answer to "what are my
 * options" — because the weights encode a preference the operator never stated, and a
 * strategy that is 4 % worse overall but recovers twice as fast may be exactly what the
 * shift needs at 14:40 on a Thursday.
 *
 * So this module does not choose. It finds the non-dominated set — the strategies where
 * you cannot do better on one objective without doing worse on another — names the corner
 * each one owns, and, crucially, explains why every *other* strategy is not recommended.
 *
 * That last part is §35, and it is the part that makes the recommendation trustworthy: a
 * system that can only say why it picked A is indistinguishable from one that picked A at
 * random and rationalised afterwards. A system that can say "B was rejected because it
 * puts L-22 into its warning band" is showing its work.
 */

import { round } from './rng'

// ─────────────────────────────────────────────────────────────────────────────
// The trade-off space
// ─────────────────────────────────────────────────────────────────────────────

export const TRADEOFF_METRICS = [
  'risk_reduction',
  'cost',
  'customer_impact',
  'execution_time',
  'asset_stress',
  'battery_consumption',
  'renewable_curtailment',
  'confidence',
] as const
export type TradeoffMetric = (typeof TRADEOFF_METRICS)[number]

/** Which direction is good. Everything is normalised into "higher is better" internally. */
export const METRIC_DIRECTION: Record<TradeoffMetric, 'lower_is_better' | 'higher_is_better'> = {
  risk_reduction: 'higher_is_better',
  cost: 'lower_is_better',
  customer_impact: 'lower_is_better',
  execution_time: 'lower_is_better',
  asset_stress: 'lower_is_better',
  battery_consumption: 'lower_is_better',
  renewable_curtailment: 'lower_is_better',
  confidence: 'higher_is_better',
}

export const METRIC_LABELS: Record<TradeoffMetric, [string, string]> = {
  risk_reduction: ['Risk reduction', 'خفض الخطر'],
  cost: ['Cost', 'التكلفة'],
  customer_impact: ['Customer impact', 'الأثر على المشتركين'],
  execution_time: ['Execution time', 'زمن التنفيذ'],
  asset_stress: ['Asset stress', 'إجهاد الأصل'],
  battery_consumption: ['Battery consumption', 'استهلاك البطارية'],
  renewable_curtailment: ['Renewable curtailment', 'تقليص المتجددة'],
  confidence: ['Confidence', 'الثقة'],
}

export const METRIC_UNITS: Record<TradeoffMetric, string> = {
  risk_reduction: 'pts',
  cost: 'index',
  customer_impact: 'customers',
  execution_time: 'min',
  asset_stress: 'index',
  battery_consumption: '%',
  renewable_curtailment: 'MW',
  confidence: '%',
}

export interface TradeoffCandidate {
  id: string
  key: string
  label: string
  labelAr: string
  metrics: Record<TradeoffMetric, number>
  /** Set when the physics validator rejected it — such a strategy is never on the frontier. */
  physicsRejection?: { reason: string; reasonAr: string } | null
}

export const PARETO_CORNERS = [
  'lowest_cost',
  'lowest_risk',
  'fastest_recovery',
  'lowest_customer_impact',
  'balanced',
] as const
export type ParetoCorner = (typeof PARETO_CORNERS)[number]

export interface ParetoEntry {
  candidate: TradeoffCandidate
  isOnFrontier: boolean
  /** How many other candidates beat this one on every objective at once. */
  dominatedBy: number
  /** Which corner of the frontier this strategy owns, if any. */
  corner: ParetoCorner | ''
  /** Spread around this point on the frontier. Higher = a more distinct compromise. */
  crowdingDistance: number
  rank: number
  /** Populated for everything that is not the recommendation (§35). */
  rejectionReason: string
  rejectionReasonAr: string
}

export interface ParetoResult {
  entries: ParetoEntry[]
  frontier: ParetoEntry[]
  recommended: ParetoEntry | null
  /** Normalised 0–1 values per candidate per metric, for the matrix view (§36). */
  matrix: Array<{ id: string; label: string; labelAr: string; values: Record<TradeoffMetric, number> }>
}

// ─────────────────────────────────────────────────────────────────────────────
// Normalisation and dominance
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Min–max normalise every metric across the candidate set, oriented so 1 is always good.
 *
 * Normalising within the set rather than against absolute scales is deliberate: the
 * question is which of *these* options is better, and a cost index of 40 means nothing
 * until you know the alternatives cost 12 and 90.
 */
function normalise(candidates: TradeoffCandidate[]): Map<string, Record<TradeoffMetric, number>> {
  const out = new Map<string, Record<TradeoffMetric, number>>()
  const bounds = {} as Record<TradeoffMetric, { min: number; max: number }>

  for (const metric of TRADEOFF_METRICS) {
    const values = candidates.map((c) => c.metrics[metric] ?? 0)
    bounds[metric] = { min: Math.min(...values), max: Math.max(...values) }
  }

  for (const candidate of candidates) {
    const values = {} as Record<TradeoffMetric, number>
    for (const metric of TRADEOFF_METRICS) {
      const { min, max } = bounds[metric]
      const span = max - min
      // A metric on which every candidate is identical carries no information, so it
      // sits at the midpoint rather than at an arbitrary end.
      const raw = span > 1e-9 ? ((candidate.metrics[metric] ?? 0) - min) / span : 0.5
      values[metric] = METRIC_DIRECTION[metric] === 'higher_is_better' ? raw : 1 - raw
    }
    out.set(candidate.id, values)
  }

  return out
}

/** True when `a` is at least as good as `b` everywhere and strictly better somewhere. */
function dominates(
  a: Record<TradeoffMetric, number>,
  b: Record<TradeoffMetric, number>,
): boolean {
  let strictly = false
  for (const metric of TRADEOFF_METRICS) {
    if (a[metric] < b[metric] - 1e-9) return false
    if (a[metric] > b[metric] + 1e-9) strictly = true
  }
  return strictly
}

// ─────────────────────────────────────────────────────────────────────────────
// The explorer
// ─────────────────────────────────────────────────────────────────────────────

export function exploreParetoFrontier(candidates: TradeoffCandidate[]): ParetoResult {
  if (!candidates.length) {
    return { entries: [], frontier: [], recommended: null, matrix: [] }
  }

  const normalised = normalise(candidates)

  // A strategy the physics validator rejected is not a trade-off, it is unavailable.
  // Keeping it in the dominance calculation would let an impossible option shape the
  // frontier that the possible ones are measured against.
  const eligible = candidates.filter((c) => !c.physicsRejection)

  const entries: ParetoEntry[] = candidates.map((candidate) => {
    const mine = normalised.get(candidate.id)!
    const dominatedBy = candidate.physicsRejection
      ? eligible.length
      : eligible.filter((other) => other.id !== candidate.id && dominates(normalised.get(other.id)!, mine))
          .length

    return {
      candidate,
      isOnFrontier: !candidate.physicsRejection && dominatedBy === 0,
      dominatedBy,
      corner: '',
      crowdingDistance: 0,
      rank: 0,
      rejectionReason: '',
      rejectionReasonAr: '',
    }
  })

  const frontier = entries.filter((e) => e.isOnFrontier)

  // ── Corners ───────────────────────────────────────────────────────────────
  const claimCorner = (corner: ParetoCorner, metric: TradeoffMetric) => {
    const best = frontier
      .filter((e) => !e.corner)
      .sort((a, b) => normalised.get(b.candidate.id)![metric] - normalised.get(a.candidate.id)![metric])[0]
    if (best) best.corner = corner
  }
  claimCorner('lowest_risk', 'risk_reduction')
  claimCorner('lowest_cost', 'cost')
  claimCorner('fastest_recovery', 'execution_time')
  claimCorner('lowest_customer_impact', 'customer_impact')

  // ── Crowding distance and the balanced pick ───────────────────────────────
  // The balanced strategy is the one whose normalised scores are most *even*: least
  // spread between its best and worst objective. That is a different question from
  // "highest average", and it is the one an operator means by "balanced" — no nasty
  // surprise on any single axis.
  const means = new Map<string, number>()
  for (const entry of frontier) {
    const values = TRADEOFF_METRICS.map((m) => normalised.get(entry.candidate.id)![m])
    const mean = values.reduce((a, b) => a + b, 0) / values.length
    const spread = Math.sqrt(values.reduce((s, v) => s + (v - mean) ** 2, 0) / values.length)
    entry.crowdingDistance = round(spread, 3)
    means.set(entry.candidate.id, mean)
  }

  // Evenness first, then overall standing. Two candidates can be mirror images of each
  // other — one evenly good, one evenly bad — and have identical spread; without the
  // second term the pick between them would come down to array order.
  const byBalance = (a: ParetoEntry, b: ParetoEntry) =>
    a.crowdingDistance - b.crowdingDistance ||
    (means.get(b.candidate.id) ?? 0) - (means.get(a.candidate.id) ?? 0)

  const balanced = frontier.filter((e) => !e.corner).sort(byBalance)[0]
  if (balanced) balanced.corner = 'balanced'
  else {
    // Every frontier point already owns a corner. The recommendation is then the most
    // even of them, which may also be a corner-holder.
    const evenest = [...frontier].sort(byBalance)[0]
    if (evenest) evenest.corner = evenest.corner || 'balanced'
  }

  const recommended =
    frontier.find((e) => e.corner === 'balanced') ?? [...frontier].sort(byBalance)[0] ?? null

  // ── Ranking and "why not" ─────────────────────────────────────────────────
  const ordered = [...entries].sort((a, b) => {
    if (a.isOnFrontier !== b.isOnFrontier) return a.isOnFrontier ? -1 : 1
    return a.dominatedBy - b.dominatedBy || a.crowdingDistance - b.crowdingDistance
  })
  ordered.forEach((entry, index) => {
    entry.rank = index + 1
  })

  for (const entry of entries) {
    if (recommended && entry.candidate.id === recommended.candidate.id) continue
    const [en, ar] = explainRejection(entry, recommended, normalised, candidates)
    entry.rejectionReason = en
    entry.rejectionReasonAr = ar
  }

  return {
    entries: ordered,
    frontier,
    recommended,
    matrix: candidates.map((c) => ({
      id: c.id,
      label: c.label,
      labelAr: c.labelAr,
      values: normalised.get(c.id)!,
    })),
  }
}

/**
 * Why this strategy is not the recommendation.
 *
 * Answers in order of decisiveness: physics first (it is not available at all), then
 * dominance (something beats it on everything), then the specific axes where it loses to
 * the recommendation. Anything on the frontier gets the honest answer — it is a real
 * alternative, just a different trade.
 */
function explainRejection(
  entry: ParetoEntry,
  recommended: ParetoEntry | null,
  normalised: Map<string, Record<TradeoffMetric, number>>,
  candidates: TradeoffCandidate[],
): [string, string] {
  if (entry.candidate.physicsRejection) {
    return [
      `Rejected by the physics validator: ${entry.candidate.physicsRejection.reason}`,
      `مرفوض من مدقق الفيزياء: ${entry.candidate.physicsRejection.reasonAr}`,
    ]
  }

  if (!recommended) return ['', '']

  const mine = normalised.get(entry.candidate.id)!
  const theirs = normalised.get(recommended.candidate.id)!

  if (entry.dominatedBy > 0) {
    const dominator = candidates.find(
      (c) => c.id !== entry.candidate.id && dominates(normalised.get(c.id)!, mine),
    )
    const worse = TRADEOFF_METRICS.filter(
      (m) => dominator && normalised.get(dominator.id)![m] > mine[m] + 0.05,
    )
      .slice(0, 2)
      .map((m) => METRIC_LABELS[m])
    return [
      dominator
        ? `"${dominator.label}" is at least as good on every objective and better on ${worse.map((w) => w[0].toLowerCase()).join(' and ')}`
        : 'Beaten on every objective by another available strategy',
      dominator
        ? `"${dominator.labelAr}" لا تقل عنها في أي هدف وتتفوق في ${worse.map((w) => w[1]).join(' و')}`
        : 'تتفوق عليها استراتيجية أخرى في كل الأهداف',
    ]
  }

  // On the frontier: name the two axes where it trades worst against the recommendation.
  const losses = TRADEOFF_METRICS.map((m) => ({ m, gap: theirs[m] - mine[m] }))
    .filter((l) => l.gap > 0.08)
    .sort((a, b) => b.gap - a.gap)
    .slice(0, 2)

  if (!losses.length) {
    return [
      'A close alternative — it trades slightly less evenly across the objectives',
      'بديل قريب — يوازن بين الأهداف بشكل أقل انتظامًا',
    ]
  }

  const detail = (locale: 0 | 1) =>
    losses
      .map((l) => {
        const raw = entry.candidate.metrics[l.m]
        const other = recommended.candidate.metrics[l.m]
        const unit = METRIC_UNITS[l.m]
        return locale === 0
          ? `${METRIC_LABELS[l.m][0].toLowerCase()} ${round(raw, 1)} ${unit} against ${round(other, 1)}`
          : `${METRIC_LABELS[l.m][1]} ${round(raw, 1)} ${unit} مقابل ${round(other, 1)}`
      })
      .join(locale === 0 ? '; ' : '؛ ')

  return [`Worse on ${detail(0)}`, `أضعف في ${detail(1)}`]
}
