/**
 * WoT members threshold formula for binary votes.
 *
 * Exact TypeScript port of backend/app/engine/threshold.py,
 * smith_threshold.py and techcomm_threshold.py (Python is the
 * reference implementation — same guards, same ceil rounding).
 *
 * Core formula:
 *   Result = C + B^W + (M + (1-M) * (1 - (T/W)^G)) * max(0, T - C)
 *
 * Where:
 *   C = constantBase
 *   B = baseExponent
 *   W = wotSize (corpus of eligible voters)
 *   T = totalVotes (for + against)
 *   M = majorityRatio (majorityPct / 100)
 *   G = gradientExponent
 *
 * Inertia behaviour:
 *   - Low participation (T << W) -> near-unanimity required
 *   - High participation (T -> W) -> simple majority M suffices
 *
 * Reference test case:
 *   wotSize=7224, votesFor=97, votesAgainst=23 (total=120)
 *   params M50 B.1 G.2 => threshold=94, adopted (97 >= 94)
 */

/**
 * Compute the minimum number of *for* votes required for adoption.
 *
 * A vote passes when `votesFor >= wotThreshold(...)`.
 *
 * @param wotSize - Size of the eligible voter corpus (WoT members)
 * @param totalVotes - Number of votes cast (for + against)
 * @param majorityPct - Majority percentage (0-100). 50 = simple majority at full participation
 * @param baseExponent - B in the formula; B^W is a vanishingly small offset when W is large
 * @param gradientExponent - G controls how fast the super-majority decays toward M
 * @param constantBase - C, a fixed additive floor on the threshold
 * @returns The ceiling of the computed threshold
 */
export function wotThreshold(
  wotSize: number,
  totalVotes: number,
  majorityPct: number = 50,
  baseExponent: number = 0.1,
  gradientExponent: number = 0.2,
  constantBase: number = 0.0,
): number {
  if (wotSize <= 0) {
    throw new Error('wotSize doit etre strictement positif')
  }
  if (totalVotes < 0) {
    throw new Error('totalVotes ne peut pas etre negatif')
  }
  if (majorityPct < 0 || majorityPct > 100) {
    throw new Error('majorityPct doit etre entre 0 et 100')
  }

  const C = constantBase
  const B = baseExponent
  const W = wotSize
  const T = totalVotes
  const M = majorityPct / 100.0
  const G = gradientExponent

  // Guard: if no votes, threshold is at least ceil(C + B^W)
  if (T === 0) {
    return Math.ceil(C + B ** W)
  }

  // Core formula
  const participationRatio = T / W
  const inertiaFactor = 1.0 - participationRatio ** G
  const requiredRatio = M + (1.0 - M) * inertiaFactor
  const result = C + B ** W + requiredRatio * Math.max(0.0, T - C)

  return Math.ceil(result)
}

/**
 * Smith sub-WoT threshold criterion.
 *
 * Formula: ceil(smithWotSize ^ S)
 *
 * @param smithWotSize - Number of active Smith members (forgerons)
 * @param exponent - S in the formula
 * @returns Minimum Smith votes required
 */
export function smithThreshold(smithWotSize: number, exponent: number = 0.1): number {
  if (smithWotSize <= 0) {
    throw new Error('smithWotSize doit etre strictement positif')
  }
  return Math.ceil(smithWotSize ** exponent)
}

/**
 * Technical Committee threshold criterion.
 *
 * Formula: ceil(cotecSize ^ T)
 *
 * @param cotecSize - Number of Technical Committee members
 * @param exponent - T in the formula
 * @returns Minimum TechComm votes required
 */
export function techcommThreshold(cotecSize: number, exponent: number = 0.1): number {
  if (cotecSize <= 0) {
    throw new Error('cotecSize doit etre strictement positif')
  }
  return Math.ceil(cotecSize ** exponent)
}

