#!/usr/bin/env python3
"""
publish_wiki_page.py — programmatic reference for publishing to the Adom Wiki.

WHY THIS FILE EXISTS
--------------------
So a new AI session (or teammate) does NOT have to re-figure-out how to publish a
wiki page on the fly. Read these functions top-to-bottom and you understand the whole
lifecycle: auth → locate → clone → edit → snapshot → (preview+approve) → push → verify,
plus publishing a brand-new page. Every non-obvious step has a comment explaining the
"why" and the gotcha that bit us the first time.

HOW IT WORKS
------------
The Adom Wiki has ONE CLI: `adom-wiki` (a Rust binary, every call prints JSON). This
module is a thin, documented wrapper around it — there is no separate REST SDK to learn;
`adom-wiki api <url>`-style access exists but the pillar verbs below are the supported
surface. Everything here shells out to `adom-wiki` and parses the JSON, so the behaviour
is identical to what you'd type by hand — this file just makes the sequence copy-pasteable
and encodes the rules from the SKILL (pull-before-edit, snapshot, real changelog, verify).

USAGE
-----
    from publish_wiki_page import (
        ensure_author, find_page_owner, clone_page, snapshot, push_files,
        set_hero, verify_asset, publish_new_package,
    )

    # --- update an existing page ---
    owner = find_page_owner("in-s42atr")          # pages may live under aravk/, not adom/
    work = clone_page(owner, "in-s42atr", "/tmp/edit-in-s42atr")
    snap = snapshot(work)                          # revert insurance
    # ... edit files under `work` ...
    # ... render a PREVIEW and get EXPLICIT human approval (NOT done here) ...
    push_files(owner, "in-s42atr", ["IN-S42ATR.html"],
               "Fix brightness-vs-temp curve to match datasheet page 5")
    verify_asset("in-s42atr", "IN-S42ATR.html")    # confirm the blob is live

    # --- publish a brand-new page/skill/component ---
    publish_new_package("/path/to/pkg", org="adom", private=True,
                        changelog="Initial <slug> v0.1.0 — <what it is>")

This file is deliberately dependency-free (stdlib only) so it runs anywhere the
`adom-wiki` binary is on PATH.
"""

from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.request
import urllib.error

TOKEN_PATH = os.path.expanduser("~/.config/adom-wiki/token")  # set by `adom-wiki set-author`

WIKI_HOST = "https://wiki.adom.inc"           # public path — verify assets here, not localhost
BLOB = WIKI_HOST + "/blob/component/{slug}/{file}"   # the URL the page + 3D viewer fetch


# --------------------------------------------------------------------------- #
#  Low-level: run the adom-wiki CLI and parse its JSON envelope.
# --------------------------------------------------------------------------- #
def wiki(*args: str, check: bool = True) -> dict | list | str:
    """Run `adom-wiki <args>` and return parsed JSON (or raw text if not JSON).

    Every adom-wiki verb prints a JSON envelope to stdout. We parse it. On a
    non-zero exit we raise with the stderr so failures are loud, not silent.
    """
    proc = subprocess.run(["adom-wiki", *args], capture_output=True, text=True)
    if check and proc.returncode != 0:
        raise RuntimeError(f"adom-wiki {' '.join(args)} failed:\n{proc.stderr or proc.stdout}")
    out = proc.stdout.strip()
    try:
        return json.loads(out)
    except json.JSONDecodeError:
        return out


# --------------------------------------------------------------------------- #
#  Auth — WIKI_STRICT_AUTH is ON: real author identity + real changelog required.
# --------------------------------------------------------------------------- #
def whoami() -> dict:
    """Who will publishes be attributed to? (server-controlled, from the bearer token)."""
    return wiki("whoami")


def ensure_author() -> None:
    """Make sure we publish under a real Adom identity, not the shared 'Developer' token.

    The CLI hard-blocks publishing as the shared identity. `set-author` SSOs the
    container's Adom identity into the wiki (writes a token to ~/.config/adom-wiki/).
    Idempotent — safe to call every run.
    """
    me = whoami()
    user = (me or {}).get("user", {}) if isinstance(me, dict) else {}
    if not user.get("email"):
        wiki("set-author")


