"""Gitea webhook receiver per §4.1.

Both the webhook receiver and the reconciler are §4.1 cache writers.
On a meaningful event — meta-repo push or PR change — we re-read just
what changed from Gitea and update the cache. The signature is verified
against the configured shared secret so spurious POSTs cannot poison
the cache.
"""
from __future__ import annotations

import hashlib
import hmac
import json
import logging
import os

from fastapi import APIRouter, Header, HTTPException, Request

from . import cache, db, projects as projects_mod, registry as registry_mod
from .config import Config
from .gitea import Gitea

log = logging.getLogger(__name__)

EVENTS_OF_INTEREST = {
    "push",            # meta-repo or RFC-repo commits
    "pull_request",    # opened / closed / merged
    "create",          # branch or repo created
    "delete",          # branch deleted
    "repository",      # repo created or deleted
}


def make_router(config: Config, gitea: Gitea) -> APIRouter:
    router = APIRouter()

    @router.post("/api/webhooks/gitea")
    async def receive(
        request: Request,
        x_gitea_event: str = Header(default=""),
        x_gitea_signature: str = Header(default=""),
    ):
        body = await request.body()
        # v0.18.0: defense in depth. config.py refuses to start
        # when the secret is empty unless `RFC_APP_INSECURE_WEBHOOKS=1`
        # is set; this branch catches the dev-bypass case (the only
        # path where `config.webhook_secret` can be empty) and surfaces
        # it loudly to the client. A POST that lands here with an
        # empty secret on a production deployment indicates a
        # mis-configuration (somebody flipped the bypass in prod),
        # and the loud 500 is the proposal's whole point.
        insecure = os.environ.get("RFC_APP_INSECURE_WEBHOOKS", "").strip() == "1"
        if not config.webhook_secret:
            if not insecure:
                log.error(
                    "webhook receiver misconfigured: GITEA_WEBHOOK_SECRET is empty "
                    "and RFC_APP_INSECURE_WEBHOOKS=1 is not set"
                )
                raise HTTPException(status_code=500, detail="Webhook receiver misconfigured")
            log.warning(
                "webhook receiver running with RFC_APP_INSECURE_WEBHOOKS=1 — "
                "signature verification is DISABLED. Production deployments MUST NOT set this."
            )
        else:
            if not _verify_signature(body, x_gitea_signature, config.webhook_secret):
                raise HTTPException(status_code=401, detail="Invalid signature")

        event = x_gitea_event.lower()
        if event not in EVENTS_OF_INTEREST:
            return {"ok": True, "ignored": event}

        # Identify the originating repo. For the meta repo we refresh
        # the entry cache + meta-PR cache; for a per-RFC repo we refresh
        # just that repo's branches/PRs/main body. The handler stays
        # generous in what it accepts — refreshes are idempotent and
        # small enough that overlapping events do not pile up.
        try:
            payload = json.loads(body) if body else {}
        except Exception:
            payload = {}
        repo_full = (payload.get("repository") or {}).get("full_name") or ""
        registry_full = f"{config.gitea_org}/{config.registry_repo}"
        # §22/G-15: a corpus push can land on ANY project's content_repo, not
        # just the default — recognise the full set so a non-default project's
        # push triggers the (multi-project) corpus/branch/PR refresh.
        content_fulls = {
            f"{config.gitea_org}/{r['content_repo']}"
            for r in db.conn().execute(
                "SELECT content_repo FROM projects "
                "WHERE content_repo IS NOT NULL AND content_repo != ''")
        }
        if not content_fulls:
            log.warning("webhook: no project content_repo is known; corpus refresh skipped")
        try:
            if repo_full == registry_full:
                # §22.2: a registry-repo push re-mirrors the projects table.
                # Tolerate a malformed projects.yaml (keep last-good rows); let a
                # transport error bubble to the outer 500 so an unreachable Gitea
                # on a registry push is loud rather than silently dropped.
                try:
                    await registry_mod.refresh_registry(config, gitea)
                except registry_mod.RegistryError:
                    log.exception("registry webhook: invalid projects.yaml; keeping last-good")
            elif content_fulls and (repo_full in content_fulls or not repo_full):
                await cache.refresh_meta_repo(config, gitea)
                await cache.refresh_meta_branches(config, gitea)
                await cache.refresh_meta_pulls(config, gitea)
            else:
                slug = _slug_for_repo(repo_full)
                if slug:
                    await cache.refresh_rfc_repo(config, gitea, slug)
                else:
                    log.info(
                        "webhook received for unknown repo: repo_full=%s event=%s "
                        "(no cached_rfcs row matched; hook may be on a fork or stale)",
                        repo_full, event,
                    )
        except Exception:
            log.exception("webhook refresh failed")
            raise HTTPException(status_code=500, detail="Refresh failed")

        return {"ok": True}

    return router


def _verify_signature(body: bytes, header: str, secret: str) -> bool:
    if not header:
        return False
    expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)


def _slug_for_repo(repo_full: str) -> str | None:
    row = db.conn().execute(
        "SELECT slug FROM cached_rfcs WHERE repo = ?", (repo_full,)
    ).fetchone()
    return row["slug"] if row else None
