#!/usr/bin/env python3
"""Verify every manifest-listed file against the production Pages deployment."""

from __future__ import annotations

import argparse
import hashlib
import json
import time
from pathlib import Path

import requests

DEFAULT_SITE = (
    Path(__file__).resolve().parent / "cloudflare_mw_m31_profile_explorer" / "site"
)


def fetch(session: requests.Session, url: str, attempts: int = 6) -> requests.Response:
    response: requests.Response | None = None
    last_error: requests.RequestException | None = None
    for attempt in range(attempts):
        try:
            response = session.get(url, timeout=60)
            if response.status_code == 200:
                return response
        except requests.RequestException as error:
            last_error = error
        if attempt + 1 == attempts:
            break
        time.sleep(min(2**attempt, 20))
    if response is None:
        raise RuntimeError(f"no response for {url}: {last_error}") from last_error
    return response


def root_headers_from_config(path: Path) -> dict[str, str]:
    headers: dict[str, str] = {}
    in_root_rule = False
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if line == "/*":
            in_root_rule = True
            continue
        if in_root_rule and line.startswith("/"):
            break
        if in_root_rule and ":" in line:
            name, value = line.split(":", 1)
            headers[name.strip()] = value.strip()
    return headers


def verify(site_root: Path) -> dict[str, object]:
    local_manifest_path = site_root / "report_manifest.json"
    local_manifest_bytes = local_manifest_path.read_bytes()
    manifest = json.loads(local_manifest_bytes)
    base = manifest["canonical_url"].rstrip("/") + "/"
    failures: list[dict[str, object]] = []
    checked = 0
    checked_bytes = 0
    config_checked = 0
    expected_root_headers: dict[str, str] = {}
    for item in manifest.get("deployment_config", []):
        path = site_root / item["path"]
        if (
            not path.is_file()
            or path.stat().st_size != item["bytes"]
            or hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"]
        ):
            failures.append(
                {"path": item["path"], "error": "local deployment config mismatch"}
            )
            continue
        config_checked += 1
        if item["path"] == "_headers":
            expected_root_headers = root_headers_from_config(path)
    with requests.Session() as session:
        root = fetch(session, base)
        if root.status_code != 200:
            failures.append({"path": "/", "status": root.status_code})
        for name, expected in expected_root_headers.items():
            actual = root.headers.get(name)
            if actual != expected:
                failures.append(
                    {
                        "path": "/",
                        "header": name,
                        "expected": expected,
                        "actual": actual,
                    }
                )
        remote_manifest = fetch(session, base + "report_manifest.json")
        if (
            remote_manifest.status_code != 200
            or remote_manifest.content != local_manifest_bytes
        ):
            failures.append(
                {
                    "path": "report_manifest.json",
                    "status": remote_manifest.status_code,
                    "expected_bytes": len(local_manifest_bytes),
                    "actual_bytes": len(remote_manifest.content),
                    "expected_sha256": hashlib.sha256(local_manifest_bytes).hexdigest(),
                    "actual_sha256": hashlib.sha256(
                        remote_manifest.content
                    ).hexdigest(),
                }
            )
        for item in manifest["files"]:
            path = item["path"]
            response = fetch(session, base + path)
            digest = (
                hashlib.sha256(response.content).hexdigest()
                if response.status_code == 200
                else None
            )
            if (
                response.status_code != 200
                or len(response.content) != item["bytes"]
                or digest != item["sha256"]
            ):
                failures.append(
                    {
                        "path": path,
                        "status": response.status_code,
                        "expected_bytes": item["bytes"],
                        "actual_bytes": len(response.content),
                        "expected_sha256": item["sha256"],
                        "actual_sha256": digest,
                    }
                )
            checked += 1
            checked_bytes += len(response.content)
    return {
        "canonical_url": base,
        "checked_files": checked,
        "checked_bytes": checked_bytes,
        "deployment_config_checked": config_checked,
        "security_headers_checked": len(expected_root_headers),
        "remote_manifest_checked": True,
        "failures": failures,
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--site-root", type=Path, default=DEFAULT_SITE)
    args = parser.parse_args()
    result = verify(args.site_root)
    print(json.dumps(result, indent=2))
    raise SystemExit(bool(result["failures"]))


if __name__ == "__main__":
    main()
