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

import com.coldboreballisticsllc.scopedope.ballistics.BallisticsEngine
import com.coldboreballisticsllc.scopedope.ballistics.BallisticsParams
import com.coldboreballisticsllc.scopedope.ballistics.BcZone
import com.coldboreballisticsllc.scopedope.ballistics.DopeEntryInput
import com.coldboreballisticsllc.scopedope.ballistics.TrajectoryResult
import com.coldboreballisticsllc.scopedope.ballistics.UnitConversion
import com.coldboreballisticsllc.scopedope.domain.model.*
import javax.inject.Inject

data class CalculationInput(
    val ammo:                AmmoLoad,
    val zeroDistanceYards:   Double,
    val sightHeightIn:       Double,
    val dopeEntries:         List<DopeEntry>,
    val conditions:          RangeConditions,
    val wind:                WindConditions,
    val outputDistances:     List<Int>,          // yards
    val zeroTempF:           Double        = 59.0,
    val zeroAltitudeFt:      Double?       = null,
    val zeroPressureHpa:     Double?       = null,
    val twistRateIn:         Double        = 0.0,
    val twistDirection:      TwistDirection = TwistDirection.RH,
    val useCoriolis:         Boolean        = false,
    val coriolisLatitudeDeg: Double         = 0.0,
    val firingAzimuthDeg:    Double         = 0.0,
    val firingAngleDeg:      Double         = 0.0,
    val clickValue:          String         = "0.1_mrad",
)

data class CalculationOutput(
    val rows: List<ResultRow>,
)

// Fields that determine entry-bias values — independent of conditions, wind, distances, Coriolis.
private data class BiasKey(
    val dopeInputs:        List<DopeEntryInput>,
    val muzzleVelocityFps: Double,
    val bc:                Double,
    val bcModel:           BcModel,
    val bcZones:           List<BcZone>,
    val zeroDistanceYards: Double,
    val sightHeightIn:     Double,
    val zeroMuzzleVelocityFps: Double?,
    val zeroTempF:         Double,
    val zeroAltitudeFt:    Double?,
    val zeroPressureHpa:   Double?,
    val bulletWeightGr:    Double,
    val bulletDiameterIn:  Double,
    val bulletLengthIn:    Double,
    val formFactor:        Double,
    // Fallback conditions used for entries that have no stored atmospheric values.
    val tempF:             Double,
    val altitudeFt:        Double,
    val pressureHpa:       Double,
    val humidityPct:       Double,
)

class CalculateDopeUseCase @Inject constructor() {

    @Volatile private var cachedInput: CalculationInput? = null
    @Volatile private var cachedOutput: CalculationOutput? = null

    @Volatile private var cachedBiasKey: BiasKey? = null
    @Volatile private var cachedBiases: List<Pair<DopeEntryInput, Double>> = emptyList()

    operator fun invoke(input: CalculationInput): CalculationOutput {
        if (input == cachedInput) cachedOutput?.let { return it }
        return compute(input).also { cachedInput = input; cachedOutput = it }
    }