# --------------------------------------------------------------------------- #
#  Locate — a page's OWNER is not always `adom`. Basic-parts live under `aravk/`.
# --------------------------------------------------------------------------- #
def page_exists(owner: str, slug: str) -> bool:
    """True if <owner>/<slug> resolves. A 404 here does NOT mean the page doesn't
    exist — only that it doesn't exist UNDER THAT OWNER (see find_page_owner)."""
    res = wiki("page", "stats", f"{owner}/{slug}", check=False)
    if isinstance(res, dict):
        return res.get("http_status") not in (404, "404") and "PAGE_NOT_FOUND" not in json.dumps(res)
    return False


def find_page_owner(slug: str, candidates: list[str] | None = None) -> str | None:
    """Find which owner a component page lives under. GOTCHA: the basic-parts pages
    are owned by `aravk`, not `adom` — a naive `adom/<slug>` check 404s. We also
    fall back to full-text discovery to catch any other owner."""
    for owner in (candidates or ["aravk", "adom"]):
        if page_exists(owner, slug):
            return owner
    # last resort: search and read the owner off the first exact-slug hit
    res = wiki("discover", "search", "--query", slug, "--limit", "10", check=False)
    for hit in (res.get("results", []) if isinstance(res, dict) else []):
        if hit.get("slug") == slug:
            return hit.get("owner")
    return None


# --------------------------------------------------------------------------- #
#  Clone / snapshot — ALWAYS pull fresh before editing; ALWAYS snapshot before push.
# --------------------------------------------------------------------------- #
def clone_page(owner: str, slug: str, dest: str) -> str:
    """Pull the page's repo fresh into `dest`. RULE: do this immediately before you
    edit — someone else may have pushed since your last pull, and a stale local copy
    will clobber their work on push."""
    if os.path.exists(dest):
        shutil.rmtree(dest)
    wiki("repo", "clone", f"{owner}/{slug}", "--dir", dest)
    return dest


def snapshot(work_dir: str, stamp: str | None = None) -> str:
    """Copy the pristine pulled state to a timestamped folder so revert is a one-liner.
    Pass `stamp` explicitly (Date.now-style calls are unavailable in some sandboxes)."""
    stamp = stamp or str(int(time.time()))
    snap = f"{work_dir.rstrip('/')}-snapshot-{stamp}"
    shutil.copytree(work_dir, snap)
    print(f"snapshot: {snap}   (revert with: cp -r {snap}/* {work_dir}/ && push)")
    return snap


# --------------------------------------------------------------------------- #
#  Push — only the files you changed, only this page, with a REAL changelog.
# --------------------------------------------------------------------------- #
def push_files(owner: str, slug: str, files: list[str], changelog: str) -> dict:
    """Commit + push `files` (paths RELATIVE to the page repo root) to <owner>/<slug>.

    RULES enforced by the server (WIKI_STRICT_AUTH):
      - changelog must be >=10 chars, >=2 words, and NOT a placeholder ('update',
        'release v1', ...). Make it describe the actual change (cite the datasheet
        page for characterization edits).
      - only the files you list are pushed. Never push files you didn't change and
        never touch another page.
    Run this from inside the page's working dir, or pass repo-relative paths.
    """
    if len(changelog.split()) < 2 or len(changelog) < 10:
        raise ValueError(f"changelog too weak (needs >=2 words, >=10 chars): {changelog!r}")
    return wiki("repo", "push", "--files", *files, "-m", changelog, f"{owner}/{slug}")


def repo_log(owner: str, slug: str, limit: int = 5) -> dict:
    """Like `git log` for the page — confirm ONLY your commit landed after a push."""
    return wiki("repo", "log", f"{owner}/{slug}", "--limit", str(limit))


# --------------------------------------------------------------------------- #
#  Hero — the page card image. GOTCHA: this verb takes NO --changelog.
# --------------------------------------------------------------------------- #
def set_hero(owner: str, slug: str, image_rel_path: str) -> dict:
    """Set the page hero/billboard image (path relative to the page repo)."""
    return wiki("page", "hero", f"{owner}/{slug}", "--image", image_rel_path)


