"""Public-endpoint defenses: honeypot, submission timing, rate limiting, origin/CORS.

Keep this cheap and synchronous. Rate limiting is an in-memory sliding window
(resets on restart — acceptable for this scale and fails open only after restart).
"""

from __future__ import annotations

import time
from collections import defaultdict, deque
from typing import Optional
from urllib.parse import urlparse

# (form_id, ip) -> deque[timestamps]
_hits: dict[tuple[int, str], deque] = defaultdict(deque)


def client_ip(request) -> str:
    # Caddy sets X-Forwarded-For; uvicorn runs with --forwarded-allow-ips=*.
    xff = request.headers.get("x-forwarded-for")
    if xff:
        return xff.split(",")[0].strip()
    return request.client.host if request.client else "unknown"


def request_origin(request) -> Optional[str]:
    """Normalised scheme://host[:port] of the submitting page, from Origin or Referer."""
    raw = request.headers.get("origin")
    if not raw:
        ref = request.headers.get("referer")
        if ref:
            p = urlparse(ref)
            raw = f"{p.scheme}://{p.netloc}" if p.scheme and p.netloc else None
    if not raw:
        return None
    p = urlparse(raw)
    if not p.scheme or not p.netloc:
        return None
    return f"{p.scheme}://{p.netloc}".lower()


def parse_allowed(allowed_origins: str) -> list[str]:
    return [o.strip().lower().rstrip("/") for o in allowed_origins.split(",") if o.strip()]


def _pattern_matches(origin: str, pattern: str) -> bool:
    """A pattern is one of:
      - bare domain:    site.com        → the apex AND any subdomain (www, shop, …), any
                        scheme. This is the common case: add the root domain, done.
      - wildcard host:  *.demoing.info  → same as a bare domain (apex + subdomains).
      - exact origin:   https://site.com → exact scheme + host[:port] only (pin one origin).
    """
    o_host = urlparse(origin).netloc.lower()
    o_norm = origin.lower().rstrip("/")
    p = pattern.lower().rstrip("/")
    if "://" in p:                       # explicit scheme → exact-origin match
        return o_norm == p
    p_host = p[2:] if p.startswith("*.") else p   # treat *.domain the same as bare domain
    return o_host == p_host or o_host.endswith("." + p_host)


def origin_allowed(origin: Optional[str], allowed_origins: str) -> bool:
    """Empty allowlist = learn mode (allow anything, caller records it).
    Supports exact origins, bare hosts, and wildcard hosts like *.demoing.info."""
    patterns = parse_allowed(allowed_origins)
    if not patterns:
        return True
    if origin is None:
        return False
    return any(_pattern_matches(origin, p) for p in patterns)


def honeypot_tripped(payload: dict, honeypot_field: str) -> bool:
    val = payload.get(honeypot_field)
    return bool(val and str(val).strip())


def too_fast(payload: dict, min_seconds: int) -> bool:
    """If embed.js included a render timestamp (_ts, epoch ms), enforce min dwell time.
    Plain no-JS forms omit _ts and are not penalised here."""
    if min_seconds <= 0:
        return False
    ts = payload.get("_ts")
    if ts is None:
        return False
    try:
        rendered_ms = float(ts)
    except (TypeError, ValueError):
        return False
    elapsed = time.time() - (rendered_ms / 1000.0)
    return 0 <= elapsed < min_seconds


def rate_limited(form_id: int, ip: str, per_hour: int) -> bool:
    if per_hour <= 0:
        return False
    now = time.time()
    window = 3600.0
    dq = _hits[(form_id, ip)]
    while dq and now - dq[0] > window:
        dq.popleft()
    if len(dq) >= per_hour:
        return True
    dq.append(now)
    return False


def safe_redirect(candidate: Optional[str], origin: Optional[str],
                  allowed_origins: str) -> Optional[str]:
    """Validate a form-supplied success redirect (no-JS `_redirect` field).

    Returns the URL only if it points back to the SAME site that submitted the form
    (same host as the Origin) or to a host explicitly listed in the form's
    allowed_origins. This prevents the submit endpoint from becoming an open redirect.
    """
    if not candidate:
        return None
    p = urlparse(candidate)
    if p.scheme not in ("http", "https") or not p.netloc:
        return None
    if origin and p.netloc.lower() == urlparse(origin).netloc.lower():
        return candidate
    if parse_allowed(allowed_origins) and origin_allowed(f"{p.scheme}://{p.netloc}", allowed_origins):
        return candidate
    return None


def cors_headers(origin: Optional[str], allowed_origins: str) -> dict:
    """Echo the origin only when allowed; never wildcard with credentials."""
    if origin and origin_allowed(origin, allowed_origins):
        return {
            "Access-Control-Allow-Origin": origin,
            "Access-Control-Allow-Methods": "POST, OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type, X-Requested-With",
            "Access-Control-Max-Age": "600",
            "Vary": "Origin",
        }
    return {}
