"""Total Connect Zone."""

import logging
from enum import Enum, IntFlag
from typing import TYPE_CHECKING, Any, Final

from .const import PROJECT_URL
from .exceptions import TotalConnectError

if TYPE_CHECKING:
    from .location import TotalConnectLocation

LOGGER: Final = logging.getLogger(__name__)


class ZoneStatus(IntFlag):
    """Class to represent ZoneStatus."""

    NORMAL = 0
    BYPASSED = 1
    FAULT = 2  # only returned when Sensor Events are enabled in account
    TROUBLE = 8  # is also Tampered
    TAMPER = 16  # Tamper for ProA7, see #176
    COMMUNICATION_FAILURE = 32  # see #191
    LOW_BATTERY = 64
    TRIGGERED = 256
    KNOWN = (
        NORMAL
        | BYPASSED
        | FAULT
        | TROUBLE
        | TAMPER
        | COMMUNICATION_FAILURE
        | LOW_BATTERY
        | TRIGGERED
    )


class ZoneType(Enum):
    """
    These are the "standard" Honeywell zone types. However some panels such
    as the Lynx 7000 report most security zones as zone type 3.

    https://www.alarmliquidators.com/content/Vista%2021IP-%20Programming%20Guide.pdf
    http://techresource.online/training/ssnw/honeywell/zone-types
    """

    SECURITY = 0  # for Vista, zone type 0 is not used
    ENTRY_EXIT1 = 1  # starts countdown timer #1
    ENTRY_EXIT2 = 2  # like ENTRY_EXIT1 but uses timer #2
    PERIMETER = 3  # zone type 3 usually triggers an immediate alarm...
    PROA7_SECURITY = 3  # but some panels like Lynx 7000 report timed zones as 3
    INTERIOR_FOLLOWER = 4  # inactive when armed STAY
    TROUBLE_ALARM = 5  # trouble by day, alarm by night
    SILENT_24HR = 6  # 24-hour silent alarm (often used for police/hold-up)
    AUDIBLE_24HR = 7  # 24-hour audible alarm (often used for police)
    AUX_24HR = 8  # no local siren but keypad beeps (often used for medical)
    FIRE_SMOKE = 9
    INTERIOR_DELAY = 10  # inactive when armed STAY, otherwise like ENTRY_EXIT1
    MONITOR = 12  # e.g. temperature or flood
    CARBON_MONOXIDE = 14
    PROA7_MEDICAL = 15
    FIRE_W_VERIFICATION = 16  # must trigger twice to cause an alarm
    RF_ARM_STAY = 20  # keyfob
    RF_ARM_AWAY = 21  # keyfob
    RF_DISARM = 22  # keyfob
    NO_ALARM_RESPONSE = 23  # per Vista docs
    SILENT_BURGLARY = 24  # per Vista docs
    LYRIC_KEYPAD = 50
    PROA7_GARAGE_MONITOR = 53  # issue #167
    KEYSWITCH = 77  # per Vista20P docs
    AAV_MONITOR = 81  # per Vista20P docs
    LYRIC_LOCAL_ALARM = 89

    # According to the VISTA docs, these can be programmed via downloader
    # software or from a keypad using data fields *182-*185

    VISTA_CONFIGURABLE_90 = 90
    VISTA_CONFIGURABLE_91 = 91
    VISTA_CONFIGURABLE_92 = 92
    VISTA_CONFIGURABLE_93 = 93


