from __future__ import annotations

import json
import secrets
from dataclasses import asdict, dataclass, field
from pathlib import Path
from time import time
from typing import Any


@dataclass(slots=True)
class PairingCode:
    code: str
    created_at: float
    expires_at: float
    created_by: int | None = None
    note: str = ""
    max_uses: int = 1
    uses: int = 0

    @property
    def expired(self) -> bool:
        return self.expires_at <= time() or self.uses >= self.max_uses


@dataclass(slots=True)
class AuthGrant:
    user_id: int | None = None
    chat_id: int | None = None
    granted_at: float = field(default_factory=time)
    source: str = "pairing"
    paired_by: int | None = None
    username: str | None = None
    chat_title: str | None = None


class TelegramAccessManager:
    def __init__(self, config: dict[str, Any], state_path: str | Path | None = None):
        self.config = config
        self.state_path = Path(state_path or self._default_state_path(config)).expanduser().resolve()
        self.state_path.parent.mkdir(parents=True, exist_ok=True)
        self.state = self._load_state()

    @staticmethod
    def _default_state_path(config: dict[str, Any]) -> Path:
        data_dir = Path(config.get("data_dir") or Path(config.get("project_root", ".")) / "data")
        return data_dir / "telegram_auth.json"

    def _load_state(self) -> dict[str, Any]:
        if not self.state_path.exists():
            return {"grants": [], "pairing_codes": [], "used_bootstrap_codes": []}
        try:
            data = json.loads(self.state_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            data = {"grants": [], "pairing_codes": [], "used_bootstrap_codes": []}
        data.setdefault("grants", [])
        data.setdefault("pairing_codes", [])
        data.setdefault("used_bootstrap_codes", [])
        return data

    def save(self) -> None:
        self.state_path.write_text(json.dumps(self.state, ensure_ascii=False, indent=2), encoding="utf-8")

    def reload(self) -> dict[str, Any]:
        self.state = self._load_state()
        return self.state

    def _telegram_cfg(self) -> dict[str, Any]:
        return self.config.get("telegram", {}) or {}

    def _pairing_cfg(self) -> dict[str, Any]:
        return self._telegram_cfg().get("pairing", {}) or {}

    def owner_user_ids(self) -> set[int]:
        return {int(item) for item in self._telegram_cfg().get("owner_user_ids", [])}

    def owner_chat_ids(self) -> set[int]:
        return {int(item) for item in self._telegram_cfg().get("owner_chat_ids", [])}

    def allowed_user_ids(self) -> set[int]:
        configured = {int(item) for item in self._telegram_cfg().get("allowed_user_ids", [])}
        granted = {int(item["user_id"]) for item in self.state.get("grants", []) if item.get("user_id") is not None}
        return configured | granted | self.owner_user_ids()

    def allowed_chat_ids(self) -> set[int]:
        configured = {int(item) for item in self._telegram_cfg().get("allowed_chat_ids", [])}
        granted = {int(item["chat_id"]) for item in self.state.get("grants", []) if item.get("chat_id") is not None}
        return configured | granted | self.owner_chat_ids()

    def is_owner(self, user_id: int | None = None, chat_id: int | None = None) -> bool:
        if user_id is not None and int(user_id) in self.owner_user_ids():
            return True
        if chat_id is not None and int(chat_id) in self.owner_chat_ids():
            return True
        return False

    def is_allowed(self, user_id: int | None = None, chat_id: int | None = None) -> bool:
        if self.is_owner(user_id=user_id, chat_id=chat_id):
            return True
        if user_id is not None and int(user_id) in self.allowed_user_ids():
            return True
        if chat_id is not None and int(chat_id) in self.allowed_chat_ids():
            return True
        return False

    def is_approved(self, user_id: int | None = None, chat_id: int | None = None) -> bool:
        return self.is_allowed(user_id=user_id, chat_id=chat_id)

    def pairing_enabled(self) -> bool:
        return bool(self._pairing_cfg().get("enabled", True))

    def pairing_code_ttl_sec(self) -> int:
        pairing_cfg = self._pairing_cfg()
        return int(pairing_cfg.get("code_ttl_sec", pairing_cfg.get("ttl_seconds", 900)))

    def bootstrap_code(self) -> str:
        return str(self._pairing_cfg().get("bootstrap_code", "") or "").strip().upper()

    def bootstrap_one_time(self) -> bool:
        return bool(self._pairing_cfg().get("bootstrap_one_time", True))

    def _pairing_codes(self) -> list[PairingCode]:
        return [PairingCode(**item) for item in self.state.get("pairing_codes", [])]

    def _set_pairing_codes(self, codes: list[PairingCode]) -> None:
        self.state["pairing_codes"] = [asdict(item) for item in codes]

    def cleanup_expired_codes(self) -> int:
        current = self._pairing_codes()
        kept = [item for item in current if not item.expired]
        removed = len(current) - len(kept)
        if removed:
            self._set_pairing_codes(kept)
            self.save()
        return removed

    def create_pairing_code(
        self,
        *,
        created_by: int | None = None,
        expires_in: int | None = None,
        note: str = "",
        max_uses: int = 1,
    ) -> PairingCode:
        if not self.pairing_enabled():
            raise RuntimeError("pairing is disabled")
        self.cleanup_expired_codes()
        ttl = int(expires_in if expires_in is not None else self.pairing_code_ttl_sec())
        now = time()
        code = PairingCode(
            code=secrets.token_hex(3).upper(),
            created_at=now,
            expires_at=now + max(ttl, 1),
            created_by=created_by,
            note=note,
            max_uses=max(max_uses, 1),
        )
        codes = self._pairing_codes()
        codes.append(code)
        self._set_pairing_codes(codes)
        self.save()
        return code

    def generate_pairing_code(
        self,
        *,
        created_by: int | None = None,
        note: str | None = None,
        ttl_seconds: int | None = None,
    ) -> dict[str, Any]:
        return asdict(self.create_pairing_code(created_by=created_by, expires_in=ttl_seconds, note=note or ""))

    def _find_code(self, code: str) -> tuple[int, PairingCode] | tuple[None, None]:
        lookup = code.strip().upper()
        codes = self._pairing_codes()
        for index, item in enumerate(codes):
            if item.code.upper() == lookup:
                return index, item
        return None, None

    def list_pairing_codes(self) -> list[PairingCode]:
        self.cleanup_expired_codes()
        return self._pairing_codes()

    def pair_with_code(
        self,
        code: str,
        *,
        user_id: int | None = None,
        chat_id: int | None = None,
        username: str | None = None,
        chat_title: str | None = None,
    ) -> AuthGrant:
        self.cleanup_expired_codes()
        index, pairing_code = self._find_code(code)
        if pairing_code is None or index is None:
            raise ValueError("invalid pairing code")
        if pairing_code.expired:
            raise ValueError("pairing code expired")
        grant = AuthGrant(
            user_id=int(user_id) if user_id is not None else None,
            chat_id=int(chat_id) if chat_id is not None else None,
            granted_at=time(),
            source="pairing_code",
            paired_by=pairing_code.created_by,
            username=username,
            chat_title=chat_title,
        )
        self.add_grant(grant)
        codes = self._pairing_codes()
        codes[index].uses += 1
        if codes[index].expired:
            del codes[index]
        self._set_pairing_codes(codes)
        self.save()
        return grant

    def claim_pairing_code(self, code: str, *, user_id: int | None, chat_id: int | None) -> dict[str, Any]:
        normalized = code.strip().upper()
        if self.bootstrap_code() and normalized == self.bootstrap_code():
            if self.bootstrap_one_time() and normalized in set(self.state.get("used_bootstrap_codes", [])):
                raise ValueError("bootstrap pairing code already used")
            payload = self.approve_identity(user_id=user_id, chat_id=chat_id, via="bootstrap", code=normalized)
            if self.bootstrap_one_time():
                used = set(self.state.get("used_bootstrap_codes", []))
                used.add(normalized)
                self.state["used_bootstrap_codes"] = sorted(used)
                self.save()
            payload.setdefault("approved", True)
            payload.setdefault("via", payload.get("source", "bootstrap"))
            payload.setdefault("code", normalized)
            return payload
        grant = asdict(self.pair_with_code(code, user_id=user_id, chat_id=chat_id))
        grant.setdefault("via", grant.get("source"))
        grant["approved"] = True
        return grant

    def add_grant(self, grant: AuthGrant) -> AuthGrant:
        grants = self.state.get("grants", [])
        normalized = asdict(grant)
        normalized.setdefault("via", normalized.get("source"))
        duplicate = any(
            item.get("user_id") == normalized.get("user_id") and item.get("chat_id") == normalized.get("chat_id")
            for item in grants
        )
        if not duplicate:
            grants.append(normalized)
            self.state["grants"] = grants
            self.save()
        return grant

    def approve_identity(
        self,
        *,
        user_id: int | None = None,
        chat_id: int | None = None,
        via: str = "manual",
        paired_by: int | None = None,
        username: str | None = None,
        chat_title: str | None = None,
        code: str | None = None,
    ) -> dict[str, Any]:
        grant = AuthGrant(
            user_id=int(user_id) if user_id is not None else None,
            chat_id=int(chat_id) if chat_id is not None else None,
            source=via,
            paired_by=paired_by,
            username=username,
            chat_title=chat_title,
        )
        self.add_grant(grant)
        payload = asdict(grant)
        if code is not None:
            payload["code"] = code
        payload["approved"] = True
        return payload

    def revoke_user(self, user_id: int) -> int:
        before = len(self.state.get("grants", []))
        self.state["grants"] = [item for item in self.state.get("grants", []) if item.get("user_id") != int(user_id)]
        removed = before - len(self.state["grants"])
        if removed:
            self.save()
        return removed

    def revoke_chat(self, chat_id: int) -> int:
        before = len(self.state.get("grants", []))
        self.state["grants"] = [item for item in self.state.get("grants", []) if item.get("chat_id") != int(chat_id)]
        removed = before - len(self.state["grants"])
        if removed:
            self.save()
        return removed

    def revoke(self, *, user_id: int | None = None, chat_id: int | None = None) -> dict[str, Any]:
        removed = 0
        if user_id is not None:
            removed += self.revoke_user(int(user_id))
        if chat_id is not None:
            removed += self.revoke_chat(int(chat_id))
        return {"revoked": removed > 0, "removed": removed, "user_id": user_id, "chat_id": chat_id}

    def export_summary(self) -> dict[str, Any]:
        self.cleanup_expired_codes()
        grants = []
        for item in self.state.get("grants", []):
            payload = dict(item)
            payload.setdefault("via", payload.get("source", "manual"))
            grants.append(payload)
        pairing_codes = [asdict(item) for item in self._pairing_codes()]
        return {
            "owner_user_ids": sorted(self.owner_user_ids()),
            "owner_chat_ids": sorted(self.owner_chat_ids()),
            "allowed_user_ids": sorted(self.allowed_user_ids()),
            "allowed_chat_ids": sorted(self.allowed_chat_ids()),
            "grants": grants,
            "pending_codes": pairing_codes,
            "pairing_codes": pairing_codes,
            "state_path": str(self.state_path),
            "bootstrap_code_configured": bool(self.bootstrap_code()),
        }

    def summary(self) -> dict[str, Any]:
        return self.export_summary()

    def export_state(self) -> dict[str, Any]:
        return self.export_summary()
