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

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

from dataclasses import dataclass, field
from datetime import datetime
from enum import IntEnum, auto
from typing import Optional


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
    latitude: Optional[float] = None
    longitude: Optional[float] = None
    last_seen: Optional[datetime] = None
    device_lastmod: Optional[datetime] = None
    last_message_at: Optional[datetime] = 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.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
        # Filter out (0,0)..(1,1) range — unset or deliberately hidden
        if 0.0 <= lat <= 1.0 and 0.0 <= lon <= 1.0:
            return False
        return True

    @property
    def short_key(self) -> str:
        h = self.public_key_hex
        return f'{h[:8]}...{h[-8:]}'


@dataclass
class Message:
    sender_key: bytes
    text: str
    timestamp: datetime
    is_outgoing: bool
    is_cli: bool = False
    status: MessageStatus = MessageStatus.PENDING
    message_id: Optional[str] = None
    retry_count: int = 0
    expected_ack_hash: Optional[int] = None
    sent_at: Optional[datetime] = None
    delivered_at: Optional[datetime] = None
    trip_time_ms: Optional[int] = None
    path_length: Optional[int] = 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
        import hashlib
        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))

    @staticmethod
    def from_hex(index: int, name: str, psk_hex: str) -> 'Channel':
        psk = bytes.fromhex(psk_hex)
        if len(psk) != 16:
            raise ValueError('PSK must be 16 bytes')
        return Channel(index=index, name=name, psk=psk)


@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: Optional[float] = None
    longitude: Optional[float] = 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
