// Copyright (C) 2026 Jason M. Schwefel. All Rights Reserved.
package com.coldboreballisticsllc.scopedope.ballistics

import kotlin.math.*

// ── Thresholds ────────────────────────────────────────────────────────────────

// Calibrated reference band — for dense, smooth calculated/reference trajectories.
// Real-world fired DOPE (cases with realworld = true) is rounded to scope-click
// resolution and sparser, so it is scored per-case against a wider field band
// (0.1 mil = one scope click).
private const val TIER3_OK          = 0.05
private const val TIER3_INVESTIGATE = 0.10
private const val SUB3_OK           = 0.025
private const val SUB3_INVESTIGATE  = 0.05
private const val REAL_OK           = 0.10
private const val REAL_INVESTIGATE  = 0.20
private const val TRANSONIC_MULT    = 2.0

private const val TRANSONIC_17HMR_YD = 250.0

// ── Utilities ─────────────────────────────────────────────────────────────────

private fun isTransonic(caseId: String, distYd: Double): Boolean =
    "22lr" in caseId || ("17hmr" in caseId && distYd > TRANSONIC_17HMR_YD)

private fun thresholds(tier: String, transonic: Boolean, realworld: Boolean): Pair<Double, Double> {
    val mult = if (transonic) TRANSONIC_MULT else 1.0
    val (ok, inv) = when {
        realworld   -> REAL_OK to REAL_INVESTIGATE
        tier == "3" -> TIER3_OK to TIER3_INVESTIGATE
        else        -> SUB3_OK to SUB3_INVESTIGATE
    }
    return ok * mult to inv * mult
}

fun classify(delta: Double, tier: String, transonic: Boolean, realworld: Boolean): String {
    val (ok, inv) = thresholds(tier, transonic, realworld)
    val ad = abs(delta)
    return when {
        ad < ok  -> "OK"
        ad < inv -> "INVESTIGATE"
        else     -> "FAIL"
    }
}

private fun percentile(values: List<Double>, pct: Double): Double {
    if (values.isEmpty()) return Double.NaN
    val s = values.sorted()
    val k = (s.size - 1) * pct / 100.0
    val f = k.toInt(); val c = ceil(k).toInt()
    return if (f == c) s[f] else s[f] * (c - k) + s[c] * (k - f)
}

/** Generate all size-k subsets of indices [0, n). */
private fun combinations(n: Int, k: Int): Sequence<IntArray> = sequence {
    if (k == 0) { yield(IntArray(0)); return@sequence }
    if (k > n)  return@sequence
    val combo = IntArray(k) { it }
    while (true) {
        yield(combo.copyOf())
        var i = k - 1
        while (i >= 0 && combo[i] == i + n - k) i--
        if (i < 0) break
        combo[i]++
        for (j in i + 1 until k) combo[j] = combo[j - 1] + 1
    }
}

// ── Runner ────────────────────────────────────────────────────────────────────

class ExhaustiveCVRunner {

