"""NUT (Network UPS Tools) service provider — polling-based implementation."""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any

from notify_bridge_core.models.events import EventType, ServiceEvent
from notify_bridge_core.providers.base import ServiceProvider, ServiceProviderType
from notify_bridge_core.templates.variables import TemplateVariableDefinition

from .client import NutClient, NutClientError
from .models import NutUpsData

_LOGGER = logging.getLogger(__name__)

# Map of status flag transitions to event types
_STATUS_EVENTS: dict[str, tuple[str, EventType, str]] = {
    # flag, event when flag appears, human description
    "OB": ("ups_on_battery", EventType.UPS_ON_BATTERY, "UPS switched to battery power"),
    "LB": ("ups_low_battery", EventType.UPS_LOW_BATTERY, "Battery charge critically low"),
    "RB": ("ups_replace_battery", EventType.UPS_REPLACE_BATTERY, "Battery needs replacement"),
    "OVER": ("ups_overload", EventType.UPS_OVERLOAD, "UPS load exceeded capacity"),
}

_RESTORE_EVENTS: dict[str, tuple[str, EventType, str]] = {
    # flag, event when flag disappears
    "OB": ("ups_online", EventType.UPS_ONLINE, "UPS back on mains power"),
    "LB": ("ups_battery_restored", EventType.UPS_BATTERY_RESTORED, "Battery charge recovered"),
}


# NUT-specific template variables
NUT_VARIABLES: list[TemplateVariableDefinition] = [
    TemplateVariableDefinition(
        name="ups_name",
        type="string",
        description="UPS device name",
        example="myups",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="ups_model",
        type="string",
        description="UPS hardware model",
        example="Smart-UPS 1500",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="ups_manufacturer",
        type="string",
        description="UPS manufacturer",
        example="APC",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="battery_charge",
        type="int",
        description="Battery charge percentage",
        example="95",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="battery_runtime",
        type="string",
        description="Estimated runtime on battery (formatted)",
        example="1:23:45",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="battery_runtime_seconds",
        type="int",
        description="Estimated runtime on battery in seconds",
        example="5025",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="ups_load",
        type="int",
        description="UPS load as percentage of rated capacity",
        example="42",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="ups_status",
        type="string",
        description="Raw UPS status flags (e.g. OL, OB, LB)",
        example="OL",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="input_voltage",
        type="string",
        description="Input voltage",
        example="230.0",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="output_voltage",
        type="string",
        description="Output voltage",
        example="230.0",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="event_description",
        type="string",
        description="Human-readable description of the event",
        example="UPS switched to battery power",
        provider_type=ServiceProviderType.NUT,
    ),
    TemplateVariableDefinition(
        name="previous_status",
        type="string",
        description="Previous UPS status flags before this event",
        example="OL",
        provider_type=ServiceProviderType.NUT,
    ),
]


