# Copyright 2026, Jiri Eischmann <jiri@eischmann.cz> and the meshy contributors
# SPDX-License-Identifier: GPL-3.0-or-later

"""Data models for MeshCore entities."""

import hashlib
from dataclasses import dataclass, field
from datetime import datetime
from enum import IntEnum, auto


def battery_percent_from_mv(mv: int) -> int:
    """Estimate LiPo battery percentage from voltage.

    Piecewise linear approximation with breakpoints at
    4.2V/100%, 4.0V/90%, 3.6V/10%, 3.5V/5%, 3.0V/0%.
    """
    _TABLE = [(4200, 100), (4000, 90), (3600, 10), (3500, 5), (3000, 0)]
    if mv >= _TABLE[0][0]:
        return 100
    if mv <= _TABLE[-1][0]:
        return 0
    for i in range(len(_TABLE) - 1):
        if mv >= _TABLE[i + 1][0]:
            v_hi, p_hi = _TABLE[i]
            v_lo, p_lo = _TABLE[i + 1]
            return round(p_lo + (p_hi - p_lo) * (mv - v_lo) / (v_hi - v_lo))
    return 0


def battery_icon_name(percent: int) -> str:
    if percent >= 90:
        return "battery-level-100-symbolic"
    if percent >= 80:
        return "battery-level-80-symbolic"
    if percent >= 70:
        return "battery-level-70-symbolic"
    if percent >= 60:
        return "battery-level-60-symbolic"
    if percent >= 40:
        return "battery-level-50-symbolic"
    if percent >= 30:
        return "battery-level-30-symbolic"
    if percent >= 20:
        return "battery-level-20-symbolic"
    return "battery-level-low-symbolic"


SNR_THRESHOLDS = {
    7: (4.0, -2.0, -4.0, -6.0),
    8: (4.0, -4.0, -6.0, -8.0),
    9: (4.0, -6.0, -8.0, -10.0),
    10: (4.0, -8.0, -10.0, -13.0),
    11: (4.0, -10.0, -12.5, -15.0),
    12: (4.0, -12.5, -15.0, -18.0),
}

SIGNAL_ICONS = (
    "strength-bars-1-symbolic",
    "strength-bars-2-symbolic",
    "strength-bars-3-symbolic",
    "strength-bars-4-symbolic",
    "strength-bars-5-symbolic",
)


SIGNAL_LABELS = (
    "Excellent",
    "Good",
    "Fair",
    "Poor",
    "Very poor",
)


def snr_to_icon(snr: float, sf: int = 12) -> str:
    thresholds = SNR_THRESHOLDS.get(sf, SNR_THRESHOLDS[12])
    for i, t in enumerate(thresholds):
        if snr >= t:
            return SIGNAL_ICONS[i]
    return SIGNAL_ICONS[4]


def snr_to_label(snr: float, sf: int = 12) -> str:
    thresholds = SNR_THRESHOLDS.get(sf, SNR_THRESHOLDS[12])
    for i, t in enumerate(thresholds):
        if snr >= t:
            return SIGNAL_LABELS[i]
    return SIGNAL_LABELS[4]


class ContactType(IntEnum):
    CHAT = 1
    REPEATER = 2
    ROOM = 3
    SENSOR = 4


class MessageStatus(IntEnum):
    PENDING = auto()
    SENT = auto()
    DELIVERED = auto()
    FAILED = auto()


CONTACT_FLAG_FAVORITE = 0x01
CONTACT_FLAG_TELE_BASE = 0x02
CONTACT_FLAG_TELE_LOC = 0x04
CONTACT_FLAG_TELE_ENV = 0x08


@dataclass
class Contact:
    public_key: bytes
    name: str
    type: int = ContactType.CHAT
    flags: int = 0
    path_length: int = -1  # -1 = flood
    path: bytes = b""
    path_hash_size: int = 1  # 1, 2, or 3 bytes per hop
    force_flood: bool = False
    auto_reset_path: bool = True
    smart_route_selection: bool = False
    latitude: float | None = None
    longitude: float | None = None
    last_seen: datetime | None = None
    received_at: datetime | None = None
    device_lastmod: datetime | None = None
    last_message_at: datetime | None = None
    is_active: bool = True

    @property
    def public_key_hex(self) -> str:
        return self.public_key.hex()

    @property
    def type_label(self) -> str:
        labels = {
            ContactType.CHAT: "Chat",
            ContactType.REPEATER: "Repeater",
            ContactType.ROOM: "Room",
            ContactType.SENSOR: "Sensor",
        }
        return labels.get(self.type, "Unknown")

    @property
    def path_label(self) -> str:
        if self.smart_route_selection:
            return "Smart"
        if self.force_flood:
            return "Flood"
        if self.path_length < 0:
            return "Flood"
        if self.path_length == 0:
            return "Direct"
        return f"{self.path_length} hops"

    @property
    def path_hops(self) -> list[str]:
        """Split path bytes into individual hop prefixes using path_hash_size."""
        if self.path_length <= 0 or not self.path:
            return []
        hs = self.path_hash_size
        return [
            self.path[i : i + hs].hex().upper()
            for i in range(0, min(len(self.path), self.path_length * hs), hs)
        ]

    @property
    def is_favorite(self) -> bool:
        return bool(self.flags & CONTACT_FLAG_FAVORITE)

    @property
    def has_location(self) -> bool:
        if self.latitude is None or self.longitude is None:
            return False
        lat, lon = self.latitude, self.longitude
        if not (-90.0 <= lat <= 90.0 and -180.0 <= lon <= 180.0):
            return False
        # Filter out (0,0)..(1,1) range — unset or deliberately hidden
        return not (0.0 <= lat <= 1.0 and 0.0 <= lon <= 1.0)


