"""Passwordless authentication helpers for RapidKit projects."""

from __future__ import annotations

import json
import secrets
import time
import urllib.parse
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, Mapping, MutableMapping, Optional


DEFAULTS: Dict[str, Any] = json.loads(
    """{{ module_defaults | default({}) | tojson(indent=2) }}"""
)

_FEATURE_FLAGS: tuple[str, ...] = (
    "magic_links",
    "one_time_codes",
    "rate_limiting",
    "delivery_method_controls",
)


@dataclass(slots=True)
class PasswordlessSettings:
    """Configuration properties governing passwordless issuance."""

    token_ttl_seconds: int
    token_length: int
    resend_cooldown_seconds: int
    max_attempts: int
    delivery_methods: tuple[str, ...]
    magic_link_base_url: str


@dataclass(slots=True)
class PasswordlessToken:
    """Represents a passwordless code awaiting verification."""

    token_id: str
    identifier: str
    delivery_method: str
    code: str
    issued_at: float
    expires_at: float
    attempts: int = 0
    metadata: Dict[str, Any] = field(default_factory=dict)

    def is_expired(self, *, now: Optional[float] = None) -> bool:
        return (now or time.time()) >= self.expires_at


@dataclass(slots=True)
class MagicLink:
    """Magic link payload returned to delivery services."""

    token: PasswordlessToken
    url: str


class PasswordlessRuntime:
    """Issue and verify passwordless tokens with cooldown enforcement."""

    def __init__(self, settings: PasswordlessSettings) -> None:
        self._settings = settings
        self._tokens: MutableMapping[str, PasswordlessToken] = {}
        self._active_by_recipient: MutableMapping[str, str] = {}
        self._last_sent: MutableMapping[str, float] = {}

    @property
    def settings(self) -> PasswordlessSettings:
        return self._settings

    def issue_code(
        self,
        identifier: str,
        *,
        delivery_method: str,
        metadata: Optional[Mapping[str, Any]] = None,
    ) -> PasswordlessToken:
        token = self._issue_token(identifier, delivery_method, metadata)
        return token

    def issue_magic_link(
        self,
        identifier: str,
        *,
        delivery_method: str,
        metadata: Optional[Mapping[str, Any]] = None,
    ) -> MagicLink:
        token = self._issue_token(identifier, delivery_method, metadata)
        url = self._build_magic_link(token)
        return MagicLink(token=token, url=url)

    def verify_code(
        self,
        identifier: str,
        code: str,
        *,
        delivery_method: Optional[str] = None,
    ) -> PasswordlessToken:
        key = self._resolve_recipient_key(identifier, delivery_method)
        token_id = self._active_by_recipient.get(key)
        if not token_id:
            raise ValueError("No active passwordless token for identifier")

        token = self._tokens.get(token_id)
        if token is None or token.is_expired():
            self._tokens.pop(token_id, None)
            self._active_by_recipient.pop(key, None)
            raise ValueError("Passwordless token has expired")

        if token.attempts >= self._settings.max_attempts:
            self.invalidate(token.token_id)
            raise ValueError("Maximum verification attempts exceeded")

        token.attempts += 1
        if not secrets.compare_digest(token.code, code):
            raise ValueError("Passwordless code is invalid")

        self.invalidate(token.token_id)
        return token

    def invalidate(self, token_id: str) -> None:
        token = self._tokens.pop(token_id, None)
        if not token:
            return
        key = self._recipient_key(token.identifier, token.delivery_method)
        if self._active_by_recipient.get(key) == token_id:
            self._active_by_recipient.pop(key, None)

    def purge_expired(self) -> None:
        now = time.time()
        expired: Iterable[str] = [
            token_id for token_id, token in self._tokens.items() if token.is_expired(now=now)
        ]
        for token_id in expired:
            self.invalidate(token_id)

    def metadata(self) -> Dict[str, Any]:
        return describe_passwordless(self._settings)

    def _issue_token(
        self,
        identifier: str,
        delivery_method: str,
        metadata: Optional[Mapping[str, Any]] = None,
    ) -> PasswordlessToken:
        method = delivery_method.lower()
        if method not in self._settings.delivery_methods:
            raise ValueError(f"Unsupported delivery method: {delivery_method}")

        key = self._recipient_key(identifier, method)
        now = time.time()
        last_sent = self._last_sent.get(key, 0.0)
        if now - last_sent < self._settings.resend_cooldown_seconds:
            raise ValueError("Passwordless token recently sent; try again later")

        code = self._generate_code()
        token = PasswordlessToken(
            token_id=secrets.token_urlsafe(18),
            identifier=identifier,
            delivery_method=method,
            code=code,
            issued_at=now,
            expires_at=now + self._settings.token_ttl_seconds,
            metadata=dict(metadata or {}),
        )

        self._tokens[token.token_id] = token
        self._active_by_recipient[key] = token.token_id
        self._last_sent[key] = now
        return token

    def _build_magic_link(self, token: PasswordlessToken) -> str:
        base = self._settings.magic_link_base_url.rstrip("/")
        query = urllib.parse.urlencode(
            {
                "token": token.token_id,
                "code": token.code,
                "identifier": token.identifier,
            }
        )
        return f"{base}?{query}"

    def _generate_code(self) -> str:
        alphabet = "0123456789"
        return "".join(secrets.choice(alphabet) for _ in range(self._settings.token_length))

    def _resolve_recipient_key(self, identifier: str, delivery_method: Optional[str]) -> str:
        if delivery_method:
            return self._recipient_key(identifier, delivery_method.lower())
        for method in self._settings.delivery_methods:
            key = self._recipient_key(identifier, method)
            if key in self._active_by_recipient:
                return key
        raise ValueError("No active passwordless token for identifier")

    @staticmethod
    def _recipient_key(identifier: str, delivery_method: str) -> str:
        return f"{identifier.lower()}::{delivery_method.lower()}"


