"""AR-Autopilot deployment package compiler — Sprint 4.

An ``.appack`` is a ZIP archive that contains everything needed to deploy
a configured autopilot to a vessel:

    <project_id>/
    ├── manifest.json        — metadata, checksums, schema version
    ├── project.yaml         — full ProjectConfig (human-readable)
    ├── firmware_config.h    — generated C header consumed by the firmware build
    └── install_notes.txt    — human summary for the field engineer

The format is intentionally transparent (plain ZIP, readable YAML/JSON/C)
so that a field engineer can inspect the package contents without special tools.

Typical workflow
----------------
1. Integrator configures a ``ProjectConfig`` in the Studio (Project tab).
2. Clicks "Compile .appack…" → this compiler runs.
3. Studio writes the .appack to disk.
4. Field engineer transfers the .appack to the vessel.
5. Future Sprint: Studio "Deploy" tab extracts the package, calls PlatformIO
   with the generated header, and flashes the board.
"""

from __future__ import annotations

import hashlib
import json
import zipfile
from datetime import UTC, datetime
from pathlib import Path
from textwrap import dedent

from arautopilot.core.actuator_config import ActuatorType
from arautopilot.core.project_config import ProjectConfig
from arautopilot.core.sensor_config import DualRudderSensorConfig, RudderSensorType
from arautopilot.version import __version__

_SCHEMA_VERSION = "0.1.0"


