/**
 * Juror balancing: z-score normalization to correct for per-juror grading harshness.
 *
 * A juror who grades 1 standard deviation below their peers on shared projects
 * shouldn't punish those projects more than a juror who grades at the mean.
 * We compute per-juror mean + stddev across their scores in a round, z-normalize
 * each score, then rescale back onto the same 1-10 scale using the overall
 * round-level mean + stddev so the balanced number is directly comparable to
 * the raw average.
 */

export type ScorePoint = {
  projectId: string
  userId: string
  rawScore: number
}

export type BalancedProjectResult = {
  projectId: string
  rawAverage: number | null
  balancedAverage: number | null
  count: number
}

export type JurorBalance = {
  userId: string
  mean: number
  stddev: number
  count: number
}

export type BalanceContext = {
  overallMean: number
  overallStddev: number
  jurorStats: Map<string, JurorBalance>
}

/**
 * Build per-juror and overall grading statistics from a flat list of
 * (project, juror, score) points. Returns the stats plus a helper to
 * rescale z-scores back onto the raw-score scale.
 */
export function computeBalanceContext(points: ScorePoint[]): BalanceContext {
  const jurorScores = new Map<string, number[]>()
  for (const p of points) {
    const arr = jurorScores.get(p.userId) ?? []
    arr.push(p.rawScore)
    jurorScores.set(p.userId, arr)
  }

  const jurorStats = new Map<string, JurorBalance>()
  for (const [userId, scores] of jurorScores.entries()) {
    const mean = scores.reduce((a, b) => a + b, 0) / scores.length
    const variance = scores.length > 1
      ? scores.reduce((s, v) => s + (v - mean) ** 2, 0) / scores.length
      : 0
    jurorStats.set(userId, {
      userId,
      mean,
      stddev: Math.sqrt(variance),
      count: scores.length,
    })
  }

  const allScores = points.map((p) => p.rawScore)
  const overallMean = allScores.length > 0
    ? allScores.reduce((a, b) => a + b, 0) / allScores.length
    : 0
  const overallStddev = allScores.length > 1
    ? Math.sqrt(
        allScores.reduce((s, v) => s + (v - overallMean) ** 2, 0) / allScores.length,
      )
    : 0

  return { overallMean, overallStddev, jurorStats }
}

/**
 * Aggregate per-project raw + balanced averages from score points.
 */
export function computeBalancedProjectScores(
  points: ScorePoint[],
  ctx: BalanceContext,
): Map<string, BalancedProjectResult> {
  const byProject = new Map<string, ScorePoint[]>()
  for (const p of points) {
    const arr = byProject.get(p.projectId) ?? []
    arr.push(p)
    byProject.set(p.projectId, arr)
  }

  const results = new Map<string, BalancedProjectResult>()
  for (const [projectId, projectPoints] of byProject.entries()) {
    const rawAverage = projectPoints.reduce((a, b) => a + b.rawScore, 0) / projectPoints.length

    let balancedAverage: number | null = null
    if (ctx.overallStddev > 0) {
      const zValues: number[] = []
      for (const pt of projectPoints) {
        const stats = ctx.jurorStats.get(pt.userId)
        if (stats && stats.stddev > 0) {
          zValues.push((pt.rawScore - stats.mean) / stats.stddev)
        } else {
          zValues.push((pt.rawScore - ctx.overallMean) / ctx.overallStddev)
        }
      }
      const avgZ = zValues.reduce((a, b) => a + b, 0) / zValues.length
      balancedAverage = ctx.overallMean + avgZ * ctx.overallStddev
    }

    results.set(projectId, {
      projectId,
      rawAverage,
      balancedAverage,
      count: projectPoints.length,
    })
  }

  return results
}

/**
 * Per-round balanced rollup: groups points by roundId, computes a balance
 * context per round, then averages the per-round balanced averages for each
 * project. Use when surfacing edition-level rankings — never pool z-contexts
 * across rounds, because a juror's grading profile differs by round type.
 */
export type RoundScopedScorePoint = ScorePoint & { roundId: string }

export type EditionRollupResult = {
  projectId: string
  rawAverage: number | null
  balancedAverage: number | null
  count: number
  roundCount: number
}

