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

"""Backup and restore for MeshCore device configuration and messages."""

import uuid
from datetime import datetime

from gi.repository import GLib
from meshy.models import (
    ChannelMessage,
    Message,
    MessageStatus,
    NotificationLevel,
)
from meshy.protocol import (
    build_add_update_contact,
    build_device_query,
    build_set_auto_add_config,
    build_set_channel,
)


class BackupManager:
    """Stateless helper for building and applying backup dicts."""

    def export_backup(self, win) -> dict:
        si = win.self_info
        di = win.device_info

        backup = {
            "name": di.name or si.get("name", ""),
            "public_key": di.public_key or si.get("public_key", ""),
            "radio_settings": {
                "frequency": int(di.radio_freq * 1000)
                if di.radio_freq
                else si.get("radio_freq", 0) * 1000,
                "bandwidth": int(di.radio_bw * 1000)
                if di.radio_bw
                else si.get("radio_bw", 0) * 1000,
                "spreading_factor": di.radio_sf or si.get("radio_sf", 0),
                "coding_rate": di.radio_cr or si.get("radio_cr", 0),
                "tx_power": di.tx_power or si.get("tx_power", 0),
            },
            "position_settings": {
                "latitude": str(si.get("adv_lat", 0.0)),
                "longitude": str(si.get("adv_lon", 0.0)),
            },
            "other_settings": {
                "manual_add_contacts": 1 if si.get("manual_add_contacts") else 0,
                "advert_location_policy": si.get("adv_loc_policy", 0),
            },
            "auto_add_settings": win.get_auto_add_config(),
        }

        backup["channels"] = []
        for ch in win.channels:
            if not ch.is_empty:
                backup["channels"].append(
                    {
                        "name": ch.name,
                        "secret": ch.psk_hex,
                    }
                )

        backup["contacts"] = []
        for c in win.contacts:
            entry = {
                "type": c.type,
                "name": c.name,
                "public_key": c.public_key_hex,
                "flags": c.flags,
                "latitude": str(c.latitude) if c.latitude else "0.0",
                "longitude": str(c.longitude) if c.longitude else "0.0",
                "last_advert": int(c.last_seen.timestamp()) if c.last_seen else 0,
                "last_modified": int(c.device_lastmod.timestamp()) if c.device_lastmod else 0,
                "out_path": ",".join(c.path_hops) if c.path_hops else None,
            }
            backup["contacts"].append(entry)

        if win.storage:
            backup["messages"] = {}
            for c in win.contacts:
                msgs = win.storage.get_messages(c.public_key_hex, limit=10000)
                if msgs:
                    backup["messages"][c.public_key_hex] = [
                        {
                            "text": m.text,
                            "timestamp": m.timestamp.timestamp(),
                            "is_outgoing": m.is_outgoing,
                            "sender_key_hex": m.sender_key_hex,
                            "room_sender_name": m.room_sender_name,
                        }
                        for m in msgs
                    ]

            backup["channel_messages"] = {}
            for ch in win.channels:
                if ch.is_empty:
                    continue
                msgs = win.storage.get_channel_messages(ch.index, limit=10000)
                if msgs:
                    backup["channel_messages"][str(ch.index)] = [
                        {
                            "sender_name": m.sender_name,
                            "text": m.text,
                            "timestamp": m.timestamp.timestamp(),
                            "is_outgoing": m.is_outgoing,
                        }
                        for m in msgs
                    ]

            passwords = {}
            for c in win.contacts:
                pw = win.storage.get_room_password(c.public_key_hex)
                if pw:
                    passwords[c.public_key_hex] = pw
            if passwords:
                backup["room_passwords"] = passwords

            notif = {}
            for ch in win.channels:
                if not ch.is_empty and ch.notification_level != NotificationLevel.DEFAULT:
                    notif[str(ch.index)] = ch.notification_level.value
            if notif:
                backup["notification_levels"] = notif

        return backup

    def import_backup(self, win, backup: dict, restore_messages: bool = True):
        imported_contacts = 0
        imported_channels = 0

        settings_cmds = []
        name = backup.get("name", "")
        if name:
            settings_cmds.append(lambda: win.set_advert_name(name))

        radio = backup.get("radio_settings", {})
        if radio:
            freq = radio.get("frequency", 0)
            bw = radio.get("bandwidth", 0)
            sf = radio.get("spreading_factor", 0)
            cr = radio.get("coding_rate", 0)
            tx = radio.get("tx_power", 0)
            if freq and bw and sf and cr:
                settings_cmds.append(lambda: win.set_radio_params(freq, bw, sf, cr))
            if tx:
                settings_cmds.append(lambda: win.set_tx_power(tx))

        pos = backup.get("position_settings", {})
        if pos:
            try:
                lat = float(pos.get("latitude", 0))
                lon = float(pos.get("longitude", 0))
                if abs(lat) > 1e-6 or abs(lon) > 1e-6:
                    settings_cmds.append(lambda: win.set_advert_latlon(lat, lon))
            except (ValueError, TypeError):
                pass

        other = backup.get("other_settings", {})
        if other:
            loc_policy = other.get("advert_location_policy", None)
            if loc_policy is not None:
                settings_cmds.append(lambda: win.set_advert_loc_policy(loc_policy))

        auto = backup.get("auto_add_settings", {})
        if auto:
            frame = build_set_auto_add_config(
                chat=auto.get("auto_add_chat", False),
                repeater=auto.get("auto_add_repeater", False),
                room=auto.get("auto_add_room_server", False),
                sensor=auto.get("auto_add_sensor", False),
                overwrite_oldest=auto.get("overwrite_oldest", False),
                max_hops=auto.get("max_hops", 0),
            )
            settings_cmds.append(lambda: win.send_frame(frame))

        for i, cmd in enumerate(settings_cmds):
            GLib.timeout_add(i * 300, lambda c=cmd: c() or False)
        settings_delay = len(settings_cmds) * 300

        # Optimistically update device info and settings UI
        di = win._device_info
        if name:
            di.name = name
        if radio:
            if freq and bw and sf and cr:
                di.radio_freq = freq
                di.radio_bw = bw
                di.radio_sf = sf
                di.radio_cr = cr
            if tx:
                di.tx_power = tx
        if pos:
            try:
                lat_v = float(pos.get("latitude", 0))
                lon_v = float(pos.get("longitude", 0))
                if abs(lat_v) > 1e-6 or abs(lon_v) > 1e-6:
                    di.latitude = lat_v
                    di.longitude = lon_v
            except (ValueError, TypeError):
                pass
        win._settings_view.update_device_info(di)

        device_cmds = []
        channels = backup.get("channels", [])
        used = {c.index for c in win.channels if not c.is_empty}
        for ch_data in channels:
            ch_name = ch_data.get("name", "")
            secret_hex = ch_data.get("secret", "")
            if ch_name and secret_hex and len(secret_hex) == 32:
                try:
                    psk = bytes.fromhex(secret_hex)
                    already = any(c.psk_hex == secret_hex for c in win.channels if not c.is_empty)
                    if already:
                        continue
                    slot = None
                    for idx in range(win.device_info.max_channels or 8):
                        if idx not in used:
                            slot = idx
                            break
                    if slot is not None:
                        device_cmds.append(
                            lambda s=slot, n=ch_name, p=psk: win.send_frame(
                                build_set_channel(s, n, p)
                            )
                        )
                        imported_channels += 1
                        used.add(slot)
                except ValueError:
                    pass

        contacts = backup.get("contacts", [])
        contacts_by_key = {c.public_key_hex: c for c in win.contacts}
        for c_data in contacts:
            pub_key_hex = c_data.get("public_key", "")
            if not pub_key_hex or len(pub_key_hex) != 64:
                continue
            if pub_key_hex in contacts_by_key:
                continue
            try:
                pub_key = bytes.fromhex(pub_key_hex)
                c_name = c_data.get("name", "Unknown")
                contact_type = c_data.get("type", 1)
                flags = c_data.get("flags", 0)
                lat = float(c_data.get("latitude", 0))
                lon = float(c_data.get("longitude", 0))
                last_mod = c_data.get("last_modified")
                device_cmds.append(
                    lambda pk=pub_key,
                    ct=contact_type,
                    fl=flags,
                    n=c_name,
                    la=lat,
                    lo=lon,
                    lm=last_mod: win.send_frame(
                        build_add_update_contact(
                            pub_key=pk,
                            contact_type=ct,
                            flags=fl,
                            name=n,
                            lat=la if abs(la) > 1e-6 else None,
                            lon=lo if abs(lo) > 1e-6 else None,
                            last_modified=lm,
                        )
                    )
                )
                imported_contacts += 1
            except (ValueError, TypeError):
                pass

        for i, cmd in enumerate(device_cmds):
            GLib.timeout_add(settings_delay + i * 300, lambda c=cmd: c() or False)

        if restore_messages and win.storage:
            with win.storage.batch():
                for pub_key_hex, msgs in backup.get("messages", {}).items():
                    for m in msgs:
                        ts = m.get("timestamp", 0)
                        text = m.get("text", "")
                        if win.storage.has_duplicate_message(pub_key_hex, ts, text):
                            continue
                        message = Message(
                            sender_key=bytes.fromhex(m.get("sender_key_hex", pub_key_hex)),
                            text=text,
                            timestamp=datetime.fromtimestamp(ts),
                            is_outgoing=m.get("is_outgoing", False),
                            status=MessageStatus.DELIVERED,
                            message_id=str(uuid.uuid4()),
                            room_sender_name=m.get("room_sender_name", ""),
                        )
                        win.storage.save_message(pub_key_hex, message)

                for ch_idx_str, msgs in backup.get("channel_messages", {}).items():
                    ch_idx = int(ch_idx_str)
                    for m in msgs:
                        msg = ChannelMessage(
                            channel_index=ch_idx,
                            sender_name=m.get("sender_name", ""),
                            text=m.get("text", ""),
                            timestamp=datetime.fromtimestamp(m.get("timestamp", 0)),
                            is_outgoing=m.get("is_outgoing", False),
                        )
                        win.storage.save_channel_message(msg)

                for pub_key_hex, pw in backup.get("room_passwords", {}).items():
                    win.storage.save_room_password(pub_key_hex, pw)

        total_delay = settings_delay + len(device_cmds) * 300 + 1000
        GLib.timeout_add(total_delay, lambda: win._frame_handler.send_get_contacts() or False)
        GLib.timeout_add(total_delay + 500, lambda: win._fetch_channels_sequential(0) or False)
        GLib.timeout_add(total_delay + 1000, lambda: win.send_frame(build_device_query()) or False)

        return imported_contacts, imported_channels
