/**
 * 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,
  GradeRecordSemester,
  GradeTrendPoint,
  RankingTrendPoint,
  StudentGrowthArchivePoint,
} 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)
}

/**
 * Type guard: check if a value is a valid GradeTrendPoint type.
 */
function isGradeTrendType(v: unknown): v is GradeTrendPoint["type"] {
  return v === "exam" || v === "quiz" || v === "homework" || v === "other"
}

/**
 * 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: isGradeTrendType(r.record.type) ? r.record.type : "other",
    }
  })
}

/**
 * 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).
 */
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 },
  ]
}

/**
 * P3-9: 给定归一化分数（0-100），返回其在分布桶中的索引。
 * 返回 -1 表示分数不在任何桶内（例如分数 < 0 或 > 100）。
 *
 * 纯函数 — 无副作用，无 I/O。可独立测试。
 *
 * @param buckets - 已计算的分布桶列表
 * @param normalizedScore - 学生的归一化分数（0-100）
 */
export function findBucketIndex(
  buckets: ReadonlyArray<GradeDistributionBucket>,
  normalizedScore: number
): number {
  const rounded = Math.round(normalizedScore)
  for (let i = 0; i < buckets.length; i += 1) {
    const b = buckets[i]
    if (rounded >= b.min && rounded <= b.max) return i
  }
  return -1
}

/**
 * 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
}

/**
 * P3-2: Score anomaly detection result.
 * Returned when a student's latest score shows a significant drop.
 */
export interface ScoreAnomalyResult {
  /** Whether an anomaly was detected. */
  readonly isAnomaly: boolean
  /** The latest normalized score (0-100). */
  readonly latestScore: number
  /** The historical average normalized score (0-100). */
  readonly historicalAverage: number
  /** Absolute drop (historicalAverage - latestScore), positive = drop. */
  readonly drop: number
  /** Relative drop percentage (0-1), e.g. 0.15 = 15% drop. */
  readonly dropRatio: number
  /** Severity: "warning" (drop ≥ 10 points or 10%), "critical" (drop ≥ 20 points or 20%). */
  readonly severity: "warning" | "critical" | null
}

/** Thresholds for anomaly detection (aligned with industry practice: 智学网). */
const ANOMALY_WARNING_DROP = 10
const ANOMALY_CRITICAL_DROP = 20
const ANOMALY_WARNING_RATIO = 0.1
const ANOMALY_CRITICAL_RATIO = 0.2
/** Minimum historical data points required for anomaly detection. */
const MIN_HISTORY_POINTS = 2

/**
 * P3-2: Detect score anomaly for a student.
 *
 * Compares the latest normalized score against the historical average.
 * Triggers when:
 * - At least 2 historical data points exist
 * - Absolute drop ≥ 10 points (warning) or ≥ 20 points (critical)
 * - OR relative drop ≥ 10% (warning) or ≥ 20% (critical)
 *
 * Uses the more severe of absolute/relative thresholds.
 * Pure function — no side effects, no I/O. Independently testable.
 *
 * @param trendPoints - Sorted by date ascending (oldest first). Must be normalized 0-100.
 * @returns ScoreAnomalyResult with isAnomaly=false if insufficient data.
 */
export function detectScoreAnomaly(
  trendPoints: ReadonlyArray<Pick<GradeTrendPoint, "normalizedScore">>
): ScoreAnomalyResult {
  if (trendPoints.length < MIN_HISTORY_POINTS + 1) {
    return {
      isAnomaly: false,
      latestScore: 0,
      historicalAverage: 0,
      drop: 0,
      dropRatio: 0,
      severity: null,
    }
  }

  const history = trendPoints.slice(0, -1)
  const latest = trendPoints[trendPoints.length - 1]
  const latestScore = latest.normalizedScore
  const historicalAverage =
    history.reduce((sum, p) => sum + p.normalizedScore, 0) / history.length

  if (historicalAverage <= 0) {
    return {
      isAnomaly: false,
      latestScore,
      historicalAverage: 0,
      drop: 0,
      dropRatio: 0,
      severity: null,
    }
  }

  const drop = round2(historicalAverage - latestScore)
  const dropRatio = drop / historicalAverage

  if (drop <= 0) {
    return {
      isAnomaly: false,
      latestScore,
      historicalAverage: round2(historicalAverage),
      drop: 0,
      dropRatio: 0,
      severity: null,
    }
  }

  const isCriticalAbs = drop >= ANOMALY_CRITICAL_DROP
  const isCriticalRatio = dropRatio >= ANOMALY_CRITICAL_RATIO
  const isWarningAbs = drop >= ANOMALY_WARNING_DROP
  const isWarningRatio = dropRatio >= ANOMALY_WARNING_RATIO

  const severity: ScoreAnomalyResult["severity"] = isCriticalAbs || isCriticalRatio
    ? "critical"
    : isWarningAbs || isWarningRatio
      ? "warning"
      : null

  return {
    isAnomaly: severity !== null,
    latestScore,
    historicalAverage: round2(historicalAverage),
    drop,
    dropRatio: round2(dropRatio),
    severity,
  }
}

