# 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 (
    Message, ChannelMessage, MessageStatus, NotificationLevel,
)
from meshy.protocol import (
    build_set_auto_add_config, build_set_channel, build_add_update_contact,
    build_device_query,
)


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:
            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