class AppackCompiler:
    """Compiles a ``ProjectConfig`` into an ``.appack`` deployment archive."""

    def __init__(self, project: ProjectConfig) -> None:
        if not project.client_name.strip():
            raise ValueError("project.client_name must not be empty")
        if not project.project_name.strip():
            raise ValueError("project.project_name must not be empty")
        self._project = project

    def compile(self, output_path: Path | str) -> Path:
        """Build the ``.appack`` archive and write it to *output_path*.

        Returns the resolved output path.
        """
        out = Path(output_path)
        out.parent.mkdir(parents=True, exist_ok=True)

        project_yaml = self._project.to_yaml()
        firmware_header = self._generate_firmware_header()
        install_notes = self._generate_install_notes()
        manifest = self._build_manifest(project_yaml, firmware_header)

        prefix = str(self._project.project_id)
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zf:
            zf.writestr(f"{prefix}/manifest.json", json.dumps(manifest, indent=2))
            zf.writestr(f"{prefix}/project.yaml", project_yaml)
            zf.writestr(f"{prefix}/firmware_config.h", firmware_header)
            zf.writestr(f"{prefix}/install_notes.txt", install_notes)

        return out

    # ------------------------------------------------------------------
    # Manifest
    # ------------------------------------------------------------------

    def _build_manifest(self, project_yaml: str, firmware_header: str) -> dict:
        return {
            "appack_schema": _SCHEMA_VERSION,
            "arautopilot_version": __version__,
            "compiled_at": datetime.now(UTC).isoformat(),
            "project_id": str(self._project.project_id),
            "client_name": self._project.client_name,
            "project_name": self._project.project_name,
            "vessel_name": self._project.vessel.name,
            "vessel_type": self._project.vessel.type.value,
            "actuator_type": self._project.vessel.actuator.type.value,
            "checksums": {
                "project_yaml_sha256": _sha256(project_yaml),
                "firmware_config_h_sha256": _sha256(firmware_header),
            },
        }

    # ------------------------------------------------------------------
    # Firmware header generator
    # ------------------------------------------------------------------

    def _generate_firmware_header(self) -> str:
        p = self._project
        v = p.vessel
        a = v.actuator
        pid = v.pid
        sensors = v.sensors

        actuator_enum = _actuator_type_to_enum(a.type)
        primary_sensor_enum = _sensor_type_to_enum(sensors.primary.type)

        redundant_enabled = "1" if sensors.has_redundancy else "0"
        redundant_cs = sensors.redundant.spi_cs_gpio if sensors.redundant else 0
        redundant_sensor_enum = (
            _sensor_type_to_enum(sensors.redundant.type)
            if sensors.redundant
            else "RUDDER_SENSOR_NONE"
        )
        diverge_alarm = sensors.primary.divergence_alarm_deg
        diverge_failover = sensors.primary.divergence_failover_deg

        lines = [
            "// AUTO-GENERATED by AR-Autopilot Studio — DO NOT EDIT BY HAND.",
            f"// Project : {p.project_name}",
            f"// Client  : {p.client_name}",
            f"// Vessel  : {v.name} ({v.type.value})",
            f"// Generated: {datetime.now(UTC).isoformat()}",
            "//",
            "#pragma once",
            "",
            "// --- Project identity -------------------------------------------------",
            f'#define AR_PROJECT_ID     "{p.project_id}"',
            f'#define AR_CLIENT_NAME    "{p.client_name}"',
            f'#define AR_PROJECT_NAME   "{p.project_name}"',
            f'#define AR_VESSEL_NAME    "{v.name}"',
            "",
            "// --- Vessel kinematics ------------------------------------------------",
            f"#define AR_VESSEL_LENGTH_M         {v.length_m:.1f}f",
            f"#define AR_VESSEL_MAX_SPEED_KN     {v.max_speed_kn:.1f}f",
            "",
            "// --- Actuator ---------------------------------------------------------",
            f"#define AR_ACTUATOR_TYPE           {actuator_enum}",
            f"#define AR_RUDDER_ANGLE_LIMIT_DEG  {a.max_rudder_angle_deg:.1f}f",
            f"#define AR_ACTUATOR_DEADBAND_PCT   {a.deadband_pct:.1f}f",
            f"#define AR_ACTUATOR_MAX_RATE_DPS   {a.max_rate_dps:.1f}f",
            f"#define AR_ACTUATOR_MAX_CURRENT_A  {a.max_current_a:.1f}f",
            f"#define AR_ACTUATOR_ASYM_STBD      {a.asymmetry_stbd_over_port:.3f}f",
            "",
            "// --- Rudder sensors ---------------------------------------------------",
            f"#define AR_SENSOR_PRIMARY_TYPE     {primary_sensor_enum}",
            f"#define AR_SENSOR_PRIMARY_CS_GPIO  {sensors.primary.spi_cs_gpio}",
            f"#define AR_SENSOR_PRIMARY_FSD_DEG  {sensors.primary.full_scale_deg:.1f}f",
            f"#define AR_SENSOR_REDUNDANT        {redundant_enabled}",
            f"#define AR_SENSOR_REDUNDANT_TYPE   {redundant_sensor_enum}",
            f"#define AR_SENSOR_REDUNDANT_CS_GPIO {redundant_cs}",
            f"#define AR_SENSOR_DIVERGE_ALARM_DEG    {diverge_alarm:.1f}f",
            f"#define AR_SENSOR_DIVERGE_FAILOVER_DEG {diverge_failover:.1f}f",
            "",
            "// --- PID base gains (integrator IP — not exposed to operator) ---------",
            f"#define AR_PID_INNER_KP  {pid.inner_loop_base.kp:.4f}f",
            f"#define AR_PID_INNER_KI  {pid.inner_loop_base.ki:.4f}f",
            f"#define AR_PID_INNER_KD  {pid.inner_loop_base.kd:.4f}f",
            f"#define AR_PID_OUTER_KP  {pid.outer_loop_base.kp:.4f}f",
            f"#define AR_PID_OUTER_KI  {pid.outer_loop_base.ki:.4f}f",
            f"#define AR_PID_OUTER_KD  {pid.outer_loop_base.kd:.4f}f",
            f"#define AR_PID_ROT_FF    {pid.rot_feedforward_gain:.4f}f",
            f"#define AR_PID_DEADBAND_DEG     {pid.setpoint_deadband_deg:.2f}f",
            f"#define AR_PID_RATE_LIMIT_DPS   {pid.setpoint_rate_limit_dps:.2f}f",
            f"#define AR_PID_ANTI_WINDUP      {pid.anti_windup_limit:.2f}f",
            "",
            "// End of generated header.",
        ]
        return "\n".join(lines) + "\n"

    # ------------------------------------------------------------------
    # Install notes
    # ------------------------------------------------------------------

    def _generate_install_notes(self) -> str:
        p = self._project
        v = p.vessel
        a = v.actuator
        s = v.sensors
        return dedent(f"""\
            AR-Autopilot — Field Installation Notes
            ========================================

            Project  : {p.project_name}
            Client   : {p.client_name}
            Vessel   : {v.name}  ({v.type.value.replace("_", " ").title()})
            Generated: {datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")}

            ACTUATOR
            --------
            Type            : {a.type.value.replace("_", " ").title()}
            Model / label   : {a.name or "(not specified)"}
            Rudder limit    : ±{a.max_rudder_angle_deg:.1f}°  (software limit — verify ≤ mechanical stop)
            Deadband        : {a.deadband_pct:.1f} %
            Max slew rate   : {a.max_rate_dps:.1f} °/s
            Overcurrent     : {a.max_current_a:.1f} A trip

            SENSORS
            -------
            Primary sensor  : {s.primary.type.value.upper()}  GPIO CS={s.primary.spi_cs_gpio}
            Full-scale      : ±{s.primary.full_scale_deg:.1f}°
            Redundant sensor: {"ENABLED" if s.has_redundancy else "DISABLED"}
            {"Redundant type : " + s.redundant.type.value.upper() + "  GPIO CS=" + str(s.redundant.spi_cs_gpio) if s.redundant else ""}
            Divergence alarm    : {s.primary.divergence_alarm_deg:.1f}°
            Divergence failover : {s.primary.divergence_failover_deg:.1f}°

            COMMISSIONING STEPS (Sprint 7 wizard will automate these)
            -----------------------------------------------------------
            1. Flash firmware with this .appack (Flash Console or pio upload).
            2. Connect primary rudder sensor to SPI CS GPIO {s.primary.spi_cs_gpio}.
            {"3. Connect redundant sensor to SPI CS GPIO " + str(s.redundant.spi_cs_gpio) + "." if s.redundant else "3. (Redundant sensor not configured.)"}
            4. Verify rudder moves to ±{a.max_rudder_angle_deg:.1f}° and stops.
            5. Run Modbus client test: python tools/modbus_client_test.py
            6. Engage HEADING_HOLD and confirm heading capture.
            7. Tune PID gains via Modbus if required.

            SUPPORT
            -------
            Contact: AR Suite — alro65@gmail.com
        """)


# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------

def _sha256(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()


def _actuator_type_to_enum(t: ActuatorType) -> str:
    return {
        ActuatorType.HYDRAULIC_REVERSIBLE: "ACTUATOR_HYDRAULIC_REVERSIBLE",
        ActuatorType.ELECTRIC_DC_REVERSIBLE: "ACTUATOR_ELECTRIC_DC",
        ActuatorType.SERVOMOTOR_FEEDBACK: "ACTUATOR_SERVOMOTOR",
        ActuatorType.STERNDRIVE_ANALOG: "ACTUATOR_STERNDRIVE_ANALOG",
    }.get(t, "ACTUATOR_UNKNOWN")


def _sensor_type_to_enum(t: RudderSensorType) -> str:
    return {
        RudderSensorType.AS5048A_SPI: "RUDDER_SENSOR_AS5048A_SPI",
        RudderSensorType.POTENTIOMETER: "RUDDER_SENSOR_POTENTIOMETER",
    }.get(t, "RUDDER_SENSOR_UNKNOWN")
