"""
Frame transformation benchmark task specifications.
"""

import math
import random

from benchmarks.comparative.results import AccuracyComparison
from benchmarks.comparative.tasks.base import BenchmarkTask, _flatten

R_EARTH = 6378137.0  # meters
GM = 3.986004418e14  # m^3/s^2


def _generate_frame_params(seed: int, n: int = 50) -> dict:
    """Generate ``n`` test cases with JD epochs and 6D state vectors."""
    rng = random.Random(seed)
    cases = []

    for _ in range(n):
        # Spread epochs uniformly across 2024 (JD 2460310.5 = 2024-01-01T00:00:00 UTC).
        # Don't index-walk past one year — Basilisk's pyswice ITRF93 PCK
        # coverage doesn't extend far past the data file's bundled horizon, so
        # an "idx * 7 days" spread used to break at N > ~150 with a SPICE
        # "no coverage" error in frames tasks.
        jd = 2460310.5 + rng.uniform(0.0, 365.0)

        # Generate random Keplerian elements and convert to Cartesian
        a = R_EARTH + rng.uniform(200e3, 36000e3)
        e = rng.uniform(0.001, 0.3)
        i = math.radians(rng.uniform(0.0, 180.0))
        raan = math.radians(rng.uniform(0.0, 360.0))
        argp = math.radians(rng.uniform(0.0, 360.0))
        nu = math.radians(rng.uniform(0.0, 360.0))

        p = a * (1.0 - e * e)
        r = p / (1.0 + e * math.cos(nu))
        r_pqw = [r * math.cos(nu), r * math.sin(nu), 0.0]
        v_mag = math.sqrt(GM / p)
        v_pqw = [-v_mag * math.sin(nu), v_mag * (e + math.cos(nu)), 0.0]

        cos_raan, sin_raan = math.cos(raan), math.sin(raan)
        cos_argp, sin_argp = math.cos(argp), math.sin(argp)
        cos_i, sin_i = math.cos(i), math.sin(i)

        r11 = cos_raan * cos_argp - sin_raan * sin_argp * cos_i
        r12 = -(cos_raan * sin_argp + sin_raan * cos_argp * cos_i)
        r21 = sin_raan * cos_argp + cos_raan * sin_argp * cos_i
        r22 = -(sin_raan * sin_argp - cos_raan * cos_argp * cos_i)
        r31 = sin_argp * sin_i
        r32 = cos_argp * sin_i

        x = r11 * r_pqw[0] + r12 * r_pqw[1]
        y = r21 * r_pqw[0] + r22 * r_pqw[1]
        z = r31 * r_pqw[0] + r32 * r_pqw[1]
        vx = r11 * v_pqw[0] + r12 * v_pqw[1]
        vy = r21 * v_pqw[0] + r22 * v_pqw[1]
        vz = r31 * v_pqw[0] + r32 * v_pqw[1]

        cases.append(
            {
                "jd": jd,
                "state": [x, y, z, vx, vy, vz],
            }
        )

    return {"cases": cases}


def _altitude_km_from_state(state: list[float]) -> float:
    r = math.sqrt(state[0] ** 2 + state[1] ** 2 + state[2] ** 2)
    return (r - R_EARTH) / 1000.0


class StateEciToEcefTask(BenchmarkTask):
    """Benchmark ECI to ECEF 6D state transformation."""

    @property
    def name(self) -> str:
        return "frames.state_eci_to_ecef"

    @property
    def module(self) -> str:
        return "frames"

    @property
    def description(self) -> str:
        return "Transform 6D state vector from ECI to ECEF at given epoch"

    @property
    def languages(self) -> list[str]:
        return ["python", "rust", "java", "basilisk", "gmat", "nyx"]

    def generate_params(self, seed: int) -> dict:
        return _generate_frame_params(seed)

    def generate_accuracy_samples(self, seed: int, n: int) -> dict:
        return _generate_frame_params(seed, n=n)

    def accuracy_sample_key(self, params: dict) -> dict:
        if "state" in params and isinstance(params["state"], list):
            return {"altitude_km": _altitude_km_from_state(params["state"])}
        return {}

    def compare_results(
        self,
        results_a: list,
        results_b: list,
        language_a: str,
        language_b: str,
    ) -> AccuracyComparison:
        """Compare with wider tolerance due to EOP differences."""
        flat_a = _flatten(results_a)
        flat_b = _flatten(results_b)

        if not flat_a or not flat_b:
            return AccuracyComparison(
                task_name=self.name,
                reference_language=language_a,
                comparison_language=language_b,
                max_abs_error=float("nan"),
                max_rel_error=float("nan"),
                rms_error=float("nan"),
            )

        n = min(len(flat_a), len(flat_b))
        abs_errors = [abs(flat_a[i] - flat_b[i]) for i in range(n)]
        rel_errors = [
            abs(flat_a[i] - flat_b[i]) / max(abs(flat_a[i]), 1e-30) for i in range(n)
        ]
        rms = math.sqrt(sum(e * e for e in abs_errors) / n)

        return AccuracyComparison(
            task_name=self.name,
            reference_language=language_a,
            comparison_language=language_b,
            max_abs_error=max(abs_errors),
            max_rel_error=max(rel_errors),
            rms_error=rms,
        )


class StateEcefToEciTask(BenchmarkTask):
    """Benchmark ECEF to ECI 6D state transformation."""

    @property
    def name(self) -> str:
        return "frames.state_ecef_to_eci"

    @property
    def module(self) -> str:
        return "frames"

    @property
    def description(self) -> str:
        return "Transform 6D state vector from ECEF to ECI at given epoch"

    @property
    def languages(self) -> list[str]:
        return ["python", "rust", "java", "basilisk", "gmat", "nyx"]

    def generate_params(self, seed: int) -> dict:
        return _generate_frame_params(seed)

    def generate_accuracy_samples(self, seed: int, n: int) -> dict:
        return _generate_frame_params(seed, n=n)

    def accuracy_sample_key(self, params: dict) -> dict:
        if "state" in params and isinstance(params["state"], list):
            return {"altitude_km": _altitude_km_from_state(params["state"])}
        return {}

    def compare_results(
        self,
        results_a: list,
        results_b: list,
        language_a: str,
        language_b: str,
    ) -> AccuracyComparison:
        """Compare with wider tolerance due to EOP differences."""
        flat_a = _flatten(results_a)
        flat_b = _flatten(results_b)

        if not flat_a or not flat_b:
            return AccuracyComparison(
                task_name=self.name,
                reference_language=language_a,
                comparison_language=language_b,
                max_abs_error=float("nan"),
                max_rel_error=float("nan"),
                rms_error=float("nan"),
            )

        n = min(len(flat_a), len(flat_b))
        abs_errors = [abs(flat_a[i] - flat_b[i]) for i in range(n)]
        rel_errors = [
            abs(flat_a[i] - flat_b[i]) / max(abs(flat_a[i]), 1e-30) for i in range(n)
        ]
        rms = math.sqrt(sum(e * e for e in abs_errors) / n)

        return AccuracyComparison(
            task_name=self.name,
            reference_language=language_a,
            comparison_language=language_b,
            max_abs_error=max(abs_errors),
            max_rel_error=max(rel_errors),
            rms_error=rms,
        )
