import numpy as np

# ------------------------------------------------------
#  Utility functions
# ------------------------------------------------------

def los_from_az_el(az, el):
    """Convert az/el (radians) to topocentric LOS unit vector."""
    # Local topocentric frame: x = east, y = north, z = up
    x = np.cos(el) * np.sin(az)
    y = np.cos(el) * np.cos(az)
    z = np.sin(el)
    return np.vstack((x, y, z)).T


def quadratic_fit(t, values):
    """Fit a quadratic polynomial to component values vs time."""
    A = np.vstack([t**2, t, np.ones_like(t)]).T
    coeff, *_ = np.linalg.lstsq(A, values, rcond=None)
    return coeff  # a, b, c


def eval_quadratic(coeff, t):
    a, b, c = coeff
    return a * t**2 + b * t + c


def pca_plane_projection(R):
    """
    Fit a plane to the set of 3D points R (Nx3).
    Return projected points and plane normal.
    """
    mean_r = R.mean(axis=0)
    X = R - mean_r
    C = X.T @ X / len(R)
    eigvals, eigvecs = np.linalg.eigh(C)
    # Smallest eigenvalue eigenvector = plane normal
    n = eigvecs[:, np.argmin(eigvals)]
    # project
    proj = R - np.outer((R - mean_r) @ n, n)
    return proj, n


def reject_outliers(residuals, k=3.0):
    """Return boolean mask: True = keep point."""
    med = np.median(residuals)
    mad = np.median(np.abs(residuals - med))
    if mad == 0:
        return np.ones_like(residuals, dtype=bool)
    z = 0.6745 * (residuals - med) / mad
    return np.abs(z) < k


# ------------------------------------------------------
#  Main preprocessing pipeline
# ------------------------------------------------------

def preprocess_for_iod(t, az, el, rho, r_site_eci, el_min_deg=20.0):
    """
    Implements steps 2 → 6 from the earlier list.
    Returns: r1, r2, r3 (3 clean ECI position vectors)
    """

    # -------------------------
    # Step 2: Drop low elevation
    # -------------------------
    mask = (el * 180/np.pi) > el_min_deg
    t = t[mask]
    az = az[mask]
    el = el[mask]
    rho = rho[mask]
    r_site_eci = r_site_eci[mask]

    # -------------------------
    # Build raw ECI positions
    # -------------------------
    los = los_from_az_el(az, el)
    R = r_site_eci + rho[:, None] * los   # Nx3

    # -------------------------
    # Step 3: Quadratic fit on each component
    # -------------------------
    t0 = t - t.mean()  # center time for numerical stability

    coeff_x = quadratic_fit(t0, R[:, 0])
    coeff_y = quadratic_fit(t0, R[:, 1])
    coeff_z = quadratic_fit(t0, R[:, 2])

    R_fit = np.column_stack([
        eval_quadratic(coeff_x, t0),
        eval_quadratic(coeff_y, t0),
        eval_quadratic(coeff_z, t0)
    ])

    # -------------------------
    # Step 4: PCA plane projection
    # -------------------------
    R_proj, normal = pca_plane_projection(R_fit)

    # -------------------------
    # Step 5: Outlier rejection
    # -------------------------
    res = np.linalg.norm(R_proj - R_fit, axis=1)
    keep = reject_outliers(res, k=3.0)

    t = t[keep]
    R_proj = R_proj[keep]

    # -------------------------
    # Step 6: Pick 3 clean points around max elevation
    # -------------------------
    # Use the time closest to mid-pass (middle sample)
    mid_idx = len(t) // 2
    t2 = t[mid_idx]

    # Choose t1 and t3 symmetrically by time spacing
    # You may tune dt_window depending on pass length
    dt_window = (t[-1] - t[0]) * 0.25  # quarter-span on each side

    t1 = t2 - dt_window
    t3 = t2 + dt_window

    # interpolate smoothed/proj positions at t1, t2, t3
    def interp_R(t_query):
        return np.column_stack([
            np.interp(t_query, t, R_proj[:, 0]),
            np.interp(t_query, t, R_proj[:, 1]),
            np.interp(t_query, t, R_proj[:, 2])
        ])

    r1 = interp_R(t1)[0]
    r2 = interp_R(t2)[0]
    r3 = interp_R(t3)[0]

    return r1, r2, r3