export function computePerRoundBalanced(
  points: RoundScopedScorePoint[],
): Map<string, EditionRollupResult> {
  const byRound = new Map<string, ScorePoint[]>()
  for (const p of points) {
    const arr = byRound.get(p.roundId) ?? []
    arr.push({ projectId: p.projectId, userId: p.userId, rawScore: p.rawScore })
    byRound.set(p.roundId, arr)
  }

  const perRoundResults: Array<Map<string, BalancedProjectResult>> = []
  for (const roundPoints of byRound.values()) {
    const ctx = computeBalanceContext(roundPoints)
    perRoundResults.push(computeBalancedProjectScores(roundPoints, ctx))
  }

  const accumulator = new Map<
    string,
    { rawSum: number; rawCount: number; balancedSum: number; balancedCount: number; count: number; roundCount: number }
  >()
  for (const roundMap of perRoundResults) {
    for (const [projectId, result] of roundMap.entries()) {
      const acc = accumulator.get(projectId) ?? {
        rawSum: 0, rawCount: 0, balancedSum: 0, balancedCount: 0, count: 0, roundCount: 0,
      }
      if (result.rawAverage != null) {
        acc.rawSum += result.rawAverage
        acc.rawCount += 1
      }
      if (result.balancedAverage != null) {
        acc.balancedSum += result.balancedAverage
        acc.balancedCount += 1
      }
      acc.count += result.count
      acc.roundCount += 1
      accumulator.set(projectId, acc)
    }
  }

  const out = new Map<string, EditionRollupResult>()
  for (const [projectId, acc] of accumulator.entries()) {
    out.set(projectId, {
      projectId,
      rawAverage: acc.rawCount > 0 ? acc.rawSum / acc.rawCount : null,
      balancedAverage: acc.balancedCount > 0 ? acc.balancedSum / acc.balancedCount : null,
      count: acc.count,
      roundCount: acc.roundCount,
    })
  }
  return out
}

/**
 * Juror balancing for binary advance votes (yes/no).
 *
 * A "yes" from a juror who rarely says yes carries more weight than a "yes"
 * from a juror who routinely advances projects. We z-normalize each vote
 * against the juror's personal yes-rate distribution, then rescale the
 * project-level mean back onto the round's overall yes-rate scale so the
 * balanced number is directly comparable to the raw pass rate.
 */
export type VotePoint = {
  projectId: string
  userId: string
  vote: boolean
}

export type JurorYesRate = {
  userId: string
  yesRate: number
  stddev: number
  count: number
}

export type BalancedPassRateResult = {
  projectId: string
  rawPassRate: number | null
  balancedPassRate: number | null
  count: number
}

export type PassRateContext = {
  overallYesRate: number
  overallStddev: number
  jurorYesRates: Map<string, JurorYesRate>
}

export function computePassRateContext(votes: VotePoint[]): PassRateContext {
  const byJuror = new Map<string, boolean[]>()
  for (const v of votes) {
    const arr = byJuror.get(v.userId) ?? []
    arr.push(v.vote)
    byJuror.set(v.userId, arr)
  }

  const jurorYesRates = new Map<string, JurorYesRate>()
  for (const [userId, jurorVotes] of byJuror.entries()) {
    const yesCount = jurorVotes.filter(Boolean).length
    const yesRate = yesCount / jurorVotes.length
    // Bernoulli stddev: sqrt(p * (1 - p))
    const stddev = Math.sqrt(yesRate * (1 - yesRate))
    jurorYesRates.set(userId, { userId, yesRate, stddev, count: jurorVotes.length })
  }

  const totalYes = votes.filter((v) => v.vote).length
  const overallYesRate = votes.length > 0 ? totalYes / votes.length : 0
  const overallStddev = Math.sqrt(overallYesRate * (1 - overallYesRate))

  return { overallYesRate, overallStddev, jurorYesRates }
}

export function computeBalancedPassRates(
  votes: VotePoint[],
  ctx: PassRateContext,
): Map<string, BalancedPassRateResult> {
  const byProject = new Map<string, VotePoint[]>()
  for (const v of votes) {
    const arr = byProject.get(v.projectId) ?? []
    arr.push(v)
    byProject.set(v.projectId, arr)
  }

  const results = new Map<string, BalancedPassRateResult>()
  for (const [projectId, projectVotes] of byProject.entries()) {
    const yesCount = projectVotes.filter((v) => v.vote).length
    const rawPassRate = yesCount / projectVotes.length

    let balancedPassRate: number | null = null
    if (ctx.overallStddev > 0) {
      const zValues: number[] = []
      for (const v of projectVotes) {
        const stats = ctx.jurorYesRates.get(v.userId)
        const voteVal = v.vote ? 1 : 0
        if (stats && stats.stddev > 0) {
          zValues.push((voteVal - stats.yesRate) / stats.stddev)
        } else {
          zValues.push((voteVal - ctx.overallYesRate) / ctx.overallStddev)
        }
      }
      const avgZ = zValues.reduce((a, b) => a + b, 0) / zValues.length
      // Rescale and clamp to [0, 1] — z-rescaling can otherwise produce values
      // slightly outside that range when the round's yes rate is near 0 or 1.
      const rescaled = ctx.overallYesRate + avgZ * ctx.overallStddev
      balancedPassRate = Math.max(0, Math.min(1, rescaled))
    }

    results.set(projectId, {
      projectId,
      rawPassRate,
      balancedPassRate,
      count: projectVotes.length,
    })
  }

  return results
}