    fun runCase(tc: TestCase): CaseResult {
        val tier      = tc.tier
        val realworld = tc.realworld
        val envPct    = tc.conditions.confirmedFields.size / 4.0 * 100.0
        val hasFired  = tc.dopePoints.any { it.fired }

        if (tc.bc <= 0.0)     return skip(tc, tier, envPct, hasFired, "bc is zero")
        if (tc.mvFps <= 0.0)  return skip(tc, tier, envPct, hasFired, "mv_fps is zero")

        val params = tc.toParams()
        val zeroYd = tc.zeroYards

        val validPts = tc.dopePoints.mapNotNull { p ->
            val yd = p.yards; val mil = p.mil
            if (yd == 0.0 || yd == zeroYd) null else Pair(yd, mil)
        }
        val N = validPts.size

        if (N < 4) return CaseResult(
            tc.id, tc.caliber, tc.loadName, tier, envPct, hasFired, tc.mvSource, tc.dataNotes,
            distStats  = emptyList(),
            realworld  = realworld,
            looSkipped = true,
            looSkipWhy = "only $N usable points — need ≥4 for C(N,k) with k=1..N-3",
        )

        // ── Precompute physics + biases (2N integrations total) ──────────────
        val allDists = validPts.map { it.first }
        val physicsMap: Map<Double, TrajectoryResult> =
            BallisticsEngine.calculateTrajectory(params, allDists)

        val dopeEntries = validPts.map { (yd, mil) ->
            DopeEntryInput(distanceYards = yd, adjustmentMil = mil)
        }
        val biases = dopeEntries.map { entry ->
            try { BallisticsEngine.entryBias(entry, params) } catch (_: Exception) { Double.NaN }
        }

        // ── Enumerate C(N,k) for k = 1 .. N-3 ───────────────────────────────
        data class ComboPoint(
            val holdoutYd: Double, val actualMil: Double,
            val physicsMil: Double, val dopeMil: Double,
            val deltaPhysics: Double, val deltaDope: Double,
            val k: Int, val isInterior: Boolean, val flags: List<String>,
        )

        val allResults = mutableListOf<ComboPoint>()

        for (k in 1..N - 3) {
            for (holdoutIdxs in combinations(N, k)) {
                val holdoutSet   = holdoutIdxs.toHashSet()
                val trainingPairs = (0 until N)
                    .filter { it !in holdoutSet }
                    .map { dopeEntries[it] to biases[it] }
                val trainingYds  = trainingPairs.map { it.first.distanceYards }
                val tMin = trainingYds.min(); val tMax = trainingYds.max()

                for (hi in holdoutIdxs) {
                    val (holdYd, holdMil) = validPts[hi]
                    val matchDay = physicsMap[holdYd] ?: TrajectoryResult(0.0, 0.0)
                    val interior = tMin < holdYd && holdYd < tMax

                    val dopeMil = try {
                        BallisticsEngine.applyBias(trainingPairs, matchDay, holdYd).elevationMil
                    } catch (_: Exception) { Double.NaN }

                    val dPhys  = matchDay.elevationMil - holdMil
                    val dDope  = dopeMil - holdMil
                    val transo = isTransonic(tc.id, holdYd)
                    val flags  = buildList {
                        if (transo)    add("TRANSONIC_ZONE")
                        if (!dDope.isNaN()) add(classify(dDope, tier, transo, realworld))
                    }
                    allResults.add(ComboPoint(holdYd, holdMil, matchDay.elevationMil,
                        dopeMil, dPhys, dDope, k, interior, flags))
                }
            }
        }

        // ── Aggregate per distance ────────────────────────────────────────────
        val distStats = validPts.map { (holdYd, holdMil) ->
            val group    = allResults.filter { it.holdoutYd == holdYd && !it.deltaDope.isNaN() }
            val absAll   = group.map { abs(it.deltaDope) }
            val worst    = group.maxByOrNull { abs(it.deltaDope) }
                ?: return@map null
            val phMil    = physicsMap[holdYd]?.elevationMil ?: 0.0
            val transonic = isTransonic(tc.id, holdYd)

            // Per-k band stats
            val byK = group.groupBy { it.k }.mapValues { (kVal, kGroup) ->
                val kAbs = kGroup.map { abs(it.deltaDope) }
                val kInt = kGroup.filter { it.isInterior }.map { abs(it.deltaDope) }
                val kExt = kGroup.filter { !it.isInterior }.map { abs(it.deltaDope) }
                val iMax = if (kInt.isNotEmpty()) kInt.max() else Double.NaN
                val eMax = if (kExt.isNotEmpty()) kExt.max() else Double.NaN
                KBandStats(
                    k = kVal, nCombos = kGroup.size,
                    meanAbs   = kAbs.average(),
                    medianAbs = percentile(kAbs, 50.0),
                    p95Abs    = percentile(kAbs, 95.0),
                    maxAbs    = kAbs.max(),
                    nInterior       = kInt.size,
                    maxAbsInterior  = iMax,
                    nExtrap         = kExt.size,
                    maxAbsExtrap    = eMax,
                    verdictInterior = if (!iMax.isNaN()) classify(iMax, tier, transonic, realworld) else "N/A",
                    verdict         = classify(kAbs.max(), tier, transonic, realworld),
                )
            }

            val k1      = byK[1]
            val vk1Int  = k1?.verdictInterior ?: "N/A"
            val vAll    = classify(worst.deltaDope, tier, transonic, realworld)
            val flags   = buildList { if (transonic) add("TRANSONIC_ZONE") }

            DistStats(
                distYd       = holdYd,
                actualMil    = holdMil,
                physicsMil   = phMil,
                deltaPhysics = phMil - holdMil,
                nCombos      = group.size,
                meanAbs      = absAll.average(),
                medianAbs    = percentile(absAll, 50.0),
                p95Abs       = percentile(absAll, 95.0),
                maxAbs       = absAll.max(),
                maxDelta     = worst.deltaDope,
                worstK       = worst.k,
                byK          = byK,
                flags        = flags,
                verdictK1    = vk1Int,
                verdictAll   = vAll,
            )
        }.filterNotNull()

        return CaseResult(tc.id, tc.caliber, tc.loadName, tier, envPct, hasFired,
            tc.mvSource, tc.dataNotes, distStats, realworld = realworld)
    }

