"""Known peers + pending approvals storage."""
from __future__ import annotations

import json
import os
import threading
import time
import uuid
from dataclasses import dataclass
from typing import Any

from persistence import MUGI_DIR, secure_atomic_write

from mesh.identity_hint import (
    MESH_NICKNAME_MAX_LEN,
    sanitize_mesh_intro_hint,
    sanitize_mesh_nickname,
)


MESH_DIR = os.path.join(MUGI_DIR, "mesh")
KNOWN_PEERS_PATH = os.path.join(MESH_DIR, "known_peers.json")
PENDING_HANDSHAKES_PATH = os.path.join(MESH_DIR, "pending_handshakes.json")
_PENDING_FILE_SCHEMA = 1


@dataclass
class PendingRequest:
    request_id: str
    public_key: str
    fingerprint: str
    hostname: str
    address: str
    port: int
    created_at: float
    expires_at: float
    status: str = "pending"


class PeerStore:
    def __init__(self, pending_ttl_seconds: int = 86400):
        self.pending_ttl_seconds = max(30, int(pending_ttl_seconds))
        self._pending: dict[str, PendingRequest] = {}
        self._lock = threading.RLock()
        os.makedirs(MESH_DIR, exist_ok=True)
        self._load_pending_from_disk()

    def _write_pending_disk(self) -> None:
        """Persist only active (non-expired) pending rows so restarts still show approvals."""
        with self._lock:
            now = time.time()
            rows: list[dict[str, Any]] = []
            for req in self._pending.values():
                if req.status != "pending" or req.expires_at <= now:
                    continue
                rows.append(
                    {
                        "request_id": req.request_id,
                        "public_key": req.public_key,
                        "fingerprint": req.fingerprint,
                        "hostname": req.hostname,
                        "address": req.address,
                        "port": int(req.port or 0),
                        "created_at": req.created_at,
                        "expires_at": req.expires_at,
                    }
                )
            rows.sort(key=lambda r: float(r.get("created_at") or 0))
            secure_atomic_write(
                PENDING_HANDSHAKES_PATH,
                json.dumps({"schema": _PENDING_FILE_SCHEMA, "pending": rows}, indent=2),
            )

    def _load_pending_from_disk(self) -> None:
        if not os.path.isfile(PENDING_HANDSHAKES_PATH):
            return
        try:
            with open(PENDING_HANDSHAKES_PATH, "r", encoding="utf-8") as f:
                raw = json.load(f)
        except Exception:
            return
        if not isinstance(raw, dict):
            return
        items = raw.get("pending")
        if not isinstance(items, list):
            return
        now = time.time()
        for row in items:
            if not isinstance(row, dict):
                continue
            rid = str(row.get("request_id") or "").strip()
            if not rid or rid in self._pending:
                continue
            try:
                expires_at = float(row.get("expires_at") or 0)
            except (TypeError, ValueError):
                continue
            if expires_at <= now:
                continue
            fp = str(row.get("fingerprint") or "").strip()
            if not fp:
                continue
            try:
                created_at = float(row.get("created_at") or now)
            except (TypeError, ValueError):
                created_at = now
            self._pending[rid] = PendingRequest(
                request_id=rid,
                public_key=str(row.get("public_key") or ""),
                fingerprint=fp,
                hostname=str(row.get("hostname") or ""),
                address=str(row.get("address") or ""),
                port=int(row.get("port") or 0),
                created_at=created_at,
                expires_at=expires_at,
                status="pending",
            )

    def _load(self) -> dict[str, Any]:
        if not os.path.isfile(KNOWN_PEERS_PATH):
            return {"peers": [], "blocked": [], "muted": []}
        try:
            with open(KNOWN_PEERS_PATH, "r", encoding="utf-8") as f:
                data = json.load(f)
            if not isinstance(data, dict):
                return {"peers": [], "blocked": [], "muted": []}
            data.setdefault("peers", [])
            data.setdefault("blocked", [])
            data.setdefault("muted", [])
            return data
        except Exception:
            return {"peers": [], "blocked": [], "muted": []}

    def _save(self, data: dict[str, Any]) -> None:
        with self._lock:
            secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))

    def list_peers(self) -> list[dict[str, Any]]:
        with self._lock:
            data = self._load()
            peers = data.get("peers", [])
            return peers if isinstance(peers, list) else []

    def list_blocked(self) -> list[str]:
        with self._lock:
            data = self._load()
            blocked = data.get("blocked", [])
            return blocked if isinstance(blocked, list) else []

    def is_blocked(self, fingerprint: str) -> bool:
        with self._lock:
            blocked = self._load().get("blocked") or []
            return isinstance(blocked, list) and fingerprint in blocked

    def list_muted(self) -> list[str]:
        with self._lock:
            muted = self._load().get("muted", [])
            return list(muted) if isinstance(muted, list) else []

    def is_muted(self, fingerprint: str) -> bool:
        with self._lock:
            muted = self._load().get("muted") or []
            return isinstance(muted, list) and fingerprint in muted

    def get_peer(self, fingerprint: str) -> dict[str, Any] | None:
        with self._lock:
            peers = self._load().get("peers", [])
            if not isinstance(peers, list):
                return None
            for peer in peers:
                if str(peer.get("fingerprint") or "") == fingerprint:
                    return peer
            return None

    def is_known(self, fingerprint: str) -> bool:
        return self.get_peer(fingerprint) is not None

    def add_or_update_peer(
        self,
        *,
        public_key: str,
        fingerprint: str,
        hostname: str,
        address: str = "",
        port: int = 0,
    ) -> None:
        with self._lock:
            data = self._load()
            peers = data.get("peers", [])
            if not isinstance(peers, list):
                peers = []
            now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
            updated = False
            for peer in peers:
                if str(peer.get("fingerprint") or "") == fingerprint:
                    task_trust = bool(peer.get("trusted_for_tasks", False))
                    peer["public_key"] = public_key
                    peer["hostname"] = hostname or peer.get("hostname") or ""
                    peer["address"] = address or peer.get("address") or ""
                    peer["port"] = int(port or peer.get("port") or 0)
                    peer["last_seen"] = now
                    peer["trusted"] = True
                    peer["trusted_for_tasks"] = task_trust
                    updated = True
                    break
            if not updated:
                peers.append(
                    {
                        "public_key": public_key,
                        "fingerprint": fingerprint,
                        "hostname": hostname,
                        "address": address,
                        "port": int(port or 0),
                        "first_seen": now,
                        "last_seen": now,
                        "trusted": True,
                        "trusted_for_tasks": False,
                    }
                )
            data["peers"] = peers
            data.setdefault("muted", [])
            secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))

    def trusted_for_tasks(self, fingerprint: str) -> bool:
        with self._lock:
            peers = self._load().get("peers", [])
            if not isinstance(peers, list):
                return False
            for p in peers:
                if str(p.get("fingerprint") or "") == fingerprint:
                    return bool(p.get("trusted_for_tasks", False))
            return False

    def set_trusted_for_tasks(self, fingerprint: str, allowed: bool) -> bool:
        with self._lock:
            data = self._load()
            peers = data.get("peers", [])
            if not isinstance(peers, list):
                return False
            changed = False
            for peer in peers:
                if str(peer.get("fingerprint") or "") != fingerprint:
                    continue
                peer["trusted_for_tasks"] = bool(allowed)
                changed = True
                break
            if changed:
                data["peers"] = peers
                secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))
            return changed

    def remove_peer(self, fingerprint: str) -> bool:
        with self._lock:
            data = self._load()
            peers = data.get("peers", [])
            if not isinstance(peers, list):
                return False
            new_peers = [p for p in peers if str(p.get("fingerprint") or "") != fingerprint]
            if len(new_peers) == len(peers):
                return False
            data["peers"] = new_peers
            muted = data.get("muted", [])
            if isinstance(muted, list) and fingerprint in muted:
                muted = [m for m in muted if m != fingerprint]
                data["muted"] = muted
            secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))
            return True

    def block_peer(self, fingerprint: str) -> None:
        with self._lock:
            data = self._load()
            blocked = data.get("blocked", [])
            if not isinstance(blocked, list):
                blocked = []
            if fingerprint not in blocked:
                blocked.append(fingerprint)
            data["blocked"] = blocked
            data["peers"] = [p for p in (data.get("peers") or []) if str(p.get("fingerprint") or "") != fingerprint]
            muted = data.get("muted", [])
            if isinstance(muted, list) and fingerprint in muted:
                data["muted"] = [m for m in muted if m != fingerprint]
            else:
                data.setdefault("muted", [])
            secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))

    def unblock_peer(self, fingerprint: str) -> bool:
        with self._lock:
            data = self._load()
            blocked = data.get("blocked", [])
            if not isinstance(blocked, list):
                blocked = []
            if fingerprint in blocked:
                blocked.remove(fingerprint)
                data["blocked"] = blocked
                secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))
                return True
            return False

    def clear_blocked(self) -> int:
        with self._lock:
            data = self._load()
            blocked = data.get("blocked", [])
            if not isinstance(blocked, list):
                blocked = []
            count = len(blocked)
            data["blocked"] = []
            secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))
            return count

    def mute_peer(self, fingerprint: str) -> None:
        fp = (fingerprint or "").strip()
        if not fp:
            return
        with self._lock:
            data = self._load()
            muted = data.get("muted", [])
            if not isinstance(muted, list):
                muted = []
            if fp not in muted:
                muted.append(fp)
            data["muted"] = muted
            secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))

    def unmute_peer(self, fingerprint: str) -> bool:
        fp = (fingerprint or "").strip()
        if not fp:
            return False
        with self._lock:
            data = self._load()
            muted = data.get("muted", [])
            if not isinstance(muted, list):
                muted = []
            if fp not in muted:
                return False
            data["muted"] = [m for m in muted if m != fp]
            secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))
            return True

    def create_pending(
        self,
        *,
        public_key: str,
        fingerprint: str,
        hostname: str,
        address: str,
        port: int,
    ) -> PendingRequest:
        with self._lock:
            # Reuse existing pending request for the same peer to avoid
            # requiring multiple "Allow" clicks after repeated connect attempts.
            self._prune_pending()
            for req in self._pending.values():
                if req.status != "pending":
                    continue
                if req.fingerprint == fingerprint:
                    req.hostname = hostname or req.hostname
                    req.address = address or req.address
                    req.port = int(port or req.port or 0)
                    req.expires_at = time.time() + self.pending_ttl_seconds
                    self._write_pending_disk()
                    return req
            now = time.time()
            req = PendingRequest(
                request_id=uuid.uuid4().hex,
                public_key=public_key,
                fingerprint=fingerprint,
                hostname=hostname,
                address=address,
                port=int(port or 0),
                created_at=now,
                expires_at=now + self.pending_ttl_seconds,
            )
            self._pending[req.request_id] = req
            self._write_pending_disk()
            return req

    def _prune_pending(self) -> None:
        with self._lock:
            now = time.time()
            stale = [rid for rid, req in self._pending.items() if req.expires_at <= now and req.status == "pending"]
            for rid in stale:
                self._pending[rid].status = "expired"
            if stale:
                self._write_pending_disk()

    def list_pending(self) -> list[dict[str, Any]]:
        with self._lock:
            self._prune_pending()
            out: list[dict[str, Any]] = []
            for req in self._pending.values():
                if req.status != "pending":
                    continue
                out.append(
                    {
                        "request_id": req.request_id,
                        "fingerprint": req.fingerprint,
                        "hostname": req.hostname,
                        "address": req.address,
                        "port": req.port,
                        "created_at": req.created_at,
                        "expires_at": req.expires_at,
                    }
                )
            out.sort(key=lambda x: x.get("created_at", 0))
            return out

    def resolve_pending(self, request_id: str, action: str) -> PendingRequest | None:
        with self._lock:
            self._prune_pending()
            req = self._pending.get(request_id)
            if req is None or req.status != "pending":
                return None
            act = (action or "").strip().lower()
            if act == "allow":
                req.status = "allowed"
                self.add_or_update_peer(
                    public_key=req.public_key,
                    fingerprint=req.fingerprint,
                    hostname=req.hostname,
                    address=req.address,
                    port=req.port,
                )
            else:
                req.status = "denied"
            self._write_pending_disk()
            return req

    def get_pending(self, request_id: str) -> PendingRequest | None:
        with self._lock:
            self._prune_pending()
            return self._pending.get(request_id)

    def set_peer_nickname(
        self,
        fingerprint: str,
        nickname: str,
        *,
        clear_intro_hint: bool = False,
    ) -> bool:
        """Set or clear user nickname for a known peer. Optionally clear mesh_intro_hint."""
        fp = (fingerprint or "").strip()
        if not fp:
            return False
        nick = sanitize_mesh_nickname(nickname)
        if len(nick) > MESH_NICKNAME_MAX_LEN:
            nick = nick[:MESH_NICKNAME_MAX_LEN]
        with self._lock:
            data = self._load()
            peers = data.get("peers", [])
            if not isinstance(peers, list):
                return False
            changed = False
            for peer in peers:
                if str(peer.get("fingerprint") or "") != fp:
                    continue
                if nick:
                    peer["nickname"] = nick
                elif not clear_intro_hint:
                    peer.pop("nickname", None)
                if clear_intro_hint:
                    peer.pop("mesh_intro_hint", None)
                changed = True
                break
            if changed:
                data["peers"] = peers
                secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))
            return changed

    def set_peer_mesh_intro_hint(self, fingerprint: str, hint: str) -> bool:
        """Set machine-suggested intro hint (internal). Does not overwrite non-empty hint."""
        fp = (fingerprint or "").strip()
        if not fp:
            return False
        h = sanitize_mesh_intro_hint(hint)
        if not h:
            return False
        with self._lock:
            data = self._load()
            peers = data.get("peers", [])
            if not isinstance(peers, list):
                return False
            changed = False
            for peer in peers:
                if str(peer.get("fingerprint") or "") != fp:
                    continue
                if str(peer.get("mesh_intro_hint") or "").strip():
                    return False
                peer["mesh_intro_hint"] = h
                changed = True
                break
            if changed:
                data["peers"] = peers
                secure_atomic_write(KNOWN_PEERS_PATH, json.dumps(data, indent=2))
            return changed