@dataclass
class Message:
    sender_key: bytes
    text: str
    timestamp: datetime
    is_outgoing: bool
    is_cli: bool = False
    status: MessageStatus = MessageStatus.PENDING
    message_id: str | None = None
    retry_count: int = 0
    expected_ack_hash: int | None = None
    sent_at: datetime | None = None
    delivered_at: datetime | None = None
    trip_time_ms: int | None = None
    path_length: int | None = None
    path_bytes: bytes = b""
    room_sender_name: str = ""  # resolved sender name for room messages

    @property
    def sender_key_hex(self) -> str:
        return self.sender_key.hex()


class NotificationLevel(IntEnum):
    DEFAULT = -1
    ALL = 0
    MENTIONS = 1
    NONE = 2


@dataclass
class Channel:
    index: int
    name: str
    psk: bytes  # 16 bytes
    unread_count: int = 0
    notification_level: NotificationLevel = NotificationLevel.DEFAULT
    region: str = ""

    PUBLIC_CHANNEL_PSK = "8b3387e9c5cdea6ac9e5edbaa115cd72"

    @property
    def psk_hex(self) -> str:
        return self.psk.hex()

    @property
    def is_empty(self) -> bool:
        return not self.name and all(b == 0 for b in self.psk)

    @property
    def is_public_channel(self) -> bool:
        return self.psk_hex == self.PUBLIC_CHANNEL_PSK

    @property
    def is_hashtag_channel(self) -> bool:
        if self.is_public_channel or not self.name.startswith("#"):
            return False
        expected = hashlib.sha256(self.name.encode()).digest()[:16]
        return self.psk == expected

    @property
    def channel_type(self) -> str:
        if self.is_public_channel:
            return "Public"
        if self.is_hashtag_channel:
            return "Hashtag"
        return "Private"

    @staticmethod
    def empty(index: int) -> "Channel":
        return Channel(index=index, name="", psk=bytes(16))


@dataclass
class DeviceInfo:
    # From SELF_INFO (meshcore reader field names)
    name: str = ""  # Node name
    public_key: str = ""  # Hex string
    tx_power: int = 0
    max_tx_power: int = 0
    radio_freq: float = 0.0  # MHz
    radio_bw: float = 0.0  # kHz
    radio_sf: int = 0
    radio_cr: int = 0
    cr_old_encoding: bool = False
    # From DEVICE_INFO
    model: str = ""  # Board/manufacturer name
    ver: str = ""  # Firmware version string
    fw_build: str = ""  # Build date
    fw_ver: int = 0  # Firmware version code
    max_contacts: int = 32
    max_channels: int = 8
    path_hash_mode: int = 0  # 0=1-byte, 1=2-byte, 2=3-byte (fw v10+)
    multi_acks: int = 0  # 0=off, 1=send extra ACK (fw v10+)
    repeat: bool = False  # client repeat / off-grid mode (fw v9+)
    ble_pin: int = 0  # BLE pairing PIN (from DEVICE_INFO, fw v3+)
    # Location from SELF_INFO (adv_lat/adv_lon)
    latitude: float | None = None
    longitude: float | None = None
    # From BATT_AND_STORAGE
    battery_mv: int = 0
    storage_used_kb: int = 0
    storage_total_kb: int = 0

    @property
    def battery_percent(self) -> int:
        return battery_percent_from_mv(self.battery_mv)

    @property
    def storage_percent(self) -> int:
        if self.storage_total_kb == 0:
            return 0
        return int(self.storage_used_kb * 100 / self.storage_total_kb)


PAYLOAD_TYPENAMES = [
    "REQ",
    "RESPONSE",
    "TEXT_MSG",
    "ACK",
    "ADVERT",
    "GRP_TXT",
    "GRP_DATA",
    "ANON_REQ",
    "PATH",
    "TRACE",
    "MULTIPART",
    "CONTROL",
]

ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]


@dataclass
class RxLogEntry:
    timestamp: datetime
    snr: float
    rssi: int
    payload_type: int
    route_type: int
    path: list = field(default_factory=list)
    path_hash_size: int = 1
    pkt_hash: str = ""
    size: int = 0
    transport_from: str = ""
    transport_to: str = ""
    raw_hex: str = ""

    @property
    def payload_type_name(self) -> str:
        if 0 <= self.payload_type < len(PAYLOAD_TYPENAMES):
            return PAYLOAD_TYPENAMES[self.payload_type]
        return f"UNK({self.payload_type})"

    @property
    def route_type_name(self) -> str:
        if 0 <= self.route_type < len(ROUTE_TYPENAMES):
            return ROUTE_TYPENAMES[self.route_type]
        return f"UNK({self.route_type})"


@dataclass
class ChannelMessage:
    channel_index: int
    sender_name: str
    text: str
    timestamp: datetime
    is_outgoing: bool = False
    status: MessageStatus = MessageStatus.DELIVERED
