"""SQLAlchemy models for Modbus device management and telemetry.

Two tables:
  - modbus_device: deployment/configurable metadata for each polled device.
  - modbus_reading: generic telemetry rows (one per device per poll cycle).
"""

from __future__ import annotations

import uuid as _uuid
from datetime import datetime

from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON

from app.db import Base


def _uuid4_str() -> str:
    return str(_uuid.uuid4())


class ModbusDevice(Base):
    """Deployment-layer record for a Modbus slave device reachable via a TCP gateway.

    Protocol knowledge (register map, decoding rules) lives in the YAML profile
    referenced by ``profile``.  Per-device variables (network address, slave ID,
    display name, poll rate) live here.
    """

    __tablename__ = "modbus_device"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

    # Stable internal identity — used as the API path key and HA Discovery unique_id anchor.
    uuid: Mapped[str] = mapped_column(
        String(36), unique=True, nullable=False, default=_uuid4_str
    )

    # Human-readable label (may be changed; changing it triggers re-publish of HA Discovery).
    friendly_name: Mapped[str] = mapped_column(String(255), nullable=False)

    # Transport protocol — only "tcp" is supported for now.
    transport: Mapped[str] = mapped_column(String(16), nullable=False, default="tcp")

    # TCP gateway address.
    host: Mapped[str] = mapped_column(String(255), nullable=False)
    port: Mapped[int] = mapped_column(Integer, nullable=False, default=502)

    # Modbus slave address (set on the device panel; different meters on the same gateway
    # must have distinct unit_ids).
    unit_id: Mapped[int] = mapped_column(Integer, nullable=False, default=1)

    # Which YAML profile to use for register mapping and decoding (e.g. "sdm120").
    profile: Mapped[str] = mapped_column(String(64), nullable=False)

    # Polling interval in seconds.
    poll_interval_s: Mapped[int] = mapped_column(Integer, nullable=False, default=5)

    # Whether this device is included in the periodic poll sweep.
    enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)

    # Poll-status fields (updated by the poll service after each attempt).
    last_poll_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    last_poll_ok: Mapped[bool | None] = mapped_column(Boolean, nullable=True)

    # Audit timestamps.
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False
    )

    # Relationship to readings (back-reference; not loaded eagerly).
    readings: Mapped[list["ModbusReading"]] = relationship(
        back_populates="device", cascade="save-update, merge"
    )


class ModbusReading(Base):
    """Generic telemetry row: one device, one poll instant, all decoded metrics as JSON.

    ``payload`` contains the full dict of engineering values produced by the YAML profile
    decoder, e.g. ``{"voltage": 230.2, "current": 1.3, ...}``.  The profile is the key
    to interpreting which keys exist and what units they carry.
    """

    __tablename__ = "modbus_reading"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

    # FK to the device that produced this reading.
    # ON DELETE RESTRICT: prevents accidental deletion of a device that has historical data.
    device_id: Mapped[int] = mapped_column(
        ForeignKey("modbus_device.id", ondelete="RESTRICT"), nullable=False
    )

    # The UTC timestamp of when the sample was taken — real indexed column, NOT embedded in payload.
    recorded_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, index=True
    )

    # Profile-decoded engineering values as a JSON object.
    payload: Mapped[dict] = mapped_column(JSON, nullable=False)

    device: Mapped["ModbusDevice"] = relationship(back_populates="readings")


# Composite index for efficient time-range queries scoped to a single device.
Index("ix_modbus_reading_device_recorded", ModbusReading.device_id, ModbusReading.recorded_at)
