#!/usr/bin/env python3
"""GLMChat target-host acceptance runner.

Runs non-destructive filesystem and HTTP checks against a deployed GLMChat
instance. It never performs setup, login, mutation, provider generation or
any destructive operation.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import ssl
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any


@dataclass
class Check:
    name: str
    status: str
    detail: str


def add(checks: list[Check], name: str, ok: bool, detail: str) -> None:
    checks.append(Check(name, "Passed" if ok else "Failed", detail))


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def filesystem_checks(app_dir: Path, checks: list[Check]) -> None:
    required = [
        ".htaccess", "index.html", "index.php", "manifest.webmanifest",
        "sw.js", "precache-manifest.json", "server", "storage", "assets", "icons",
    ]
    for item in required:
        path = app_dir / item
        add(checks, f"filesystem:{item}", path.exists(), str(path))

    try:
        manifest = json.loads((app_dir / "DEPLOYMENT_MANIFEST.json").read_text("utf-8"))
        files = manifest.get("files", {})
        failures: list[str] = []
        entries = files.items() if isinstance(files, dict) else (
            (entry.get("path"), entry) for entry in files if isinstance(entry, dict)
        )
        for rel, entry in entries:
            expected = entry.get("sha256")
            target = app_dir / str(rel)
            if not target.is_file():
                failures.append(f"missing:{rel}")
            elif expected and sha256(target) != expected:
                failures.append(f"hash:{rel}")
        add(checks, "filesystem:deployment-manifest", not failures,
            "inventory verified" if not failures else ", ".join(failures[:20]))
    except Exception as exc:
        add(checks, "filesystem:deployment-manifest", False, repr(exc))

    php = subprocess.run(["php", "-r", "echo PHP_VERSION;"], capture_output=True, text=True)
    add(checks, "runtime:php-cli", php.returncode == 0,
        php.stdout.strip() if php.returncode == 0 else php.stderr.strip())
    if php.returncode == 0:
        modules = subprocess.run(["php", "-m"], capture_output=True, text=True)
        available = {line.strip().lower() for line in modules.stdout.splitlines()}
        for module in ["sqlite3", "curl", "json", "openssl", "mbstring", "fileinfo", "zip"]:
            add(checks, f"runtime:php-extension:{module}", module in available,
                "loaded" if module in available else "missing")


def request(url: str, timeout: float, method: str = "GET") -> tuple[int, dict[str, str], bytes]:
    req = urllib.request.Request(url, method=method, headers={"User-Agent": "GLMChat-Acceptance/1.0"})
    context = ssl.create_default_context()
    try:
        with urllib.request.urlopen(req, timeout=timeout, context=context) as response:
            return response.status, {k.lower(): v for k, v in response.headers.items()}, response.read(2_000_000)
    except urllib.error.HTTPError as exc:
        return exc.code, {k.lower(): v for k, v in exc.headers.items()}, exc.read(2_000_000)


def http_checks(base_url: str, timeout: float, checks: list[Check]) -> None:
    base = base_url.rstrip("/") + "/"
    parsed = urllib.parse.urlsplit(base)
    add(checks, "http:https", parsed.scheme == "https", base)

    public = {
        "": (200, "text/html"),
        "index.php": (200, "text/html"),
        "manifest.webmanifest": (200, "application"),
        "sw.js": (200, "javascript"),
        "offline.html": (200, "text/html"),
    }
    for rel, (expected_status, content_hint) in public.items():
        url = urllib.parse.urljoin(base, rel)
        try:
            status, headers, body = request(url, timeout)
            content_type = headers.get("content-type", "")
            ok = status == expected_status and content_hint in content_type.lower() and len(body) > 0
            add(checks, f"http:public:{rel or 'root'}", ok,
                f"status={status}; content-type={content_type}; bytes={len(body)}")
        except Exception as exc:
            add(checks, f"http:public:{rel or 'root'}", False, repr(exc))

    blocked = [
        "server/config.php", "server/bootstrap.php", "storage/", ".htaccess",
        "DEPLOYMENT_MANIFEST.json", "sbom.cdx.json", "CHANGES_000000000000.txt",
    ]
    for rel in blocked:
        try:
            status, headers, body = request(urllib.parse.urljoin(base, rel), timeout)
            ok = status in {403, 404}
            add(checks, f"http:blocked:{rel}", ok,
                f"status={status}; bytes={len(body)}; content-type={headers.get('content-type', '')}")
        except Exception as exc:
            add(checks, f"http:blocked:{rel}", False, repr(exc))

    try:
        status, headers, body = request(urllib.parse.urljoin(base, "api/status"), timeout)
        payload: Any = json.loads(body.decode("utf-8")) if body else None
        data = payload.get("data", payload) if isinstance(payload, dict) else None
        state = data.get("state") if isinstance(data, dict) else None
        runtime = data.get("runtime") if isinstance(data, dict) else None
        error = payload.get("error", {}) if isinstance(payload, dict) else {}
        failures = error.get("failures", []) if isinstance(error, dict) else []
        healthy = status == 200 and state in {"setup_required", "installed", "unavailable"} and isinstance(runtime, dict)
        controlled_preflight = (
            status == 503
            and isinstance(error, dict)
            and error.get("code") == "ENVIRONMENT_INVALID"
            and isinstance(failures, list)
            and bool(failures)
        )
        ok = healthy or controlled_preflight
        detail = (
            f"status={status}; state={state}; cache-control={headers.get('cache-control', '')}"
            if healthy
            else f"status={status}; code={error.get('code') if isinstance(error, dict) else None}; failures={len(failures) if isinstance(failures, list) else 0}; cache-control={headers.get('cache-control', '')}"
        )
        add(checks, "http:api-status", ok, detail)
        cache = headers.get("cache-control", "").lower()
        add(checks, "http:api-no-store", "no-store" in cache,
            headers.get("cache-control", "missing cache-control"))
    except Exception as exc:
        add(checks, "http:api-status", False, repr(exc))


def main() -> int:
    parser = argparse.ArgumentParser(description="Non-destructive GLMChat deployment acceptance checks")
    parser.add_argument("--app-dir", type=Path, help="Local extracted app directory")
    parser.add_argument("--base-url", help="Deployed HTTPS URL, including nested path when applicable")
    parser.add_argument("--timeout", type=float, default=15.0)
    parser.add_argument("--json-output", type=Path)
    args = parser.parse_args()
    if not args.app_dir and not args.base_url:
        parser.error("provide --app-dir, --base-url, or both")

    checks: list[Check] = []
    if args.app_dir:
        filesystem_checks(args.app_dir.resolve(), checks)
    if args.base_url:
        http_checks(args.base_url, args.timeout, checks)

    for check in checks:
        print(f"{check.status.upper():7} {check.name} — {check.detail}")
    passed = sum(c.status == "Passed" for c in checks)
    failed = len(checks) - passed
    summary = {"passed": passed, "failed": failed, "checks": [asdict(c) for c in checks]}
    print(f"\nSUMMARY passed={passed} failed={failed}")
    if args.json_output:
        args.json_output.parent.mkdir(parents=True, exist_ok=True)
        args.json_output.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
    return 0 if failed == 0 else 1


if __name__ == "__main__":
    sys.exit(main())
