from __future__ import annotations

import math
from dataclasses import asdict, dataclass
from typing import Any

import numpy as np

from .specs import MAVIC_3M, mapping_geometry


@dataclass(frozen=True)
class VirtualField:
    field_id: str = "VIRTUAL-SX-12HA"
    length_m: float = 400.0
    width_m: float = 300.0
    crop: str = "synthetic rice canopy"
    region_context: str = "non-identifying virtual field inspired by Sanxing, Yilan"

    @property
    def area_m2(self) -> float:
        return self.length_m * self.width_m


@dataclass(frozen=True)
class MissionParameters:
    altitude_agl_m: float = 60.0
    requested_speed_m_s: float = 5.0
    front_overlap_pct: float = 80.0
    side_overlap_pct: float = 70.0
    capture_interval_s: float = 2.0
    turn_dwell_s: float = 2.0
    takeoff_and_landing_allowance_s: float = 180.0
    nominal_endurance_fraction_used_for_budget: float = 0.70
    sun_altitude_deg: float = 42.0

    def validate(self) -> None:
        if not 20 <= self.altitude_agl_m <= 120:
            raise ValueError("simulation altitude must be 20-120 m AGL")
        if not 0.5 <= self.requested_speed_m_s <= 10:
            raise ValueError("requested speed must be 0.5-10 m/s in this study")
        if self.capture_interval_s < MAVIC_3M.minimum_multispectral_interval_s:
            raise ValueError("capture interval is faster than the official multispectral minimum")
        if self.sun_altitude_deg < 30:
            raise ValueError("sun altitude violates DJI's FAQ recommendation for multispectral capture")
        if not 60 <= self.front_overlap_pct <= 95 or not 60 <= self.side_overlap_pct <= 95:
            raise ValueError("overlap targets must be 60-95 percent")


def plan_virtual_mission(
    field: VirtualField = VirtualField(),
    parameters: MissionParameters = MissionParameters(),
) -> dict[str, Any]:
    parameters.validate()
    geometry = mapping_geometry(
        parameters.altitude_agl_m,
        parameters.front_overlap_pct,
        parameters.side_overlap_pct,
    )
    cross = float(geometry["multispectralFootprintCrossTrackM"])
    along = float(geometry["multispectralFootprintAlongTrackM"])
    if cross >= field.width_m or along >= field.length_m:
        raise ValueError("virtual field is too small for this simplified route geometry")

    cross_usable = field.width_m - cross
    cross_intervals = max(1, math.ceil(cross_usable / float(geometry["targetLineSpacingM"])))
    line_y = np.linspace(cross / 2, field.width_m - cross / 2, cross_intervals + 1)
    actual_line_spacing = float(line_y[1] - line_y[0])

    along_usable = field.length_m - along
    along_intervals = max(1, math.ceil(along_usable / float(geometry["targetExposureSpacingM"])))
    base_x = np.linspace(along / 2, field.length_m - along / 2, along_intervals + 1)
    actual_exposure_spacing = float(base_x[1] - base_x[0])
    actual_capture_interval = actual_exposure_spacing / parameters.requested_speed_m_s
    if actual_capture_interval + 1e-9 < parameters.capture_interval_s:
        safe_speed = actual_exposure_spacing / parameters.capture_interval_s
    else:
        safe_speed = parameters.requested_speed_m_s
    actual_capture_interval = actual_exposure_spacing / safe_speed

    events: list[dict[str, Any]] = []
    route_points: list[dict[str, float | int]] = []
    elapsed = parameters.takeoff_and_landing_allowance_s / 2
    event_index = 0
    route_distance = 0.0
    last_route_point: tuple[float, float] | None = None
    for line_index, y_value in enumerate(line_y):
        x_values = base_x if line_index % 2 == 0 else base_x[::-1]
        start = (float(x_values[0]), float(y_value))
        end = (float(x_values[-1]), float(y_value))
        if last_route_point is not None:
            connector = math.dist(last_route_point, start)
            route_distance += connector
            elapsed += connector / safe_speed + parameters.turn_dwell_s
        route_points.append({"line": line_index + 1, "xM": round(start[0], 3), "yM": round(start[1], 3)})
        for within_index, x_value in enumerate(x_values):
            if within_index:
                elapsed += actual_capture_interval
            event_index += 1
            events.append({
                "captureId": f"SIM-CAP-{event_index:04d}",
                "line": line_index + 1,
                "sequenceOnLine": within_index + 1,
                "xM": round(float(x_value), 3),
                "yM": round(float(y_value), 3),
                "altitudeAglM": parameters.altitude_agl_m,
                "elapsedS": round(elapsed, 3),
                "fileCountExpected": 5,
            })
        route_distance += math.dist(start, end)
        elapsed += 0.0
        route_points.append({"line": line_index + 1, "xM": round(end[0], 3), "yM": round(end[1], 3)})
        last_route_point = end
    elapsed += parameters.takeoff_and_landing_allowance_s / 2

    actual_front = 100 * (1 - actual_exposure_spacing / along)
    actual_side = 100 * (1 - actual_line_spacing / cross)
    conservative_budget_min = MAVIC_3M.nominal_max_flight_time_min * parameters.nominal_endurance_fraction_used_for_budget
    estimated_min = elapsed / 60
    ms_payload_gib = (
        len(events) * MAVIC_3M.multispectral_camera_count * MAVIC_3M.multispectral_width_px
        * MAVIC_3M.multispectral_height_px * 2 / 1024**3
    )
    rgb_payload_gib = (
        len(events) * MAVIC_3M.rgb_width_px * MAVIC_3M.rgb_height_px * 3 / 1024**3
    )
    return {
        "schemaVersion": 1,
        "planner": "m3m_twin.rectangular-lawnmower-v1",
        "simulationOnly": True,
        "notForFlight": True,
        "flightExecutionAuthorized": False,
        "coordinateSystem": "LOCAL_SYNTHETIC_METRES",
        "field": {**asdict(field), "areaM2": field.area_m2, "areaHa": field.area_m2 / 10000},
        "parameters": asdict(parameters),
        "cameraGeometry": geometry,
        "actualSpeedMS": round(safe_speed, 6),
        "actualCaptureIntervalS": round(actual_capture_interval, 6),
        "actualFrontOverlapPct": round(actual_front, 4),
        "actualSideOverlapPct": round(actual_side, 4),
        "scanLineCount": len(line_y),
        "captureEventCount": len(events),
        "expectedImageFileCount": len(events) * 5,
        "routeDistanceM": round(route_distance, 3),
        "estimatedMissionMinutes": round(estimated_min, 3),
        "conservativePlanningBudgetMinutes": round(conservative_budget_min, 3),
        "planningReserveMinutes": round(conservative_budget_min - estimated_min, 3),
        "singleFlightPlanningGate": estimated_min <= conservative_budget_min,
        "estimatedUncompressedMsPixelPayloadGiB": round(ms_payload_gib, 3),
        "estimatedUncompressedRgb24BitPixelPayloadGiB": round(rgb_payload_gib, 3),
        "storageBoundary": "Uncompressed pixel payload is not a DJI file-size claim; actual JPEG/TIFF storage differs.",
        "routePoints": route_points,
        "captureEvents": events,
        "safetyBoundary": (
            "This local-coordinate artifact cannot be imported as an operational mission. A qualified operator must "
            "recreate and validate any real mission in DJI Pilot 2 after permissions, airspace, terrain, obstacles, "
            "people, weather, firmware, battery, and emergency checks."
        ),
    }