    // ── .17 HMR fired-impact validation ──────────────────────────────────────

    fun runFiredValidation(tc: TestCase): FiredValidationResult {
        val params       = tc.toParams()
        val trainingYds  = tc.trainingPointsYards.toSet()
        val training     = tc.dopePoints
            .filter { it.yards in trainingYds }
            .map { DopeEntryInput(distanceYards = it.yards, adjustmentMil = it.mil) }
        val firedDists   = tc.firedValidationPoints.map { it.yards }

        val dopeMap    = BallisticsEngine.interpolateDopeAll(training, firedDists, params)
        val physicsMap = BallisticsEngine.calculateTrajectory(params, firedDists)

        val pts = tc.firedValidationPoints.map { fv ->
            val yd        = fv.yards
            val actMil    = fv.mil
            val dopeMil   = dopeMap[yd]?.elevationMil ?: 0.0
            val phMil     = physicsMap[yd]?.elevationMil ?: 0.0
            val dDope     = dopeMil - actMil
            val transonic = yd > TRANSONIC_17HMR_YD
            val flags     = buildList {
                if (transonic) add("TRANSONIC_ZONE")
                add(classify(dDope, "3", transonic, tc.realworld))
            }
            FiredPoint(yd, actMil, phMil, dopeMil, phMil - actMil, dDope, flags)
        }
        return FiredValidationResult(tc.id, pts)
    }

    // ── .308 altitude/temperature sensitivity ────────────────────────────────

    fun runAltSensitivity(tc: TestCase): AltSensResult {
        val distsYd = tc.dopePoints.map { it.yards }.filter { it != tc.zeroYards }.sorted()
        val paramsA = tc.toParams(alt = 0.0,    temp = 59.0)
        val paramsB = tc.toParams(alt = 3200.0, temp = 64.0)
        val trajA   = BallisticsEngine.calculateTrajectory(paramsA, distsYd)
        val trajB   = BallisticsEngine.calculateTrajectory(paramsB, distsYd)
        val rows    = distsYd.map { yd ->
            val a = trajA[yd]?.elevationMil ?: 0.0
            val b = trajB[yd]?.elevationMil ?: 0.0
            AltSensRow(yd, a, b, b - a)
        }
        return AltSensResult(
            runALabel = "Sea level / 59°F (standard atmosphere)",
            runBLabel = "3200 ft / 64°F (Sniper Central conditions, ICAO-derived pressure)",
            rows      = rows,
        )
    }

    // ── Overall verdict ───────────────────────────────────────────────────────

    fun primaryVerdict(results: List<CaseResult>, fired: FiredValidationResult): String {
        var worst = "PASS"
        fun bump(v: String) {
            if (v == "FAIL") worst = "FAIL"
            else if (v == "INVESTIGATE" && worst != "FAIL") worst = "INVESTIGATE"
        }
        for (cr in results) {
            if (cr.skipped || cr.looSkipped) continue
            for (ds in cr.distStats) bump(ds.verdictK1)
        }
        // Fired-impact validation applies only to the calibrated .17 HMR case (realworld=false).
        for (fp in fired.firedPts) bump(classify(fp.deltaDope, "3", "TRANSONIC_ZONE" in fp.flags, false))
        return worst
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    private fun skip(tc: TestCase, tier: String, envPct: Double, hasFired: Boolean, reason: String) =
        CaseResult(tc.id, tc.caliber, tc.loadName, tier, envPct, hasFired,
            tc.mvSource, tc.dataNotes, emptyList(), realworld = tc.realworld, skipped = true, skipReason = reason)
}

// ── TestCase → BallisticsParams extension ────────────────────────────────────

private fun TestCase.toParams(
    alt:  Double? = null,
    temp: Double? = null,
    pres: Double? = null,
): BallisticsParams = BallisticsParams(
    muzzleVelocityFps = mvFps,
    bc                = bc,
    bcModel           = if (bcModel == "G7") BcModel.G7 else BcModel.G1,
    zeroDistanceYards = zeroYards,
    sightHeightIn     = sightHeightIn,
    altitudeFt        = alt  ?: conditions.altitudeFt,
    tempF             = temp ?: conditions.tempF,
    pressureHpa       = pres ?: conditions.pressureHpa,
    humidityPct       = conditions.humidityPct,
    bulletWeightGr    = bulletWeightGr,
    twistRateIn       = twistRateIn,
)
