"""Create OrbitalElements for an elliptical orbit from perigee or apogee.

This module provides a convenient way to define a Highly Elliptical Orbit (HEO)
by specifying either the perigee or apogee position and the opposite radius.
The orbit is assumed to be prograde (eastward velocity).

Given perigee and apogee radii, the orbit shape is fully determined:
- Semi-major axis: a = (r_p + r_a) / 2
- Eccentricity: e = (r_a - r_p) / (r_a + r_p)

References:
    - Vallado, D. "Fundamentals of Astrodynamics and Applications", 4th ed.
"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING

import numpy as np

from gri_utils.constants import GM_EARTH
from gri_utils.conversion import ecef_to_eci

from .from_state import from_position_velocity

if TYPE_CHECKING:
    from collections.abc import Sequence

    from numpy.typing import NDArray

    from .orbital_elements import OrbitalElements

TOLERANCE = 1e-10
"""Numerical tolerance for near-zero checks to avoid division by zero."""


def from_perigee_apogee(
    xyz_ecef_m: NDArray[np.floating] | Sequence,
    opposite_radius_m: float,
    unix_s: float,
) -> OrbitalElements:
    """Create an elliptical orbit from perigee or apogee position.

    Given an Earth-fixed position (either perigee or apogee) and the radius of
    the opposite apse, creates orbital elements for an elliptical orbit that:
    1. Passes through the specified position at the specified time
    2. Has prograde (eastward) ground track velocity at perigee
    3. Has the specified opposite radius

    Whether the position is perigee or apogee is determined automatically by
    comparing the position's radius to the opposite radius:
    - If position radius > opposite radius: position is apogee
    - If position radius < opposite radius: position is perigee
    - If equal: circular orbit (use from_circular instead)

    Args:
        xyz_ecef_m: Position in ECEF coordinates in meters. Shape (3,).
            This is either the apogee or perigee position (auto-detected).
        opposite_radius_m: Distance from Earth's center to the opposite apse
            in meters. If the position is apogee, this is the perigee radius.
            If the position is perigee, this is the apogee radius.
        unix_s: Unix timestamp in seconds when the satellite is at the
            specified position.

    Returns:
        OrbitalElements for the elliptical orbit. The epoch is set to unix_s
        (the time when the satellite is at the specified position).

    Raises:
        ValueError: If the position is at Earth's center (zero radius).
        ValueError: If position radius equals opposite radius (use from_circular).

    Example:
        >>> import numpy as np
        >>> from gri_utils.orbit import from_perigee_apogee
        >>> # HEO with satellite at apogee position at time 0
        >>> # Apogee at 36,000 km altitude, perigee at 500 km altitude
        >>> earth_radius_m = 6_378_137
        >>> apogee_pos = np.array([earth_radius_m + 36_000_000, 0.0, 0.0])
        >>> perigee_radius_m = earth_radius_m + 500_000
        >>> # Auto-detects that apogee_pos is at apogee (larger radius)
        >>> elements = from_perigee_apogee(apogee_pos, perigee_radius_m, unix_s=0.0)
        >>> print(f"Eccentricity: {elements.eccentricity:.3f}")

    Note:
        The resulting orbit will have:
        - Inclination determined by the perigee latitude
        - Prograde ground track at perigee (eastward velocity component)
        - At apogee, the ground track may appear retrograde for highly
          elliptical orbits where the satellite's angular velocity is
          slower than Earth's rotation
    """
    xyz_ecef = np.asarray(xyz_ecef_m, dtype=np.float64)

    # Compute radius of the given position
    r_given = float(np.linalg.norm(xyz_ecef))

    if r_given < 1e-6:  # noqa: PLR2004
        msg = "Position cannot be at Earth's center (zero radius)"
        raise ValueError(msg)

    # Auto-detect whether position is perigee or apogee
    r_opposite = opposite_radius_m

    if abs(r_given - r_opposite) < 1.0:  # Within 1 meter = circular
        msg = (
            f"Position radius ({r_given:.0f} m) equals opposite radius "
            f"({r_opposite:.0f} m). Use from_circular() for circular orbits."
        )
        raise ValueError(msg)

    if r_given > r_opposite:
        # Position is apogee (further from Earth)
        r_apogee = r_given
        r_perigee = r_opposite
        is_apogee = True
    else:
        # Position is perigee (closer to Earth)
        r_perigee = r_given
        r_apogee = r_opposite
        is_apogee = False

    # Compute orbital parameters
    semi_major_axis = (r_perigee + r_apogee) / 2

    # Convert ECEF to ECI at the given time
    xyz_eci = ecef_to_eci(xyz_ecef, unix_s)
    r_unit = xyz_eci / r_given

    # Perigee is in the opposite direction from apogee along the line of apsides
    # If we're at apogee, perigee is at -r_unit * r_perigee
    # If we're at perigee, perigee is at +r_unit * r_perigee (current position)
    perigee_unit = -r_unit if is_apogee else r_unit

    # Compute eastward direction at perigee position
    # z x r_perigee gives the eastward direction perpendicular to z and r_perigee
    z_axis = np.array([0.0, 0.0, 1.0])
    z_cross_perigee = np.cross(z_axis, perigee_unit)
    z_cross_perigee_mag = float(np.linalg.norm(z_cross_perigee))

    if z_cross_perigee_mag < TOLERANCE:
        # Perigee is on z-axis (polar orbit), pick x-direction as eastward
        eastward_at_perigee = np.array([1.0, 0.0, 0.0])
    else:
        eastward_at_perigee = z_cross_perigee / z_cross_perigee_mag

    # Project eastward onto plane perpendicular to perigee direction
    # This gives the velocity direction at perigee
    eastward_dot_perigee = np.dot(eastward_at_perigee, perigee_unit)
    v_perigee_dir = eastward_at_perigee - eastward_dot_perigee * perigee_unit
    v_perigee_dir_mag = float(np.linalg.norm(v_perigee_dir))

    if v_perigee_dir_mag < TOLERANCE:
        # Perigee direction is parallel to eastward (shouldn't happen for non-polar)
        v_perigee_dir = eastward_at_perigee
    else:
        v_perigee_dir = v_perigee_dir / v_perigee_dir_mag

    # Angular momentum vector h = r_perigee x v_perigee (direction only)
    # This defines the orbital plane with prograde motion at perigee
    h_unit = np.cross(perigee_unit, v_perigee_dir)
    h_unit = h_unit / np.linalg.norm(h_unit)

    # Velocity direction at the given position: perpendicular to both r and h
    # v = h x r (for prograde motion)
    v_direction = np.cross(h_unit, r_unit)
    v_direction = v_direction / np.linalg.norm(v_direction)

    # Velocity magnitude at the given position using vis-viva equation:
    # v^2 = GM * (2/r - 1/a)
    v_mag = math.sqrt(GM_EARTH * (2 / r_given - 1 / semi_major_axis))

    # Compute velocity vector
    v_eci = v_direction * v_mag

    # Convert to orbital elements using position and velocity at the given time
    return from_position_velocity(xyz_eci, v_eci, unix_s)