# --------------------------------------------------------------------------- #
#  Verify — fetch the served blob (the path the page + viewer actually fetch),
#  NOT localhost. GOTCHA: private pages return 403 to UNAUTHENTICATED requests, so
#  we send the same bearer token adom-wiki uses. Comparing the returned byte_count
#  to the file you pushed is the real proof (a stable URL means a 200 alone doesn't
#  tell you the *new* bytes are live — an in-place overwrite keeps the URL).
# --------------------------------------------------------------------------- #
def verify_asset(slug: str, filename: str, timeout: int = 15) -> tuple[int, int]:
    """GET the served blob (authenticated) and return (http_status, byte_count).
    Cross-check byte_count against the local file's size. If this still 403s, the
    push still landed — confirm with `repo_log(...)` that your commit is at HEAD."""
    req = urllib.request.Request(BLOB.format(slug=slug, file=filename))
    if os.path.exists(TOKEN_PATH):
        with open(TOKEN_PATH) as fh:
            req.add_header("Authorization", "Bearer " + fh.read().strip())
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return r.status, len(r.read())
    except urllib.error.HTTPError as e:
        return e.code, 0


# --------------------------------------------------------------------------- #
#  New page — scaffold + publish a fresh page/skill/component/app.
# --------------------------------------------------------------------------- #
def init_package(slug: str, ptype: str = "component", title: str | None = None) -> dict:
    """Scaffold a new package dir (package.json + README + SKILL/skeleton) in ./<slug>/.
    ptype ∈ {app, skill, component, bootstrap}."""
    args = ["pkg", "init", slug, "--type", ptype]
    if title:
        args += ["--title", title]
    return wiki(*args)


def lint_package(pkg_dir: str) -> dict:
    """Pre-publish linter. errors[] must be empty to publish (hero image is a common
    blocker); warnings[] (e.g. no README screenshots) do not block."""
    cwd = os.getcwd()
    try:
        os.chdir(pkg_dir)
        return wiki("pkg", "lint")
    finally:
        os.chdir(cwd)


def publish_new_package(pkg_dir: str, org: str | None = None, private: bool = False,
                        changelog: str = "") -> dict:
    """Publish the package in `pkg_dir` (contains package.json). For an org-owned
    private page: org='adom', private=True. Lints first and refuses on hard errors."""
    lint = lint_package(pkg_dir)
    if isinstance(lint, dict) and lint.get("errors"):
        raise RuntimeError(f"lint errors block publish: {lint['errors']}")
    args = ["pkg", "publish"]
    if org:
        args += ["--org", org]
    args += ["--private"] if private else ["--public"]
    if changelog:
        args += ["-m", changelog]
    cwd = os.getcwd()
    try:
        os.chdir(pkg_dir)
        return wiki(*args)
    finally:
        os.chdir(cwd)


# --------------------------------------------------------------------------- #
#  Worked flows — read these to see the full sequence. Guarded so importing or
#  running with no args does nothing destructive.
# --------------------------------------------------------------------------- #
def _demo_update_existing(slug: str, changed_files: list[str], changelog: str) -> None:
    """The canonical 'edit an existing component page' flow. This function does the
    SAFE steps automatically (auth, locate, clone, snapshot) and STOPS before push so
    a human can preview + approve — publishing is never silent."""
    ensure_author()
    owner = find_page_owner(slug)
    if not owner:
        raise SystemExit(f"no page found for slug {slug!r}")
    work = clone_page(owner, slug, f"/tmp/edit-{slug}")
    snapshot(work)
    print(f"\nPulled {owner}/{slug} into {work}.")
    print("NEXT (do by hand): edit the files, render a PREVIEW, get EXPLICIT approval, then:")
    print(f'  push_files("{owner}", "{slug}", {changed_files!r}, "{changelog}")')
    print(f'  verify_asset("{slug}", "{changed_files[0]}")')


if __name__ == "__main__":
    if len(sys.argv) >= 2 and sys.argv[1] == "whoami":
        print(json.dumps(whoami(), indent=2))
    elif len(sys.argv) >= 3 and sys.argv[1] == "owner":
        print(find_page_owner(sys.argv[2]))
    else:
        print(__doc__)
        print("Quick checks:  python3 publish_wiki_page.py whoami")
        print("               python3 publish_wiki_page.py owner <slug>")