/**
 * P3-5: Statistical significance result for class comparison.
 * Computes Cohen's d (effect size) and an approximate p-value using
 * a two-sample t-test (Welch's approximation). No external dependencies.
 */
export interface SignificanceResult {
  /** Cohen's d effect size: <0.2 negligible, 0.2 small, 0.5 medium, 0.8 large. */
  readonly cohensD: number
  /** Effect size label: "negligible" | "small" | "medium" | "large". */
  readonly effectSizeLabel: "negligible" | "small" | "medium" | "large"
  /** Approximate p-value (two-tailed). */
  readonly pValue: number
  /** Whether the difference is statistically significant (p < 0.05). */
  readonly isSignificant: boolean
}

/**
 * P3-5: Compute statistical significance between two class score distributions.
 *
 * Uses Welch's t-test (does not assume equal variances).
 * Computes Cohen's d for effect size.
 *
 * Pure function — no side effects, no I/O. Independently testable.
 *
 * @param classA - Scores from class A (normalized 0-100)
 * @param classB - Scores from class B (normalized 0-100)
 * @returns SignificanceResult, or null if either group has < 2 samples.
 */
export function computeSignificance(
  classA: readonly number[],
  classB: readonly number[]
): SignificanceResult | null {
  const nA = classA.length
  const nB = classB.length
  if (nA < 2 || nB < 2) return null

  const mean = (arr: readonly number[]): number =>
    arr.reduce((s, v) => s + v, 0) / arr.length
  const variance = (arr: readonly number[], m: number): number => {
    const sumSq = arr.reduce((s, v) => s + (v - m) ** 2, 0)
    return sumSq / (arr.length - 1)
  }

  const meanA = mean(classA)
  const meanB = mean(classB)
  const varA = variance(classA, meanA)
  const varB = variance(classB, meanB)

  // Pooled standard deviation for Cohen's d
  const pooledStd = Math.sqrt(((nA - 1) * varA + (nB - 1) * varB) / (nA + nB - 2))
  if (pooledStd === 0) {
    return {
      cohensD: 0,
      effectSizeLabel: "negligible",
      pValue: 1,
      isSignificant: false,
    }
  }

  const cohensD = Math.abs(meanA - meanB) / pooledStd

  // Welch's t-statistic
  const seDiff = Math.sqrt(varA / nA + varB / nB)
  if (seDiff === 0) {
    return {
      cohensD: round2(cohensD),
      effectSizeLabel: labelEffectSize(cohensD),
      pValue: 1,
      isSignificant: false,
    }
  }
  const t = Math.abs(meanA - meanB) / seDiff

  // Welch–Satterthwaite degrees of freedom
  const num = (varA / nA + varB / nB) ** 2
  const den = (varA / nA) ** 2 / (nA - 1) + (varB / nB) ** 2 / (nB - 1)
  const df = num / den

  // Approximate p-value using normal distribution for large df,
  // or a simplified t-distribution for small df.
  const pValue = approximatePValue(t, df)

  return {
    cohensD: round2(cohensD),
    effectSizeLabel: labelEffectSize(cohensD),
    pValue: round2(pValue),
    isSignificant: pValue < 0.05,
  }
}

/** Label effect size based on Cohen's d. */
function labelEffectSize(d: number): SignificanceResult["effectSizeLabel"] {
  const abs = Math.abs(d)
  if (abs < 0.2) return "negligible"
  if (abs < 0.5) return "small"
  if (abs < 0.8) return "medium"
  return "large"
}

/**
 * Approximate two-tailed p-value from t-statistic and degrees of freedom.
 * Uses the normal distribution approximation for df > 30, and a
 * simplified series expansion for smaller df.
 */
