"""One-time connect codes for WordPress plugin connect completion.

Contract: docs/COMPANION_RUNTIME_CONTRACT.md 2A.2 (EDGE-11) and C-3 (no credential in a URL).

The connect code travels in a browser URL (Dashboard -> return to the site), so it must be
worthless on its own:

* it is an opaque random reference; nothing is encoded in it (the previous format was a
  signed base64 payload that contained the installation credential in the clear, and could
  be redeemed repeatedly during its TTL);
* the server keeps what it stands for (installation id, normalized site) in Redis under a
  hash of the code, and redemption consumes it atomically, so it works once;
* the installation credential is minted at redemption and returned in the response body
  only, never placed in a URL.

A code alone still proves nothing about who holds it, so the return address is validated
against the site it is for (``validate_wordpress_return_url``): a code delivered to the wrong
origin could otherwise be redeemed by that origin.
"""
from __future__ import annotations

import hashlib
import json
import secrets
from typing import Any
from urllib.parse import urlparse

from src.config import Environment, get_settings
from src.core.exceptions import ForbiddenError, ValidationError

CODE_PREFIX = "p2c1."
CODE_TTL_SECONDS = 600
_KEY_PREFIX = "wp:connect:"
_MAX_CODE_LENGTH = 128


def _normalize_site_url(url: str) -> str:
    parsed = urlparse(url.strip().rstrip("/"))
    if not parsed.scheme or not parsed.netloc:
        raise ValidationError("invalid_site_url")
    return f"{parsed.scheme}://{parsed.netloc}".lower()


def resolve_wp_plugin_api_url() -> str:
    """Public plugin API base URL (gateway in prod, direct Core in dev)."""
    settings = get_settings()
    if settings.environment == Environment.DEV:
        return settings.api_url.rstrip("/")

    configured = (settings.wp_gateway_public_url or "").strip()
    if configured:
        return configured.rstrip("/")
    return settings.api_url.rstrip("/")


def validate_wordpress_return_url(site_url: str, return_url: str) -> str:
    """Return ``return_url`` if the browser may be sent back to it after connecting.

    It must be on exactly the same origin as ``site_url`` (scheme, host, port), carry no
    credentials, and point into the site's ``wp-admin``. Anything else could deliver the
    connect code to a third party. Raises ``ValidationError("invalid_return_url")``.
    """
    site_origin = _normalize_site_url(site_url)
    candidate = (return_url or "").strip()
    parsed = urlparse(candidate)
    if parsed.scheme not in ("http", "https") or not parsed.netloc:
        raise ValidationError("invalid_return_url")
    if "@" in parsed.netloc or parsed.fragment:
        raise ValidationError("invalid_return_url")
    if f"{parsed.scheme}://{parsed.netloc}".lower() != site_origin:
        raise ValidationError("invalid_return_url")
    if "wp-admin" not in parsed.path.split("/"):
        raise ValidationError("invalid_return_url")
    return candidate


def _key(connect_code: str) -> str:
    return _KEY_PREFIX + hashlib.sha256(connect_code.encode()).hexdigest()


async def issue_wordpress_connect_code(
    redis: Any,
    *,
    installation_id: str,
    site_url: str,
    ttl_seconds: int = CODE_TTL_SECONDS,
) -> str:
    """Create a single-use code for ``installation_id`` on ``site_url``."""
    code = CODE_PREFIX + secrets.token_urlsafe(32)
    stored = json.dumps(
        {"installation_id": installation_id, "site_url": _normalize_site_url(site_url)},
        separators=(",", ":"),
    )
    created = await redis.set(_key(code), stored, ex=ttl_seconds, nx=True)
    if not created:  # 256 random bits: only a broken RNG or store gets here
        raise RuntimeError("connect_code_collision")
    return code


async def redeem_wordpress_connect_code(
    redis: Any, connect_code: str, site_url: str
) -> dict[str, str]:
    """Consume a code and return ``{"installation_id": ...}``.

    The code is consumed by the first attempt, successful or not, so a wrong site burns it.
    Unknown, used and expired codes are indistinguishable to the caller.
    """
    connect_code = (connect_code or "").strip()
    if not connect_code.startswith(CODE_PREFIX) or len(connect_code) > _MAX_CODE_LENGTH:
        raise ValidationError("invalid_connect_code")
    expected_site = _normalize_site_url(site_url)

    # GET + DEL in one MULTI/EXEC so two concurrent redemptions cannot both succeed.
    async with redis.pipeline(transaction=True) as pipe:
        pipe.get(_key(connect_code))
        pipe.delete(_key(connect_code))
        raw, _ = await pipe.execute()

    if not raw:
        raise ForbiddenError("invalid_connect_code")
    try:
        stored = json.loads(raw)
        installation_id = str(stored["installation_id"]).strip()
        stored_site = str(stored["site_url"]).strip()
    except (ValueError, KeyError, TypeError) as exc:
        raise ForbiddenError("invalid_connect_code") from exc

    if not installation_id or stored_site != expected_site:
        raise ForbiddenError("connect_code_site_mismatch")
    return {"installation_id": installation_id}
