"""
Dedicated LNURL service for Lightning Wallet Connections.

Runs as ``nwc-lnurl.service`` on 127.0.0.1:8181 (loopback only).
Caddy proxies the public Lightning Address domain's LNURL routes to this port.

Routes:
  GET /.well-known/lnurlp/{alias}
  GET /lnurlp/{alias}/callback?amount=<msat>

All error responses are safe for public consumption — raw Alby Hub bodies
and internal credentials are never returned to callers.
"""

from __future__ import annotations

import json
import logging
import os
import re
import time
import urllib.parse
from collections import defaultdict
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import TYPE_CHECKING

from . import nwc_hub_manager as _mgr_mod
from . import nwc_audit as _audit_mod

if TYPE_CHECKING:
    from .nwc_hub_manager import AlbyHubManager

logger = logging.getLogger(__name__)

# ── Configuration ─────────────────────────────────────────────────

LNURL_BIND_HOST = "127.0.0.1"
LNURL_PORT = int(os.environ.get("NWC_LNURL_PORT", "8181"))
DOMAIN_FILE = "/var/lib/domains/lightning"

NWC_ALIAS_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,31}$")

# Rate limiting configuration
RATE_LIMIT_WINDOW_SEC = 60
RATE_LIMIT_MAX_REQUESTS = 30
_rate_limit_buckets: dict[str, list[float]] = defaultdict(list)

# ── Helpers ───────────────────────────────────────────────────────