// ─────────────────────────────────────────────────────────────
// Election — simple plurality (BLUEPRINT-V2.md Δ28, « Modalités » #4)
// ─────────────────────────────────────────────────────────────

import type { FormulaParams, Id, Vote } from '~/types/domain'

/**
 * Outcome of an election tally.
 *
 * Discriminated union on `outcome`:
 *   - 'elected'  — a single person leads by simple plurality.
 *   - 'tie'      — several persons share the top count. The engine NEVER
 *                  breaks a tie (no randomness, no first-come): the closure
 *                  flow proposes a chained runoff among `exAequoIds`, or a
 *                  draw only if the Pact planned it (FormulaParams.tieBreak).
 *   - 'rejected' — reason 'quorum': participants (blanks included) below
 *                  electionMinParticipants; `required` = that quorum.
 *                  reason 'no-designation': quorum reached (or absent) but
 *                  every vote is blank — nobody was designated, and an
 *                  empty tie would be meaningless; `required` still carries
 *                  the quorum (0 when none) for display purposes.
 */
export type ElectionOutcome =
  | {
      outcome: 'elected'
      winnerId: Id
      counts: Record<Id, number>
      blanks: number
      participants: number
    }
  | {
      outcome: 'tie'
      exAequoIds: Id[]
      counts: Record<Id, number>
      blanks: number
      participants: number
    }
  | {
      outcome: 'rejected'
      reason: 'quorum' | 'no-designation'
      participants: number
      required: number
    }

/**
 * Tally an election by simple plurality.
 *
 * CONTRACT — the caller passes the LAST ACTIVE votes only: one vote per
 * voter, `supersedesVoteId` chains already resolved (the store filters
 * superseded votes). The engine does NOT deduplicate by voterId;
 * `participants` is simply `votes.length`.
 *
 * Rules (Δ28):
 *   - A vote without `choicePersonId` is a BLANK: it counts for
 *     participation (quorum), never for designation.
 *   - Quorum: when `formula.electionMinParticipants` is set and
 *     participants (blanks included) < quorum ⇒ rejected ('quorum').
 *   - Designation by simple PLURALITY of the non-blank votes.
 *   - Tie at the top ⇒ 'tie' with `exAequoIds` sorted (lexicographic —
 *     a deterministic display order, NEVER a tie-break: the engine does
 *     not pick a winner among equals, no randomness, no first-come).
 *   - Zero designation (all blanks) ⇒ rejected ('no-designation').
 *
 * @param votes - Last active votes of the session (see contract above)
 * @param formula - Protocol formula params (only electionMinParticipants is read)
 * @returns The election outcome — never a tie silently broken
 */
export function electionResult(
  votes: Vote[],
  formula: Pick<FormulaParams, 'electionMinParticipants'>,
): ElectionOutcome {
  const participants = votes.length
  const quorum = formula.electionMinParticipants ?? 0

  if (formula.electionMinParticipants !== undefined && participants < formula.electionMinParticipants) {
    return { outcome: 'rejected', reason: 'quorum', participants, required: formula.electionMinParticipants }
  }

  const counts: Record<Id, number> = {}
  let blanks = 0
  for (const vote of votes) {
    if (vote.choicePersonId) {
      counts[vote.choicePersonId] = (counts[vote.choicePersonId] ?? 0) + 1
    } else {
      blanks++ // blank: participation only, never designation
    }
  }

  const designatedIds = Object.keys(counts)
  if (designatedIds.length === 0) {
    return { outcome: 'rejected', reason: 'no-designation', participants, required: quorum }
  }

  const topCount = Math.max(...designatedIds.map(id => counts[id]!))
  const leaders = designatedIds.filter(id => counts[id] === topCount)

  if (leaders.length === 1) {
    return { outcome: 'elected', winnerId: leaders[0]!, counts, blanks, participants }
  }

  return { outcome: 'tie', exAequoIds: [...leaders].sort(), counts, blanks, participants }
}