class NutServiceProvider(ServiceProvider):
    """Polling-based NUT service provider.

    Connects to a NUT upsd daemon via TCP, queries UPS devices,
    and detects status transitions to generate notification events.
    """

    provider_type = ServiceProviderType.NUT

    def __init__(
        self,
        host: str,
        port: int = 3493,
        username: str | None = None,
        password: str | None = None,
        name: str = "NUT",
    ) -> None:
        self._host = host
        self._port = port
        self._username = username
        self._password = password
        self._name = name
        self._client: NutClient | None = None

    def _make_client(self) -> NutClient:
        return NutClient(self._host, self._port, self._username, self._password)

    async def connect(self) -> bool:
        try:
            self._client = self._make_client()
            await self._client.connect()
            return True
        except NutClientError as exc:
            _LOGGER.error("NUT connect failed: %s", exc)
            return False

    async def disconnect(self) -> None:
        if self._client:
            await self._client.disconnect()
            self._client = None

    async def poll(
        self,
        collection_ids: list[str],
        tracker_state: dict[str, Any],
    ) -> tuple[list[ServiceEvent], dict[str, Any]]:
        """Poll UPS devices and detect status transitions."""
        events: list[ServiceEvent] = []
        new_state: dict[str, Any] = {}

        client = self._make_client()
        try:
            await client.connect()
        except NutClientError as exc:
            _LOGGER.error("NUT poll connect failed: %s", exc)
            # Generate comms_lost events for devices that were previously reachable
            for ups_name in collection_ids:
                prev = tracker_state.get(ups_name, {})
                was_ok = prev.get("comms_ok", True)
                if was_ok:
                    events.append(self._make_event(
                        EventType.UPS_COMMS_LOST,
                        ups_name,
                        prev.get("name", ups_name),
                        {"event_description": f"Lost communication with {ups_name}",
                         "previous_status": prev.get("status", ""),
                         "ups_name": ups_name},
                    ))
                new_state[ups_name] = {
                    **prev,
                    "comms_ok": False,
                    "asset_ids": [],
                    "pending_asset_ids": [],
                    "shared": False,
                }
            return events, new_state

        try:
            for ups_name in collection_ids:
                prev = tracker_state.get(ups_name, {})
                # First-ever observation has no baseline — emitting transition
                # events for whatever flags the device happens to carry would
                # spam the user with "OB"/"LB"/"REPLBATT" alerts on every fresh
                # tracker even when nothing changed. Seed state silently and
                # skip event emission until the next poll provides a baseline.
                is_first_observation = ups_name not in tracker_state
                try:
                    variables = await client.list_var(ups_name)
                    data = NutUpsData.from_variables(ups_name, variables)

                    if is_first_observation:
                        new_state[ups_name] = {
                            "name": data.description or ups_name,
                            "status": data.status,
                            "battery_charge": data.battery_charge,
                            "comms_ok": True,
                            "asset_ids": [],
                            "pending_asset_ids": [],
                            "shared": False,
                        }
                        continue

                    # Check for comms restored
                    if not prev.get("comms_ok", True):
                        events.append(self._make_event(
                            EventType.UPS_COMMS_RESTORED,
                            ups_name,
                            data.description or ups_name,
                            self._build_extra(data, "Communication restored", prev.get("status", "")),
                        ))

                    # Detect status transitions
                    prev_flags = set(prev.get("status", "").split()) if prev.get("status") else set()
                    curr_flags = data.status_flags

                    for flag, (_, event_type, desc) in _STATUS_EVENTS.items():
                        if flag in curr_flags and flag not in prev_flags:
                            events.append(self._make_event(
                                event_type, ups_name,
                                data.description or ups_name,
                                self._build_extra(data, desc, prev.get("status", "")),
                            ))

                    for flag, (_, event_type, desc) in _RESTORE_EVENTS.items():
                        if flag not in curr_flags and flag in prev_flags:
                            events.append(self._make_event(
                                event_type, ups_name,
                                data.description or ups_name,
                                self._build_extra(data, desc, prev.get("status", "")),
                            ))

                    new_state[ups_name] = {
                        "name": data.description or ups_name,
                        "status": data.status,
                        "battery_charge": data.battery_charge,
                        "comms_ok": True,
                        "asset_ids": [],
                        "pending_asset_ids": [],
                        "shared": False,
                    }
                except NutClientError as exc:
                    _LOGGER.warning("Failed to query UPS %s: %s", ups_name, exc)
                    was_ok = prev.get("comms_ok", True)
                    if was_ok:
                        events.append(self._make_event(
                            EventType.UPS_COMMS_LOST,
                            ups_name,
                            prev.get("name", ups_name),
                            {"event_description": f"Lost communication with {ups_name}",
                             "previous_status": prev.get("status", ""),
                             "ups_name": ups_name},
                        ))
                    new_state[ups_name] = {
                        **prev,
                        "comms_ok": False,
                        "asset_ids": [],
                        "pending_asset_ids": [],
                        "shared": False,
                    }
        finally:
            await client.disconnect()

        return events, new_state

    def get_available_variables(self) -> list[TemplateVariableDefinition]:
        return list(NUT_VARIABLES)

    def get_provider_config_schema(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "host": {"type": "string", "description": "NUT server hostname or IP"},
                "port": {"type": "integer", "default": 3493, "description": "NUT server port"},
                "username": {"type": "string", "description": "upsd username (optional)"},
                "password": {"type": "string", "description": "upsd password (optional)"},
            },
            "required": ["host"],
        }

    async def list_collections(self) -> list[dict[str, Any]]:
        """List UPS devices as collections."""
        client = self._make_client()
        try:
            await client.connect()
            devices = await client.list_ups()
            return [{"id": d.name, "name": d.description or d.name} for d in devices]
        except NutClientError as exc:
            _LOGGER.error("Failed to list UPS devices: %s", exc)
            return []
        finally:
            await client.disconnect()

    async def test_connection(self) -> dict[str, Any]:
        client = self._make_client()
        try:
            await client.connect()
            devices = await client.list_ups()
            await client.disconnect()
            count = len(devices)
            names = ", ".join(d.name for d in devices[:5])
            return {
                "ok": True,
                "message": f"Connected — {count} UPS device(s) found: {names}",
            }
        except NutClientError as exc:
            return {"ok": False, "message": str(exc)}

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

    def _make_event(
        self,
        event_type: EventType,
        collection_id: str,
        collection_name: str,
        extra: dict[str, Any],
    ) -> ServiceEvent:
        return ServiceEvent(
            event_type=event_type,
            provider_type=ServiceProviderType.NUT,
            provider_name=self._name,
            collection_id=collection_id,
            collection_name=collection_name,
            timestamp=datetime.now(timezone.utc),
            extra=extra,
        )

    @staticmethod
    def _build_extra(data: NutUpsData, description: str, previous_status: str) -> dict[str, Any]:
        return {
            "ups_name": data.name,
            "ups_model": data.model,
            "ups_manufacturer": data.manufacturer,
            "battery_charge": int(data.battery_charge) if data.battery_charge is not None else None,
            "battery_runtime": data.battery_runtime_formatted,
            "battery_runtime_seconds": data.battery_runtime,
            "ups_load": int(data.ups_load) if data.ups_load is not None else None,
            "ups_status": data.status,
            "input_voltage": str(data.input_voltage) if data.input_voltage is not None else None,
            "output_voltage": str(data.output_voltage) if data.output_voltage is not None else None,
            "event_description": description,
            "previous_status": previous_status,
        }
