#!/usr/bin/env python3
"""Add an already-tested Goblin Purrfect bottle to the existing hail Brew site.

Run on hail, with --stage pointing at a private release directory containing
site/ (explicit public artifacts) and package-entry.json (verified build facts).
The shared tap must already contain the corresponding formula. Nothing else
in the tap, APT repository or public package pool is changed by this utility.
"""
from __future__ import annotations

import argparse
import datetime
import fcntl
import hashlib
import html
import json
import os
from pathlib import Path, PurePosixPath
import re
import shutil
import subprocess
import tempfile


PUBLIC = Path("/mnt/distribution/brew.goblinreactor.com")
KEYHOME = Path("/home/adam/.local/share/goblin-apt/gnupg")
KEY = "B7B61E8CB85A9270530364F23273CB62402FC793"
BASE = "https://brew.goblinreactor.com/"


def digest(path: Path) -> str:
    with path.open("rb") as source:
        return hashlib.file_digest(source, "sha256").hexdigest()


def atomic_bytes(path: Path, data: bytes) -> None:
    descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.gp-", dir=path.parent)
    with os.fdopen(descriptor, "wb") as output:
        output.write(data)
        output.flush()
        os.fsync(output.fileno())
    os.chmod(temporary, 0o644)
    os.replace(temporary, path)


def artifact(path: Path) -> dict:
    relative = str(path.relative_to(PUBLIC))
    return {"path": relative, "filename": path.name, "url": BASE + relative,
            "size": path.stat().st_size, "sha256": digest(path)}


def inline(text: str) -> str:
    text = html.escape(text, quote=False)
    text = re.sub(r"\[([^\]]+)\]\((https://[^)]+)\)", r'<a href="\2">\1</a>', text)
    return re.sub(r"`([^`]+)`", r"<code>\1</code>", text)


def render(markdown: str) -> bytes:
    lines, index, result = markdown.splitlines(), 0, []
    while index < len(lines):
        line = lines[index]
        index += 1
        if not line.strip():
            continue
        if line.startswith("~~~"):
            block = []
            while index < len(lines) and not lines[index].startswith("~~~"):
                block.append(lines[index])
                index += 1
            index += 1
            result.append("<pre><code>" + html.escape("\n".join(block)) + "</code></pre>")
        elif line.startswith("#"):
            level = min(len(line) - len(line.lstrip("#")), 6)
            result.append(f"<h{level}>" + inline(line[level:].strip()) + f"</h{level}>")
        else:
            paragraph = [line]
            while index < len(lines) and lines[index].strip() and not lines[index].startswith(("#", "~~~")):
                paragraph.append(lines[index])
                index += 1
            result.append("<p>" + inline(" ".join(paragraph)) + "</p>")
    head = ('<!doctype html><html lang="en"><meta charset="utf-8">'
            '<meta name="viewport" content="width=device-width,initial-scale=1">'
            '<title>Goblin Purrfect — Homebrew installation</title><style>'
            ':root{color-scheme:light dark}body{max-width:58rem;margin:3rem auto;padding:0 1rem;'
            'font:17px/1.6 system-ui,sans-serif}pre{padding:1rem;overflow:auto;background:#8882}'
            'a{color:light-dark(#176534,#a4d79a)}</style><main>')
    return (head + "\n".join(result) + '</main><footer><a href="/">All Goblin Reactor Homebrew packages</a></footer></html>\n').encode()