def load_passwordless_settings(
    overrides: Optional[Mapping[str, Any]] = None,
) -> PasswordlessSettings:
    """Load passwordless settings with optional override support."""

    config: Dict[str, Any] = dict(DEFAULTS)
    if overrides:
        config.update(overrides)

    delivery_methods_config = list(config.get("delivery_methods", []))
    if not delivery_methods_config:
        delivery_methods_config = ["email"]

    delivery_methods = tuple(str(method).lower() for method in delivery_methods_config)
    return PasswordlessSettings(
        token_ttl_seconds=int(config.get("token_ttl_seconds", 900)),
        token_length=int(config.get("token_length", 8)),
        resend_cooldown_seconds=int(config.get("resend_cooldown_seconds", 60)),
        max_attempts=int(config.get("max_attempts", 5)),
        delivery_methods=delivery_methods,
        magic_link_base_url=str(config.get("magic_link_base_url", "https://example.com/passwordless")),
    )


def describe_passwordless(
    settings: Optional[PasswordlessSettings] = None,
) -> Dict[str, Any]:
    """Return a metadata payload describing passwordless runtime behaviour."""

    config = settings or load_passwordless_settings()
    return {
        "module": "{{ module_name | default('passwordless') }}",
        "token_ttl_seconds": config.token_ttl_seconds,
        "token_length": config.token_length,
        "resend_cooldown_seconds": config.resend_cooldown_seconds,
        "max_attempts": config.max_attempts,
        "delivery_methods": list(config.delivery_methods),
        "magic_link_base_url": config.magic_link_base_url,
        "supports_magic_links": True,
        "supports_codes": True,
        "features": list_passwordless_features(),
    }


def list_passwordless_features() -> list[str]:
    """Enumerate capabilities surfaced by the passwordless module."""

    return list(_FEATURE_FLAGS)


__all__ = [
    "PasswordlessSettings",
    "PasswordlessToken",
    "MagicLink",
    "PasswordlessRuntime",
    "load_passwordless_settings",
    "describe_passwordless",
    "list_passwordless_features",
]
