from __future__ import annotations

from dataclasses import asdict
from typing import Any

import numpy as np

from .mission import VirtualField
from .spectral import compute_indices, filter_connected_zones, robust_low_score


REQUIRED_BANDS = ("green", "red", "red_edge", "nir")


def simulate_capture_qc(
    events: list[dict[str, Any]],
    sun_altitude_deg: float = 42.0,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    records: list[dict[str, Any]] = []
    for index, event in enumerate(events, 1):
        failures: list[str] = []
        rtk_flag = 42 if index % 97 == 0 else 50
        blur_score = 0.42 if index % 131 == 0 else 0.93
        saturation_fraction = 0.032 if index % 113 == 0 else 0.001
        missing_band = "red_edge" if index % 173 == 0 else ""
        if rtk_flag != 50:
            failures.append("rtk_not_fixed")
        if blur_score < 0.70:
            failures.append("blur_gate")
        if saturation_fraction > 0.02:
            failures.append("saturation_gate")
        if missing_band:
            failures.append("missing_band")
        filenames_by_band = [
            ("rgb", f"{event['captureId']}_RGB.JPG"),
            ("green", f"{event['captureId']}_MS_G.TIF"),
            ("red", f"{event['captureId']}_MS_R.TIF"),
            ("red_edge", f"{event['captureId']}_MS_RE.TIF"),
            ("nir", f"{event['captureId']}_MS_NIR.TIF"),
        ]
        filenames = [filename for band, filename in filenames_by_band if band != missing_band]
        records.append({
            **event,
            "rtkFlag": rtk_flag,
            "rtkInterpretation": "FIXED" if rtk_flag == 50 else "FLOAT",
            "sunAltitudeDeg": sun_altitude_deg,
            "sunlightSensorRelative": round(0.97 + 0.02 * np.sin(index / 41), 6),
            "exposureTimeUs": 1000,
            "iso": 100,
            "blurScore": blur_score,
            "saturationFraction": saturation_fraction,
            "missingBand": missing_band,
            "filenames": "|".join(filenames),
            "fileCountObserved": len(filenames),
            "qcPassed": not failures,
            "qcFailures": "|".join(failures),
        })
    failed = [record for record in records if not record["qcPassed"]]
    reasons: dict[str, int] = {}
    for record in failed:
        for reason in str(record["qcFailures"]).split("|"):
            reasons[reason] = reasons.get(reason, 0) + 1
    summary = {
        "schemaVersion": 1,
        "eventCount": len(records),
        "acceptedEventCount": len(records) - len(failed),
        "rejectedEventCount": len(failed),
        "acceptanceRate": round((len(records) - len(failed)) / len(records), 6),
        "failureReasonCounts": reasons,
        "rtkFlagDefinitionSource": "DJI Mavic 3M Image Processing Guide",
        "gateBoundary": "Synthetic QC proves fail-closed program behaviour only; thresholds require field validation.",
    }
    return records, summary


def coverage_grid(
    records: list[dict[str, Any]],
    field: VirtualField,
    footprint_along_m: float,
    footprint_cross_m: float,
) -> np.ndarray:
    y_grid, x_grid = np.mgrid[0.5:field.width_m:1.0, 0.5:field.length_m:1.0]
    coverage = np.zeros(y_grid.shape, dtype=np.uint16)
    for record in records:
        if not record["qcPassed"]:
            continue
        inside = (
            (np.abs(x_grid - float(record["xM"])) <= footprint_along_m / 2)
            & (np.abs(y_grid - float(record["yM"])) <= footprint_cross_m / 2)
        )
        coverage += inside.astype(np.uint16)
    return coverage


def virtual_canopy(field: VirtualField, seed: int = 20260804) -> dict[str, np.ndarray]:
    rng = np.random.default_rng(seed)
    rows, cols = np.mgrid[0:int(field.width_m), 0:int(field.length_m)]
    row_texture = 0.025 * np.sin(cols / 4.6) + 0.018 * np.cos(rows / 17.0)
    broad_texture = 0.020 * np.sin((cols + rows) / 53.0)
    vigor = np.clip(0.76 + row_texture + broad_texture + rng.normal(0, 0.008, rows.shape), 0.60, 0.88)
    anomaly_a = ((rows - 92) / 28) ** 2 + ((cols - 118) / 42) ** 2 <= 1
    anomaly_b = ((rows - 216) / 23) ** 2 + ((cols - 304) / 34) ** 2 <= 1
    lower_vigor_truth = anomaly_a | anomaly_b
    intensity = 0.30 * anomaly_a + 0.24 * anomaly_b
    illumination_confounder = (rows >= 28) & (rows <= 58) & (cols >= 250) & (cols <= 345)
    noise = rng.normal(0, 0.0025, rows.shape)
    bands = {
        "green": np.clip(0.125 + 0.035 * (1 - vigor) + 0.025 * intensity + noise, 0.01, 0.95),
        "red": np.clip(0.075 + 0.070 * (1 - vigor) + 0.150 * intensity + noise, 0.01, 0.95),
        "red_edge": np.clip(0.235 + 0.155 * vigor - 0.125 * intensity + noise, 0.01, 0.95),
        "nir": np.clip(0.405 + 0.440 * vigor - 0.500 * intensity + noise, 0.01, 0.95),
    }
    for name in bands:
        bands[name] = np.where(illumination_confounder, bands[name] * 0.58, bands[name]).astype(np.float32)
    blue = np.clip(0.08 + 0.035 * (1 - vigor) + noise, 0, 1)
    rgb = np.stack([bands["red"], bands["green"], blue], axis=-1)
    rgb = np.clip((rgb / np.quantile(rgb, 0.995)) * 255, 0, 255).astype(np.uint8)
    return {
        **bands,
        "rgb": rgb,
        "lower_vigor_truth": lower_vigor_truth,
        "illumination_confounder": illumination_confounder,
    }


def simulate_dn_and_calibrate(bands: dict[str, np.ndarray], seed: int = 20260804) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray], dict[str, Any]]:
    rng = np.random.default_rng(seed + 31)
    shape = next(iter(bands.values())).shape
    rows, cols = np.mgrid[0:shape[0], 0:shape[1]]
    radius = np.sqrt(((rows - shape[0] / 2) / (shape[0] / 2)) ** 2 + ((cols - shape[1] / 2) / (shape[1] / 2)) ** 2)
    vignette = np.clip(1 - 0.16 * radius**2, 0.72, 1.0).astype(np.float32)
    black_level = 64.0
    radiometric_scale = 48000.0
    sensor_gain = 1.05
    exposure_factor = 0.96
    sunlight_factor = 0.98
    raw: dict[str, np.ndarray] = {}
    calibrated: dict[str, np.ndarray] = {}
    for band_index, name in enumerate(REQUIRED_BANDS):
        band_gain = 1 + (band_index - 1.5) * 0.007
        signal = bands[name] * radiometric_scale * sensor_gain * exposure_factor * sunlight_factor * band_gain * vignette
        dn = np.clip(np.rint(black_level + signal + rng.normal(0, 7, shape)), 0, 65535).astype(np.uint16)
        raw[name] = dn
        recovered = (dn.astype(np.float32) - black_level) / (
            radiometric_scale * sensor_gain * exposure_factor * sunlight_factor * band_gain * vignette
        )
        calibrated[name] = np.clip(recovered, 0, 1).astype(np.float32)
    metadata = {
        "schemaVersion": 1,
        "processingLevel": "pose-assisted-synthetic-relative-reflectance-mosaic",
        "measurementScale": "relative_reflectance",
        "dataProvenance": "deterministic_synthetic_digital_twin",
        "blackLevel": black_level,
        "radiometricScale": radiometric_scale,
        "sensorGain": sensor_gain,
        "exposureFactor": exposure_factor,
        "sunlightFactor": sunlight_factor,
        "vignettingCorrection": "known synthetic radial factor",
        "notDjiTerraAlgorithm": True,
        "warning": (
            "The simulator uses DJI-documented metadata concepts but does not reproduce DJI Terra's proprietary "
            "radiometric or photogrammetric processing. Raw Mavic 3M photos contain DN values, not reflectance."
        ),
    }
    return raw, calibrated, metadata


