"""Rudder angle sensor configuration.

Supports single and dual (redundant) sensor setups.  The primary sensor
is mandatory for closed-loop operation.  The redundant sensor is optional
but strongly recommended for Phase-1 marine deployments.

Sensor families supported in Phase 1:

- ``AS5048A_SPI``   — contactless magnetic absolute encoder, 14-bit, SPI.
                      Recommended for new installations (brief session 2026-05-18).
- ``POTENTIOMETER`` — resistive potentiometer 0-10 V or 4-20 mA.
                      Supported for legacy actuators.

Both sensors use a polycarbonate linkage arm to the rudder stock, giving
a proportional rotation to the rudder angle.
"""

from __future__ import annotations

from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field


class RudderSensorType(StrEnum):
    AS5048A_SPI = "as5048a_spi"
    """AMS AS5048A contactless magnetic encoder — 14-bit, SPI interface.
    No mechanical contact; requires diametrically magnetised 6 mm magnet
    on the rotating shaft.  Recommended for all new Phase-1 installations."""

    POTENTIOMETER = "potentiometer"
    """Resistive potentiometer with 0-10 V or 4-20 mA signal conditioning.
    Suitable for retrofit on existing actuators that already carry a pot."""


class RudderSensorConfig(BaseModel):
    """Configuration for one physical rudder angle sensor.

    The ``full_scale_deg`` / ``zero_offset_deg`` pair maps the electrical
    range of the sensor to real rudder degrees.  Calibration during
    commissioning (Sprint 7) will update these values in NVS.
    """

    model_config = ConfigDict(extra="forbid", validate_assignment=True)

    type: RudderSensorType
    """Physical sensor technology."""

    label: str = Field(
        default="",
        max_length=60,
        description="Free-form label, e.g. 'Primary – rudder stock' or 'Redundant – actuator arm'.",
    )

    # SPI-specific (ignored for POTENTIOMETER)
    spi_cs_gpio: int = Field(
        default=10,
        ge=0,
        le=39,
        description="ESP32 GPIO pin used as SPI chip-select for AS5048A.",
    )

    # Calibration
    full_scale_deg: float = Field(
        default=35.0,
        gt=0.0,
        le=45.0,
        description="Rudder angle (degrees) corresponding to full electrical output.",
    )
    zero_offset_deg: float = Field(
        default=0.0,
        ge=-10.0,
        le=10.0,
        description="Offset applied after scaling to correct mechanical amidships misalignment.",
    )

    # Cross-validation (used by dual-sensor arbitrator)
    divergence_alarm_deg: float = Field(
        default=3.0,
        gt=0.0,
        le=15.0,
        description="Alarm threshold: raise SENSOR_DIVERGE if |A - B| exceeds this value.",
    )
    divergence_failover_deg: float = Field(
        default=6.0,
        gt=0.0,
        le=20.0,
        description="Failover threshold: switch to the other sensor if |A - B| exceeds this.",
    )


class DualRudderSensorConfig(BaseModel):
    """Primary + optional redundant rudder sensor pair.

    When ``redundant`` is ``None`` the system operates in single-sensor
    mode (feedback_required still enforced).  When ``redundant`` is
    present the firmware enables cross-validation and automatic failover.

    Divergence thresholds are taken from the *primary* sensor's config so
    there is one canonical place to tune them.
    """

    model_config = ConfigDict(extra="forbid", validate_assignment=True)

    primary: RudderSensorConfig
    redundant: RudderSensorConfig | None = None

    @property
    def has_redundancy(self) -> bool:
        return self.redundant is not None
