/**
 * Grade statistics pure functions.
 *
 * Extracted from data-access / data-access-analytics / data-access-ranking
 * to keep data-access layer focused on DB I/O and make statistics logic
 * independently testable. All functions are pure (no side effects, no I/O).
 */

import { normalize, toNumber } from "./lib/grade-utils"
import type {
  ClassComparisonItem,
  GradeDistributionBucket,
  GradeDistributionResult,
  GradeStats,
  GradeTrendPoint,
  RankingTrendPoint,
} from "./types"

/** Round to 2 decimal places. */
const round2 = (n: number): number => Math.round(n * 100) / 100

/** Pass threshold (60% of full score, normalized to 60/100). */
export const PASS_THRESHOLD = 60

/** Excellent threshold (85% of full score, normalized to 85/100). */
export const EXCELLENT_THRESHOLD = 85

/**
 * Raw score row from DB (numeric columns may be string | number depending on driver).
 */
export interface RawScoreRow {
  score: unknown
  fullScore: unknown
}

/**
 * Compute aggregate stats (average/median/max/min/stdDev/passRate/excellentRate/count)
 * from a list of raw score rows. Returns null when rows is empty.
 */
export function computeGradeStats(rows: RawScoreRow[]): GradeStats | null {
  if (rows.length === 0) return null

  const scores = rows.map((r) => toNumber(r.score))
  const fullScores = rows.map((r) => toNumber(r.fullScore))
  const countN = scores.length
  const sum = scores.reduce((a, b) => a + b, 0)
  const average = sum / countN
  const sorted = [...scores].sort((a, b) => a - b)
  const mid = Math.floor(countN / 2)
  const median =
    countN % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
  const max = sorted[countN - 1]
  const min = sorted[0]
  const variance =
    scores.reduce((acc, s) => acc + Math.pow(s - average, 2), 0) / countN
  const stdDev = Math.sqrt(variance)

  let passCount = 0
  let excellentCount = 0
  for (let i = 0; i < countN; i += 1) {
    if (fullScores[i] <= 0) continue
    const ratio = scores[i] / fullScores[i]
    if (ratio >= 0.6) passCount += 1
    if (ratio >= 0.85) excellentCount += 1
  }

  return {
    average: round2(average),
    median: round2(median),
    max,
    min,
    stdDev: round2(stdDev),
    passRate: round2((passCount / countN) * 100),
    excellentRate: round2((excellentCount / countN) * 100),
    count: countN,
  }
}

/**
 * Compute average score from a list of grade record list items.
 */
export function computeAverageScore(scores: number[]): number {
  if (scores.length === 0) return 0
  return round2(scores.reduce((a, b) => a + b, 0) / scores.length)
}

/**
 * Build a grade trend point list from raw DB rows (already ordered by date asc).
 */
export function buildGradeTrendPoints(
  rows: Array<{
    record: {
      createdAt: Date
      title: string
      score: unknown
      fullScore: unknown
      type: string
    }
  }>
): GradeTrendPoint[] {
  return rows.map((r) => {
    const score = toNumber(r.record.score)
    const fullScore = toNumber(r.record.fullScore)
    return {
      date: r.record.createdAt.toISOString(),
      title: r.record.title,
      score,
      fullScore,
      normalizedScore: normalize(score, fullScore),
      type: r.record.type as GradeTrendPoint["type"],
    }
  })
}

/**
 * Compute the average of normalized scores from a list of trend points.
 */
export function computeTrendAverage(points: GradeTrendPoint[]): number {
  if (points.length === 0) return 0
  const avg =
    points.reduce((acc, p) => acc + p.normalizedScore, 0) / points.length
  return round2(avg)
}

/**
 * Compute class comparison stats for a single class from raw score rows.
 */
export function computeClassComparisonStats(
  rows: Array<{
    score: unknown
    fullScore: unknown
    studentId: string
  }>
): Pick<
  ClassComparisonItem,
  | "averageScore"
  | "medianScore"
  | "passRate"
  | "excellentRate"
  | "count"
  | "studentCount"
