"""Self-service signup endpoint for new Keiro users.

Provides ``POST /v1/auth/register`` which creates a new API key without
requiring admin authentication. Keys are registered in the active auth
backend (InMemoryAuthBackend) and persisted to a JSONL file for restart
resilience.

Rate limiting: 5 registrations per IP per hour (in-memory counter).
"""

from __future__ import annotations

import hashlib
import json
import logging
import secrets
import time
from collections import defaultdict
from pathlib import Path
from typing import Optional

from aiohttp import web

from keiro.gateway.infra.app_keys import AUTH_BACKEND_KEY, SETTINGS_KEY
from keiro.gateway.infra.middleware.auth import InMemoryAuthBackend

_LOG = logging.getLogger(__name__)

# Rate limit: max registrations per IP within the window.
_MAX_REGISTRATIONS_PER_IP = 5
_RATE_WINDOW_SECONDS = 3600  # 1 hour

# In-memory rate limit tracker: IP -> list of registration timestamps.
_registration_timestamps: dict[str, list[float]] = defaultdict(list)

# In-memory email dedup tracker: email -> org_id.
_registered_emails: dict[str, str] = {}

# Default persistence path (relative to working directory).
_DEFAULT_KEYS_FILE = Path("data/registered_keys.jsonl")


def register(app: web.Application) -> None:
    """Attach the self-service signup route to the application."""
    app.router.add_post("/v1/auth/register", post_register)


async def post_register(request: web.Request) -> web.Response:
    """Handle self-service account creation.

    Request body::

        {"email": "alice@example.com"}

    Success response (201)::

        {
            "api_key": "keiro_sk_...",
            "org_id": "org_...",
            "email": "alice@example.com"
        }
    """
    # Rate limiting by client IP.
    client_ip = _client_ip(request)
    if not _check_rate_limit(client_ip):
        raise web.HTTPTooManyRequests(
            text="Too many registration attempts. Try again later."
        )

    # Parse and validate request body.
    try:
        body = await request.json()
    except (json.JSONDecodeError, Exception):
        raise web.HTTPBadRequest(text="Request body must be valid JSON")

    if not isinstance(body, dict):
        raise web.HTTPBadRequest(text="Request body must be a JSON object")

    email = body.get("email")
    if not isinstance(email, str) or not email.strip():
        raise web.HTTPBadRequest(text="'email' field is required")
    email = email.strip().lower()

    if "@" not in email or "." not in email.split("@", 1)[1]:
        raise web.HTTPBadRequest(text="Invalid email address")

    # Check for duplicate email.
    if email in _registered_emails:
        raise web.HTTPConflict(text="An account with this email already exists")

    # Get the auth backend from the app.
    backend = request.app.get(AUTH_BACKEND_KEY)
    if not isinstance(backend, InMemoryAuthBackend):
        raise web.HTTPServiceUnavailable(
            text="Self-service registration is not available in this configuration"
        )

    # Generate credentials.
    api_key = _generate_api_key()
    org_id = _derive_org_id(email)

    # Resolve tier from settings.
    settings = request.app.get(SETTINGS_KEY)
    tier = "T1"
    if settings is not None:
        tier = getattr(settings.auth, "default_tier", "T1")

    # Register in the auth backend.
    backend.register_key(api_key, org_id, tier=tier, role="user")

    # Track email for dedup.
    _registered_emails[email] = org_id

    # Persist to JSONL for restart resilience.
    _persist_key(api_key, org_id, email, tier=tier)

    # Record the registration for rate limiting.
    _registration_timestamps[client_ip].append(time.monotonic())

    _LOG.info(
        "account_created org_id=%s email=%s tier=%s",
        org_id,
        _mask_email(email),
        tier,
    )

    return web.json_response(
        {"api_key": api_key, "org_id": org_id, "email": email},
        status=201,
    )


def _generate_api_key() -> str:
    """Generate a Keiro API key: ``keiro_sk_`` + 32 URL-safe random chars."""
    return f"keiro_sk_{secrets.token_urlsafe(32)}"


def _derive_org_id(email: str) -> str:
    """Derive a deterministic org_id from an email address."""
    digest = hashlib.sha256(email.encode("utf-8")).hexdigest()[:12]
    return f"org_{digest}"


def _client_ip(request: web.Request) -> str:
    """Extract client IP, respecting X-Forwarded-For behind a proxy."""
    forwarded = request.headers.get("X-Forwarded-For")
    if forwarded:
        return forwarded.split(",")[0].strip()
    peername = request.transport.get_extra_info("peername") if request.transport else None
    if peername:
        return str(peername[0])
    return "unknown"


def _check_rate_limit(client_ip: str) -> bool:
    """Return True if the client IP is within the rate limit."""
    now = time.monotonic()
    cutoff = now - _RATE_WINDOW_SECONDS
    timestamps = _registration_timestamps[client_ip]
    # Prune expired entries.
    _registration_timestamps[client_ip] = [t for t in timestamps if t > cutoff]
    return len(_registration_timestamps[client_ip]) < _MAX_REGISTRATIONS_PER_IP


def _persist_key(
    api_key: str,
    org_id: str,
    email: str,
    *,
    tier: str = "T1",
    keys_file: Optional[Path] = None,
) -> None:
    """Append a key record to the JSONL persistence file."""
    path = keys_file or _DEFAULT_KEYS_FILE
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open("a", encoding="utf-8") as fh:
            record = {
                "key": api_key,
                "org_id": org_id,
                "email": email,
                "tier": tier,
                "role": "user",
                "created_at": time.time(),
            }
            fh.write(json.dumps(record) + "\n")
    except OSError as exc:
        _LOG.error("Failed to persist registered key: %s", exc)


def _mask_email(email: str) -> str:
    """Mask email for log output: ``a***e@example.com``."""
    local, _, domain = email.partition("@")
    if len(local) <= 2:
        masked_local = local[0] + "***"
    else:
        masked_local = local[0] + "***" + local[-1]
    return f"{masked_local}@{domain}"


def load_persisted_emails(keys_file: Optional[Path] = None) -> None:
    """Load email dedup state from the persistence file on startup."""
    path = keys_file or _DEFAULT_KEYS_FILE
    if not path.exists():
        return
    try:
        with path.open("r", encoding="utf-8") as fh:
            for line in fh:
                line = line.strip()
                if not line:
                    continue
                try:
                    entry = json.loads(line)
                except json.JSONDecodeError:
                    continue
                email = entry.get("email")
                org_id = entry.get("org_id")
                if email and org_id:
                    _registered_emails[email.lower()] = org_id
    except OSError as exc:
        _LOG.warning("Failed to load persisted emails: %s", exc)