def publish(stage: Path) -> None:
    stage = stage.resolve()
    entry = json.loads((stage / "package-entry.json").read_text())
    assert entry["name"] == "goblin-purrfect"
    assert entry["formula"] == "goblinreactor/tap/goblin-purrfect"
    assert entry["license"] == "Apache-2.0"
    assert entry["verification"]["public_https_bottle_install"] == "pass"
    assert entry["verification"]["installed_package_test"] == "pass"
    lock_path = Path("/home/adam/.local/share/goblin-apt/brew-publish.lock")
    with lock_path.open("a") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX)
        catalog_path = PUBLIC / "packages.json"
        signature = PUBLIC / "packages.json.asc"
        subprocess.run(["gpgv", "--keyring", str(PUBLIC / "goblinreactor-archive-keyring.gpg"),
                        str(signature), str(catalog_path)], check=True)
        catalog_bytes = catalog_path.read_bytes()
        signature_bytes = signature.read_bytes()
        catalog = json.loads(catalog_bytes)
        for old in catalog["artifacts"]:
            relative = PurePosixPath(old["path"])
            assert not relative.is_absolute() and ".." not in relative.parts
            file = PUBLIC / relative
            assert file.stat().st_size == old["size"] and digest(file) == old["sha256"], file
        backup = Path(tempfile.mkdtemp(prefix="before-publication-", dir=stage))
        for name in ["README.md", "README.txt", "index.html", "packages.json", "packages.json.asc"]:
            shutil.copy2(PUBLIC / name, backup / name)
        incoming = stage / "site"
        changed = []
        for file in sorted(incoming.rglob("*")):
            if file.is_symlink():
                raise SystemExit(f"unexpected staged symlink: {file}")
            if not file.is_file():
                continue
            relative = file.relative_to(incoming)
            assert relative.parts[0] in {"sources", "bottles", "releases"}, relative
            assert "goblin-purrfect" in str(relative), relative
            target = PUBLIC / relative
            target.parent.mkdir(parents=True, exist_ok=True)
            if target.exists():
                assert digest(target) == digest(file), f"immutable artifact changed: {target}"
            else:
                atomic_bytes(target, file.read_bytes())
            changed.append(target)
        docs = (stage / "goblin-purrfect.md").read_text()
        paragraph = (
            "<!-- goblin-purrfect:start -->\n\n## Goblin Purrfect\n\n"
            f"The additional formula **goblinreactor/tap/goblin-purrfect** ({entry['version']}) "
            "is available as a tested Apple Silicon/macOS 26 Tahoe bottle. "
            "It is an Apache-2.0 retro terminal word processor with WP files, "
            "Unicode, inline equations/images and LaTeX/PDF export.\n\n"
            "~~~sh\nbrew tap goblinreactor/tap https://brew.goblinreactor.com/homebrew-tap.git\n"
            "brew install goblinreactor/tap/goblin-purrfect\ngoblin-purrfect doctor\n~~~\n\n"
            "[Goblin Purrfect setup, optional PDF tools, licenses and source](https://brew.goblinreactor.com/goblin-purrfect.html). "
            "No gp alias is installed. The existing Goblin Mosh formula remains available; "
            "the signed packages.json catalog lists all published packages.\n\n"
            "<!-- goblin-purrfect:end -->\n")
        readme = (PUBLIC / "README.md").read_text()
        readme = readme.replace("Goblin Mosh and GoblinView for macOS",
                                "Goblin Mosh, GoblinView and Goblin Purrfect for macOS", 1)
        package_row = (f"| goblinreactor/tap/goblin-purrfect | {entry['version']} | "
                       "Apple Silicon (arm64), macOS 26 Tahoe |")
        if "| goblinreactor/tap/goblin-purrfect |" not in readme:
            tables = list(re.finditer(r"(?m)^\| goblinreactor/tap/[^\n]+\n", readme))
            assert tables, "existing package table missing; inspect before publication"
            insertion = tables[-1].end()
            readme = readme[:insertion] + package_row + "\n" + readme[insertion:]
        else:
            readme = re.sub(r"(?m)^\| goblinreactor/tap/goblin-purrfect \|[^\n]+", package_row, readme)
        pattern = r"<!-- goblin-purrfect:start -->.*?<!-- goblin-purrfect:end -->\n?"
        readme = re.sub(pattern, lambda _: paragraph, readme, flags=re.S) if "<!-- goblin-purrfect:start -->" in readme else readme.rstrip() + "\n\n" + paragraph
        index_html = (PUBLIC / "index.html").read_text()
        if "<td>goblinreactor/tap/goblin-purrfect</td>" not in index_html:
            table_row = ('<tr><td>goblinreactor/tap/goblin-purrfect</td><td>'
                         + html.escape(entry["version"]) + '</td><td>Apple Silicon (arm64), macOS 26 Tahoe</td></tr>')
            assert "</tbody>" in index_html, "existing HTML package table missing"
            index_html = index_html.replace("</tbody>", table_row + "</tbody>", 1)
        section = ('<!-- goblin-purrfect:start --><section><h2>Goblin Purrfect</h2>'
                   '<p>Now available: <code>goblinreactor/tap/goblin-purrfect</code> '
                   + html.escape(entry["version"]) + ' for Apple Silicon/macOS 26 Tahoe. '
                   'Apache-2.0 terminal word processor with WordPerfect-style editing.</p>'
                   '<pre><code>brew tap goblinreactor/tap https://brew.goblinreactor.com/homebrew-tap.git\n'
                   'brew install goblinreactor/tap/goblin-purrfect</code></pre>'
                   '<p><a href="goblin-purrfect.html">Setup, optional PDF tools, licenses and source</a>. '
                   'Goblin Mosh and all other existing packages remain available.</p></section><!-- goblin-purrfect:end -->')
        if "<!-- goblin-purrfect:start -->" in index_html:
            index_html = re.sub(pattern, lambda _: section, index_html, flags=re.S)
        else:
            marker = "</main>" if "</main>" in index_html else "</body>"
            assert marker in index_html
            index_html = index_html.replace(marker, section + marker, 1)
        # Detect a concurrent publisher that does not yet use our advisory lock.
        assert catalog_path.read_bytes() == catalog_bytes and signature.read_bytes() == signature_bytes, "catalog changed concurrently; retry from fresh state"
        for name, data in [("README.md", readme.encode()), ("README.txt", readme.encode()),
                           ("index.html", index_html.encode()), ("goblin-purrfect.md", docs.encode()),
                           ("goblin-purrfect.html", render(docs))]:
            atomic_bytes(PUBLIC / name, data)
            changed.append(PUBLIC / name)
        old_packages = [p for p in catalog["packages"] if p.get("name") != "goblin-purrfect" or
                        p.get("platform_tag") != entry["platform_tag"]]
        catalog["packages"] = old_packages + [entry]
        artifacts = {a["path"]: a for a in catalog["artifacts"]}
        artifacts.update((str(path.relative_to(PUBLIC)), artifact(path)) for path in changed)
        catalog["artifacts"] = [artifacts[key] for key in sorted(artifacts)]
        catalog["updated"] = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
        catalog["tap_commit"] = subprocess.check_output(
            ["git", f"--git-dir={PUBLIC / 'homebrew-tap.git'}", "rev-parse", "HEAD"], text=True).strip()
        pending = stage / ("catalog-" + backup.name + ".json")
        pending.write_text(json.dumps(catalog, indent=2) + "\n")
        subprocess.run(["gpg", "--homedir", str(KEYHOME), "--batch", "--local-user", KEY,
                        "--armor", "--detach-sign", str(pending)], check=True)
        subprocess.run(["gpgv", "--keyring", str(PUBLIC / "goblinreactor-archive-keyring.gpg"),
                        str(pending) + ".asc", str(pending)], check=True)
        atomic_bytes(catalog_path, pending.read_bytes())
        atomic_bytes(signature, Path(str(pending) + ".asc").read_bytes())
        print(json.dumps({"result": "published", "backup": str(backup),
                          "version": entry["version"], "tap_commit": catalog["tap_commit"],
                          "packages": [p["name"] for p in catalog["packages"]]}, indent=2))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--stage", type=Path, required=True)
    publish(parser.parse_args().stage)