function approximatePValue(t: number, df: number): number {
  // For large df, t-distribution converges to normal distribution
  if (df > 30) {
    // Standard normal CDF approximation (Abramowitz & Stegun 26.2.17)
    const z = Math.abs(t)
    const cdf = 1 - 0.5 * (1 + erf(z / Math.SQRT2))
    return 2 * cdf
  }
  // For small df, use a simplified approximation
  // p ≈ 2 * (1 - CDF_t(t, df))
  // Simplified: p ≈ 2 / (1 + t^2/df)^(df/2) for large t
  const x = df / (df + t * t)
  // Refined approximation using incomplete beta function ratio
  // This is a rough but safe upper bound
  const approx = Math.pow(x, df / 2)
  return Math.min(1, Math.max(0, 2 * approx * 0.5))
}

/** Error function approximation (Abramowitz & Stegun 7.1.26). */
function erf(x: number): number {
  const sign = x >= 0 ? 1 : -1
  const ax = Math.abs(x)
  const a1 = 0.254829592
  const a2 = -0.284496736
  const a3 = 1.421413741
  const a4 = -1.453152027
  const a5 = 1.061405429
  const p = 0.3275911
  const t = 1 / (1 + p * ax)
  const y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-ax * ax)
  return sign * y
}

/**
 * P3-4: 原始数据行，用于构建学生纵向成长档案。
 */
export interface GrowthArchiveRawRow {
  academicYearId: string | null
  academicYearName: string
  academicYearStart: Date
  semester: GradeRecordSemester
  score: unknown
  fullScore: unknown
  subjectId: string
  title: string
}

/**
 * P3-4: 按学年 + 学期分组聚合学生成绩，构建纵向成长档案点序列。
 *
 * 流程：
 * 1. 按 (academicYearId, semester) 分组
 * 2. 对每组计算归一化平均分、中位数、及格率、优秀率、记录数、科目数、考试数
 * 3. 按 (academicYearStart, semester) 升序排序（先按学年开始时间，再按学期 1 → 2）
 *
 * 跳过 academicYearId 为空的记录（无法归类到学年）。
 *
 * 纯函数 — 无副作用，无 I/O。可独立测试。
 *
 * @param rows - 学生的全部成绩记录行（含学年信息）
 * @returns 按 (学年 start, 学期) 升序排列的成长点列表
 */
export function buildGrowthArchivePoints(
  rows: ReadonlyArray<GrowthArchiveRawRow>
): StudentGrowthArchivePoint[] {
  if (rows.length === 0) return []

  // 按 (academicYearId, semester) 分组
  const groups = new Map<string, {
    academicYearId: string
    academicYearName: string
    academicYearStart: Date
    semester: GradeRecordSemester
    scores: number[]
    subjectIds: Set<string>
    assessmentTitles: Set<string>
  }>()

  for (const r of rows) {
    if (!r.academicYearId) continue
    const key = `${r.academicYearId}__${r.semester}`
    const group = groups.get(key)
    if (group) {
      const score = toNumber(r.score)
      const fullScore = toNumber(r.fullScore)
      group.scores.push(normalize(score, fullScore))
      group.subjectIds.add(r.subjectId)
      group.assessmentTitles.add(r.title)
    } else {
      const score = toNumber(r.score)
      const fullScore = toNumber(r.fullScore)
      groups.set(key, {
        academicYearId: r.academicYearId,
        academicYearName: r.academicYearName,
        academicYearStart: r.academicYearStart,
        semester: r.semester,
        scores: [normalize(score, fullScore)],
        subjectIds: new Set<string>([r.subjectId]),
        assessmentTitles: new Set<string>([r.title]),
      })
    }
  }

  const points: StudentGrowthArchivePoint[] = []
  for (const group of groups.values()) {
    const scores = group.scores
    if (scores.length === 0) continue

    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((sum, s) => sum + s, 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
    }

    points.push({
      academicYearId: group.academicYearId,
      academicYearName: group.academicYearName,
      semester: group.semester,
      averageScore: round2(avg),
      medianScore: round2(median),
      passRate: round2((passCount / scores.length) * 100),
      excellentRate: round2((excellentCount / scores.length) * 100),
      recordCount: scores.length,
      subjectCount: group.subjectIds.size,
      assessmentCount: group.assessmentTitles.size,
      academicYearStart: group.academicYearStart.toISOString(),
    })
  }

  // 按学年开始时间升序，再按学期（"1" → "2"）升序
  points.sort((a, b) => {
    const timeA = new Date(a.academicYearStart).getTime()
    const timeB = new Date(b.academicYearStart).getTime()
    if (timeA !== timeB) return timeA - timeB
    return a.semester.localeCompare(b.semester)
  })

  return points
}