def analyse_virtual_mosaic(
    canopy: dict[str, np.ndarray],
    calibrated: dict[str, np.ndarray],
    coverage: np.ndarray,
) -> tuple[dict[str, np.ndarray], dict[str, Any]]:
    valid = coverage > 0
    indices = compute_indices(calibrated)
    score = robust_low_score(indices, valid)
    threshold = max(0.36, float(np.nanpercentile(score[valid], 93)))
    threshold_mask = valid & (score >= threshold)
    review_mask, zones = filter_connected_zones(threshold_mask, score, minimum_area_m2=100)
    truth = np.asarray(canopy["lower_vigor_truth"], dtype=bool)
    true_positive = int((review_mask & truth).sum())
    false_positive = int((review_mask & ~truth).sum())
    false_negative = int((~review_mask & truth).sum())
    precision = true_positive / max(true_positive + false_positive, 1)
    recall = true_positive / max(true_positive + false_negative, 1)
    iou = true_positive / max(true_positive + false_positive + false_negative, 1)
    result = {
        "schemaVersion": 1,
        "status": "REVIEW_REQUIRED" if zones else "NO_FLAG",
        "dataProvenance": "deterministic_synthetic_digital_twin",
        "fieldFlightExecuted": False,
        "realMavic3MFilesProcessed": False,
        "diseaseConfirmed": False,
        "agronomicCauseAssigned": False,
        "automationAuthorized": False,
        "reviewThreshold": round(threshold, 6),
        "coveredAreaFraction": round(float(valid.mean()), 6),
        "coverageMedianAcceptedFrames": round(float(np.median(coverage[valid])), 3),
        "reviewAreaM2": int(review_mask.sum()),
        "reviewZoneCount": len(zones),
        "reviewZones": [asdict(zone) for zone in zones],
        "indexSummaries": {
            name: {
                "median": round(float(np.nanmedian(value[valid])), 6),
                "p10": round(float(np.nanpercentile(value[valid], 10)), 6),
                "p90": round(float(np.nanpercentile(value[valid], 90)), 6),
            }
            for name, value in indices.items()
        },
        "syntheticFunctionalTest": {
            "knownLowerVigorAreaM2": int(truth.sum()),
            "precision": round(precision, 6),
            "recall": round(recall, 6),
            "intersectionOverUnion": round(iou, 6),
            "interpretation": (
                "These values compare outputs with simulator-injected masks and are not field accuracy, disease "
                "sensitivity, agronomic validity, or expected customer performance."
            ),
        },
        "decisionBoundary": (
            "Output is a relative ground-review priority map. It cannot determine disease, nutrient deficiency, "
            "water stress, yield, pesticide need, or food safety without independent field evidence."
        ),
    }
    arrays = {
        **indices,
        "anomaly_score": score,
        "review_mask": review_mask.astype(np.uint8),
        "coverage": coverage,
    }
    return arrays, result