> {
  if (rows.length === 0) {
    return {
      averageScore: 0,
      medianScore: 0,
      passRate: 0,
      excellentRate: 0,
      count: 0,
      studentCount: 0,
    }
  }

  const normalized = rows.map((r) =>
    normalize(toNumber(r.score), toNumber(r.fullScore))
  )
  const sorted = [...normalized].sort((a, b) => a - b)
  const mid = Math.floor(sorted.length / 2)
  const median =
    sorted.length % 2 === 0
      ? (sorted[mid - 1] + sorted[mid]) / 2
      : sorted[mid]
  const avg = normalized.reduce((a, b) => a + b, 0) / normalized.length
  const uniqueStudents = new Set(rows.map((r) => r.studentId)).size

  let passCount = 0
  let excellentCount = 0
  for (const s of normalized) {
    if (s >= PASS_THRESHOLD) passCount += 1
    if (s >= EXCELLENT_THRESHOLD) excellentCount += 1
  }

  return {
    averageScore: round2(avg),
    medianScore: round2(median),
    passRate: round2((passCount / normalized.length) * 100),
    excellentRate: round2((excellentCount / normalized.length) * 100),
    count: normalized.length,
    studentCount: uniqueStudents,
  }
}

/**
 * Compute subject comparison stats for a single subject from normalized scores.
 */
export function computeSubjectComparisonStats(
  scores: number[]
): Pick<
  import("./types").SubjectComparisonItem,
  "averageScore" | "medianScore" | "passRate" | "excellentRate" | "count"
> {
  if (scores.length === 0) {
    return {
      averageScore: 0,
      medianScore: 0,
      passRate: 0,
      excellentRate: 0,
      count: 0,
    }
  }

  const sorted = [...scores].sort((a, b) => a - b)
  const mid = Math.floor(sorted.length / 2)
  const median =
    sorted.length % 2 === 0
      ? (sorted[mid - 1] + sorted[mid]) / 2
      : sorted[mid]
  const avg = scores.reduce((a, b) => a + b, 0) / scores.length

  let passCount = 0
  let excellentCount = 0
  for (const s of scores) {
    if (s >= PASS_THRESHOLD) passCount += 1
    if (s >= EXCELLENT_THRESHOLD) excellentCount += 1
  }

  return {
    averageScore: round2(avg),
    medianScore: round2(median),
    passRate: round2((passCount / scores.length) * 100),
    excellentRate: round2((excellentCount / scores.length) * 100),
    count: scores.length,
  }
}

/**
 * Default distribution buckets (90-100, 80-89, 70-79, 60-69, <60).
 */
export function createDefaultBuckets(): GradeDistributionBucket[] {
  return [
    { label: "90-100", min: 90, max: 100, count: 0 },
    { label: "80-89", min: 80, max: 89, count: 0 },
    { label: "70-79", min: 70, max: 79, count: 0 },
    { label: "60-69", min: 60, max: 69, count: 0 },
    { label: "<60", min: 0, max: 59, count: 0 },
  ]
}

/**
 * Bucketize raw score rows into a grade distribution result.
 */
export function computeGradeDistribution(
  rows: RawScoreRow[]
): GradeDistributionResult {
  const buckets = createDefaultBuckets()
  for (const r of rows) {
    const normalized = normalize(toNumber(r.score), toNumber(r.fullScore))
    const rounded = Math.round(normalized)
    if (rounded >= 90) buckets[0].count += 1
    else if (rounded >= 80) buckets[1].count += 1
    else if (rounded >= 70) buckets[2].count += 1
    else if (rounded >= 60) buckets[3].count += 1
    else buckets[4].count += 1
  }
  return { buckets, totalCount: rows.length }
}

/**
 * Ranking trend entry for a single assessment (grouped by title).
 */
export interface RankingTrendEntry {
  date: Date
  entries: Array<{ studentId: string; normalized: number }>
}

/**
 * Build ranking trend points from grouped entries.
 * Returns points sorted by date ascending.
 */
export function buildRankingTrendPoints(
  byTitle: Map<string, RankingTrendEntry>,
  targetStudentId: string
): RankingTrendPoint[] {
  const points: RankingTrendPoint[] = []
  for (const [title, entry] of byTitle.entries()) {
    if (entry.entries.length === 0) continue
    const sorted = [...entry.entries].sort(
      (a, b) => b.normalized - a.normalized
    )
    let rank = 0
    let studentEntry: { studentId: string; normalized: number } | null = null
    for (let i = 0; i < sorted.length; i += 1) {
      const e = sorted[i]
      if (e.studentId === targetStudentId) {
        rank = i + 1
        studentEntry = e
        break
      }
    }
    if (rank <= 0 || !studentEntry) continue

    points.push({
      title,
      date: entry.date.toISOString(),
      score: studentEntry.normalized,
      rank,
      totalStudents: sorted.length,
    })
  }

  points.sort(
    (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
  )
  return points
}