def _read_domain() -> str | None:
    try:
        with open(DOMAIN_FILE, "r") as fh:
            raw = fh.read(256).strip().lower()
        # Strict FQDN validation: must be a valid hostname with at least one dot
        # Reject localhost, IP addresses, and single-label names
        if not re.match(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$", raw):
            return None
        # Explicitly reject local/reserved names
        if raw in {"localhost", "localhost.localdomain", "local"}:
            return None
        return raw
    except OSError:
        pass
    return None


def _check_rate_limit(client_ip: str) -> bool:
    """Check and update rate limit bucket for client IP. Returns True if allowed."""
    now = time.monotonic()
    bucket = _rate_limit_buckets[client_ip]
    # Prune old entries
    cutoff = now - RATE_LIMIT_WINDOW_SEC
    while bucket and bucket[0] < cutoff:
        bucket.pop(0)
    if len(bucket) >= RATE_LIMIT_MAX_REQUESTS:
        return False
    bucket.append(now)
    return True


def _lnurl_discovery(alias: str, manager: "AlbyHubManager", client_ip: str = "") -> tuple[dict, int]:
    alias = alias.strip().lower()
    if not NWC_ALIAS_RE.match(alias):
        return {"status": "ERROR", "reason": "Unknown Lightning Address alias"}, 404

    domain = _read_domain()
    if not domain:
        return {"status": "ERROR", "reason": "Lightning domain is not configured"}, 503

    try:
        app = manager.find_app_by_alias(alias)
    except _mgr_mod.AlbyHubError:
        return {"status": "ERROR", "reason": "Service temporarily unavailable"}, 503

    if app is None:
        return {"status": "ERROR", "reason": "Unknown Lightning Address alias"}, 404

    meta = _mgr_mod.AlbyHubManager._parse_metadata(app.get("metadata"))
    min_sendable = int(
        meta.get("lnurl_min_sendable_msat", _mgr_mod.NWC_MIN_SENDABLE_MSAT)
    )
    max_sendable = int(
        meta.get("lnurl_max_sendable_msat", _mgr_mod.NWC_MAX_SENDABLE_MSAT)
    )

    callback_alias = urllib.parse.quote(alias, safe="")
    callback = f"https://{domain}/lnurlp/{callback_alias}/callback"
    description = meta.get("lnurl_description") or f"Pay {alias}"
    metadata = json.dumps([["text/plain", description]], separators=(",", ":"))

    # Audit log: LNURL discovery
    _audit_mod.audit_log(
        "lnurl_discovery",
        alias=alias,
        domain=domain,
        client_ip=client_ip,
        min_sendable_msat=min_sendable,
        max_sendable_msat=max_sendable,
    )

    return {
        "tag": "payRequest",
        "callback": callback,
        "minSendable": min_sendable,
        "maxSendable": max_sendable,
        "metadata": metadata,
        "commentAllowed": 0,
    }, 200


def _lnurl_callback(
    alias: str, amount_str: str | None, manager: "AlbyHubManager", client_ip: str = ""
) -> tuple[dict, int]:
    payload, status_code = _lnurl_discovery(alias, manager, client_ip)
    if status_code != 200:
        return payload, status_code

    if amount_str is None:
        return {"status": "ERROR", "reason": "Missing amount parameter"}, 400
    if not re.match(r"^\d+$", amount_str):
        return {
            "status": "ERROR",
            "reason": "Amount must be an integer millisatoshi value",
        }, 400

    amount_msat = int(amount_str)
    min_sendable = int(payload["minSendable"])
    max_sendable = int(payload["maxSendable"])

    if amount_msat < min_sendable:
        return {
            "status": "ERROR",
            "reason": "Amount is below the minimum sendable value",
        }, 400
    if amount_msat > max_sendable:
        return {
            "status": "ERROR",
            "reason": "Amount is above the maximum sendable value",
        }, 400
    if amount_msat % 1000 != 0:
        return {
            "status": "ERROR",
            "reason": "Amount must be a whole-satoshi value",
        }, 400

    try:
        app = manager.find_app_by_alias(alias)
    except _mgr_mod.AlbyHubError:
        return {"status": "ERROR", "reason": "Service temporarily unavailable"}, 503

    if app is None:
        return {"status": "ERROR", "reason": "Unknown Lightning Address alias"}, 404

    meta = _mgr_mod.AlbyHubManager._parse_metadata(app.get("metadata"))
    description = meta.get("lnurl_description") or f"Pay {alias}"

    try:
        app_id = int(app["id"])
        invoice = manager.issue_invoice(app_id, amount_msat, description)
    except _mgr_mod.AlbyHubError:
        return {"status": "ERROR", "reason": "Invoice creation failed"}, 502

    # Audit log: Invoice generated via LNURL
    _audit_mod.audit_log(
        "lnurl_invoice_created",
        alias=alias,
        amount_msat=amount_msat,
        amount_sat=amount_msat // 1000,
        client_ip=client_ip,
        invoice_prefix=invoice[:50] + "..." if len(invoice) > 50 else invoice,
    )

    return {"pr": invoice, "routes": []}, 200


# ── HTTP server ───────────────────────────────────────────────────


def _make_handler(manager: "AlbyHubManager") -> type:
    """Return a handler class bound to the given manager."""

    class LnurlHandler(BaseHTTPRequestHandler):
        _manager = manager

        def log_message(self, fmt: str, *args: object) -> None:
            logger.debug(f"LNURL {self.address_string()} {fmt % args}")

        def _send_json(self, status: int, body: dict) -> None:
            raw = json.dumps(body, separators=(",", ":")).encode("utf-8")
            self.send_response(status)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(raw)))
            self.end_headers()
            self.wfile.write(raw)

        def _get_client_ip(self) -> str:
            # Check X-Forwarded-For header (set by Caddy)
            forwarded = self.headers.get("X-Forwarded-For")
            if forwarded:
                # Take the first IP in the chain
                return forwarded.split(",")[0].strip()
            # Fallback to direct connection IP
            return self.client_address[0]

        def _check_rate_limit(self) -> bool:
            client_ip = self._get_client_ip()
            if not _check_rate_limit(client_ip):
                self._send_json(429, {
                    "status": "ERROR",
                    "reason": "Rate limit exceeded. Please slow down."
                })
                _audit_mod.audit_log(
                    "rate_limit_exceeded",
                    client_ip=client_ip,
                    path=self.path,
                )
                return False
            return True

        def do_GET(self) -> None:  # noqa: N802
            if not self._check_rate_limit():
                return

            parsed = urllib.parse.urlparse(self.path)
            path = parsed.path
            qs = urllib.parse.parse_qs(parsed.query)
            client_ip = self._get_client_ip()

            # /.well-known/lnurlp/{alias}
            m = re.fullmatch(
                r"/.well-known/lnurlp/([^/]+)", path
            )
            if m:
                alias = urllib.parse.unquote(m.group(1))
                payload, code = _lnurl_discovery(alias, self._manager, client_ip)
                self._send_json(code, payload)
                return

            # /lnurlp/{alias}/callback
            m = re.fullmatch(r"/lnurlp/([^/]+)/callback", path)
            if m:
                alias = urllib.parse.unquote(m.group(1))
                amount_values = qs.get("amount")
                if not amount_values:
                    amount_str = None
                elif len(amount_values) != 1:
                    self._send_json(
                        400,
                        {
                            "status": "ERROR",
                            "reason": "A single amount parameter is required",
                        },
                    )
                    return
                else:
                    amount_str = amount_values[0]
                payload, code = _lnurl_callback(alias, amount_str, self._manager, client_ip)
                self._send_json(code, payload)
                return

            self._send_json(404, {"status": "ERROR", "reason": "Not found"})

    return LnurlHandler


def run(
    host: str = LNURL_BIND_HOST,
    port: int = LNURL_PORT,
    manager: "AlbyHubManager | None" = None,
) -> None:
    """Start the blocking LNURL HTTP server."""
    if manager is None:
        manager = _mgr_mod.get_manager()
    handler_class = _make_handler(manager)
    server = HTTPServer((host, port), handler_class)
    logger.info("nwc-lnurl service listening on %s:%d", host, port)
    server.serve_forever()


def main() -> None:
    logging.basicConfig(level=logging.INFO)
    run()


if __name__ == "__main__":
    main()