    private fun compute(input: CalculationInput): CalculationOutput {
        val baseMvFps = input.ammo.muzzleVelFps.toDouble()
        val sensitivity = input.ammo.mvSensitivityFpsPerF.coerceIn(-5.0, 5.0)

        // MV adjusted for today's range temperature
        val mvAtRangeDay = if (sensitivity != 0.0)
            baseMvFps + (input.conditions.temperatureF - input.ammo.mvReferenceTempF) * sensitivity
        else baseMvFps

        // MV adjusted for zero-establishment temperature (for zero-shift correction)
        val mvAtZeroDay = if (sensitivity != 0.0)
            baseMvFps + (input.zeroTempF - input.ammo.mvReferenceTempF) * sensitivity
        else baseMvFps

        val params = BallisticsParams(
            muzzleVelocityFps     = mvAtRangeDay,
            zeroMuzzleVelocityFps = mvAtZeroDay,
            zeroTempF             = input.zeroTempF,
            zeroAltitudeFt        = input.zeroAltitudeFt,
            zeroPressureHpa       = input.zeroPressureHpa,
            bc                    = input.ammo.bc,
            bcModel               = input.ammo.bcModel,
            bcZones               = input.ammo.bcZones,
            zeroDistanceYards     = input.zeroDistanceYards,
            sightHeightIn         = input.sightHeightIn,
            altitudeFt            = input.conditions.altitudeFt.toDouble(),
            tempF                 = input.conditions.temperatureF,
            pressureHpa           = input.conditions.pressureHpa,
            humidityPct           = input.conditions.humidityPct,
            windSpeedFps          = BallisticsEngine.mphToFps(input.wind.speedMph),
            windAngleDeg          = input.wind.angleDeg,
            twistRateIn           = input.twistRateIn,
            twistDirection        = input.twistDirection,
            useCoriolis           = input.useCoriolis,
            latitudeDeg           = input.coriolisLatitudeDeg,
            firingAzimuthDeg      = input.firingAzimuthDeg,
            firingAngleDeg        = input.firingAngleDeg,
            bulletWeightGr        = input.ammo.bulletWeightGr,
            bulletDiameterIn      = input.ammo.bulletDiameterMm * UnitConversion.MM_TO_IN,
            bulletLengthIn        = input.ammo.bulletLengthMm * UnitConversion.MM_TO_IN,
            formFactor            = input.ammo.formFactor,
        )

        val dopeInputs = input.dopeEntries.map {
            val c = it.conditionsAtMeasurement
            // Compute entry MV from sensitivity if entry temperature is known
            val entryMv = if (sensitivity != 0.0 && c?.temperatureF != null)
                baseMvFps + (c.temperatureF - input.ammo.mvReferenceTempF) * sensitivity
            else null
            DopeEntryInput(
                distanceYards          = it.distanceYards.toDouble(),
                adjustmentMil          = it.adjustmentMil,
                tempF                  = c?.temperatureF,
                altitudeFt             = c?.altitudeFt?.toDouble(),
                pressureHpa            = c?.pressureHpa,
                humidityPct            = c?.humidityPct,
                entryMuzzleVelocityFps = entryMv,
            )
        }

        val biasKey = BiasKey(
            dopeInputs        = dopeInputs,
            muzzleVelocityFps = params.muzzleVelocityFps,
            bc                = params.bc,
            bcModel           = params.bcModel,
            bcZones           = params.bcZones,
            zeroDistanceYards = params.zeroDistanceYards,
            sightHeightIn     = params.sightHeightIn,
            zeroMuzzleVelocityFps = params.zeroMuzzleVelocityFps,
            zeroTempF         = params.zeroTempF ?: input.zeroTempF,
            zeroAltitudeFt    = params.zeroAltitudeFt,
            zeroPressureHpa   = params.zeroPressureHpa,
            bulletWeightGr    = params.bulletWeightGr,
            bulletDiameterIn  = params.bulletDiameterIn,
            bulletLengthIn    = params.bulletLengthIn,
            formFactor        = params.formFactor,
            tempF             = params.tempF,
            altitudeFt        = params.altitudeFt,
            pressureHpa       = params.pressureHpa,
            humidityPct       = params.humidityPct,
        )
        val biases = if (biasKey == cachedBiasKey) cachedBiases
        else BallisticsEngine.computeEntryBiases(dopeInputs, params)
            .also { cachedBiasKey = biasKey; cachedBiases = it }

        val resultMap = BallisticsEngine.interpolateDopeAllFromBiases(
            precomputedBiases = biases,
            targetDistances   = input.outputDistances.map { it.toDouble() },
            params            = params,
        )
        val rows = input.outputDistances.map { dist ->
            val result  = resultMap[dist.toDouble()] ?: TrajectoryResult(0.0, 0.0)
            val elevMil = result.elevationMil
            val windMil = result.windageMil
            val sdMil   = result.spinDriftMil
            val ceMil   = result.coriolisElevMil
            val cwMil   = result.coriolisWindMil
            val ajMil   = result.aerodynamicJumpMil
            ResultRow(
                distanceYards      = dist,
                elevationMil       = elevMil,
                elevationMoa       = BallisticsEngine.milToMoa(elevMil),
                windageMil         = windMil,
                windageMoa         = BallisticsEngine.milToMoa(windMil),
                spinDriftMil       = sdMil,
                spinDriftMoa       = BallisticsEngine.milToMoa(sdMil),
                coriolisElevMil    = ceMil,
                coriolisElevMoa    = BallisticsEngine.milToMoa(ceMil),
                coriolisWindMil    = cwMil,
                coriolisWindMoa    = BallisticsEngine.milToMoa(cwMil),
                aerodynamicJumpMil = ajMil,
                aerodynamicJumpMoa = BallisticsEngine.milToMoa(ajMil),
                clicks             = toClicks(elevMil, input.clickValue),
                isSubsonic         = result.isSubsonic,
            )
        }

        return CalculationOutput(rows = rows)
    }

    private fun toClicks(elevMil: Double, clickValue: String): Int = when (clickValue) {
        "0.05_mrad" -> (elevMil / 0.05).roundToInt()
        "0.25_moa"  -> (elevMil * UnitConversion.MIL_TO_MOA / 0.25).roundToInt()
        "0.5_moa"   -> (elevMil * UnitConversion.MIL_TO_MOA / 0.5).roundToInt()
        "1_moa"     -> (elevMil * UnitConversion.MIL_TO_MOA / 1.0).roundToInt()
        else        -> (elevMil / 0.1).roundToInt()  // "0.1_mrad" default
    }
}

private fun Double.roundToInt(): Int = kotlin.math.round(this).toInt()