class TotalConnectZone:
    """Do not create instances of this class yourself."""

    def __init__(self, zone: dict[str, Any], parent_location: "TotalConnectLocation") -> None:
        """Initialize."""
        zone_id = zone.get("ZoneID")
        if zone_id is None:
            raise TotalConnectError("ZoneID is required")
        self.zoneid: int = zone_id
        self._parent_location: TotalConnectLocation = parent_location
        self.partition: int = 0
        self.status: ZoneStatus = ZoneStatus.NORMAL
        self.zone_type_id: ZoneType | int | None = None
        self.can_be_bypassed: bool | None = None
        self.battery_level: int | None = None
        self.signal_strength: int | None = None
        self.sensor_serial_number: str | None = None
        self.loop_number: int | None = None
        self.response_type: str | None = None
        self.alarm_report_state: str | None = None
        self.supervision_type: str | None = None
        self.chime_state: int | None = None
        self.device_type: int | None = None
        self._unknown_type_reported: bool = False
        self.description: str | None  # Set by _update()
        self._update(zone)

    def __str__(self) -> str:  # pragma: no cover
        """Return a string that is printable."""
        return (
            f"Zone {self.zoneid} - {self.description}\n"
            f"  Partition: {self.partition}\t\t"
            f"Zone Type: {self.zone_type_id}\t"
            f"CanBeBypassed: {self.can_be_bypassed}\t"
            f"Status: {self.status}\n"
            f"  Battery Level: {self.battery_level}\t"
            f"Signal Stength: {self.signal_strength}\n"
            f"  Serial Number: {self.sensor_serial_number}\t"
            f"Loop: {self.loop_number}\t"
            f"Response Type: {self.response_type}\n"
            f"  Supervision Type: {self.supervision_type}\t"
            f"Alarm Report State: {self.alarm_report_state}\n"
            f"  Chime State: {self.chime_state}\t"
            f"Device Type: {self.device_type}\n\n"
        )

    def is_bypassed(self) -> bool:
        """Return true if the zone is bypassed."""
        return self.status & ZoneStatus.BYPASSED > 0

    def is_faulted(self) -> bool:
        """Return true if the zone is faulted."""
        return self.status & ZoneStatus.FAULT > 0

    def is_tampered(self) -> bool:
        """Return true if zone is tampered."""
        return (self.status & ZoneStatus.TROUBLE > 0) or (self.status & ZoneStatus.TAMPER > 0)

    def is_low_battery(self) -> bool:
        """Return true if low battery."""
        return self.status & ZoneStatus.LOW_BATTERY > 0

    def is_troubled(self) -> bool:
        """Return true if zone is troubled."""
        return self.status & ZoneStatus.TROUBLE > 0

    def is_triggered(self) -> bool:
        """Return true if zone is triggered."""
        return self.status & ZoneStatus.TRIGGERED > 0

    def is_type_button(self) -> bool:
        """Return true if zone is a button."""
        # as seen so far, any security zone that cannot be bypassed
        # is a button on a panel
        if self.is_type_security() and not self.can_be_bypassed:
            return True

        if self.zone_type_id in (
            ZoneType.PROA7_MEDICAL,
            ZoneType.AUDIBLE_24HR,
            ZoneType.SILENT_24HR,
            ZoneType.RF_ARM_STAY,
            ZoneType.RF_ARM_AWAY,
            ZoneType.RF_DISARM,
        ):
            return True

        return False

    def is_type_security(self) -> bool:
        """Return true if zone type is security."""
        return self.zone_type_id in (
            ZoneType.SECURITY,
            ZoneType.ENTRY_EXIT1,
            ZoneType.ENTRY_EXIT2,
            ZoneType.PERIMETER,
            ZoneType.INTERIOR_FOLLOWER,
            ZoneType.TROUBLE_ALARM,
            ZoneType.SILENT_24HR,
            ZoneType.AUDIBLE_24HR,
            ZoneType.INTERIOR_DELAY,
            ZoneType.LYRIC_LOCAL_ALARM,
            ZoneType.PROA7_GARAGE_MONITOR,
        )

    def is_type_motion(self) -> bool:
        """Return true if zone type is motion."""
        return self.zone_type_id == ZoneType.INTERIOR_FOLLOWER

    def is_type_fire(self) -> bool:
        """Return true if zone type is fire or smoke."""
        return self.zone_type_id == ZoneType.FIRE_SMOKE

    def is_type_temperature(self) -> bool:
        """Return true if zone monitors the temperature."""
        return self.zone_type_id == ZoneType.MONITOR

    def is_type_carbon_monoxide(self) -> bool:
        """Return true if zone type is carbon monoxide."""
        return self.zone_type_id == ZoneType.CARBON_MONOXIDE

    def is_type_medical(self) -> bool:
        """Return true if zone type is medical."""
        return self.zone_type_id == ZoneType.PROA7_MEDICAL

    def is_type_keypad(self) -> bool:
        """Return true if zone type is keypad."""
        return self.zone_type_id == ZoneType.LYRIC_KEYPAD

    def _update(self, zone: dict[str, Any]) -> None:
        """Update zone state from zone data."""
        if not zone:
            raise TotalConnectError("Missing data in zone update")

        zid = zone.get("ZoneID")
        if not self.zoneid == zid:
            raise TotalConnectError("Zone ID mismatch")

        self.description = zone.get("ZoneDescription")
        # ZoneInfo gives 'PartitionID' but
        # ZoneStatusInfoWithPartitionId gives 'PartitionId'
        partition_value = zone.get("PartitionId") or zone.get("PartitionID")
        self.partition = partition_value if partition_value is not None else 0

        try:
            zone_status_value = zone.get("ZoneStatus")
            if zone_status_value is None:
                raise ValueError("ZoneStatus is required")
            status = ZoneStatus(zone_status_value)
            if status & ~ZoneStatus.KNOWN > 0:
                LOGGER.warning(
                    f"unknown ZoneStatus {zone.get('ZoneStatus')} in {zone}: please report at {PROJECT_URL}/issues"
                )
            self.status = status

        except ValueError:
            LOGGER.error(
                f"invalid ZoneStatus {zone.get('ZoneStatus')} in {zone}: please report at {PROJECT_URL}/issues"
            )
            raise TotalConnectError(f"unknown ZoneStatus in {zone}") from None

        self.can_be_bypassed = zone.get("CanBeBypassed")

        try:
            zid = zone.get("ZoneTypeId", self.zone_type_id)
            # TODO: if zid is None should we raise PartialResponseError?
            self.zone_type_id = None if zid is None else ZoneType(zid)
        except ValueError:
            # if we get an unknown ZoneType we do not raise an exception, because
            # we know there are more zone types than we have in our enum, and
            # having an unknown ZoneType doesn't keep us from doing our work
            if not self._unknown_type_reported:
                LOGGER.info(
                    f"unknown ZoneType {zid} in {zone}: please report at {PROJECT_URL}/issues"
                )
                self._unknown_type_reported = True
            self.zone_type_id = zid

        self.battery_level = zone.get("Batterylevel", self.battery_level)
        self.signal_strength = zone.get("Signalstrength", self.signal_strength)
        info = zone.get("zoneAdditionalInfo")
        if info:
            self.sensor_serial_number = info.get("SensorSerialNumber")
            self.loop_number = info.get("LoopNumber")
            self.response_type = info.get("ResponseType")
            self.alarm_report_state = info.get("AlarmReportState")
            self.supervision_type = info.get("ZoneSupervisionType")
            self.chime_state = info.get("ChimeState")
            self.device_type = info.get("DeviceType")

    def _mark_as_bypassed(self) -> None:
        """Set is_bypassed status."""
        self.status |= ZoneStatus.BYPASSED

    def bypass(self) -> None:
        """Bypass the zone."""
        if self.can_be_bypassed:
            self._parent_location.zone_bypass(self.zoneid)
