#!/usr/bin/env python3
"""Package and deploy an editable ecommerce detail-page HTML to the cloud nginx directory.

Default target matches Qianliyun's Hermes cloud server setup:
  https://wwyl.yipeng.online/detail-pages/<slug>/

The script:
1. Reads a local detail HTML, usually 04_页面制作/detail_editable.html.
2. Copies referenced src/href/CSS url(...) assets into a clean package.
3. Rewrites ../ asset references so the deployed entry is /index.html.
4. Copies the latest canonical detail-page-editor.js and injects project/API metadata.
5. Optionally downsizes local image copies with macOS sips for faster external viewing.
6. rsyncs the package to /srv/detail-pages/public/<slug>/ on the cloud server.

It does not print or handle secrets.
"""
from __future__ import annotations

import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.parse
from pathlib import Path

DEFAULT_HOST = "175.27.229.243"
DEFAULT_USER = "ubuntu"
DEFAULT_REMOTE_ROOT = "/srv/detail-pages/public"
DEFAULT_URL_PREFIX = "https://wwyl.yipeng.online/detail-pages"
DEFAULT_API_BASE = "/detail-page-api"
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}


def collect_refs(html_text: str) -> list[str]:
    refs: list[str] = []
    for m in re.finditer(r"(?:src|href)=[\"']([^\"']+)", html_text, flags=re.I):
        refs.append(m.group(1))
    # Quoted CSS URLs handle filenames containing parentheses.
    for m in re.finditer(r"url\(\s*(['\"])(.*?)\1\s*\)", html_text, flags=re.I | re.S):
        refs.append(m.group(2))
    # Conservative unquoted CSS URL fallback.
    for m in re.finditer(r"url\(\s*([^'\")][^)]*?)\s*\)", html_text, flags=re.I | re.S):
        refs.append(m.group(1).strip())
    return list(dict.fromkeys(refs))


def is_local_ref(ref: str) -> bool:
    return not ref.startswith(("http://", "https://", "data:", "#", "mailto:", "tel:"))


def map_dest_rel(ref: str) -> Path:
    decoded = urllib.parse.unquote(ref.split("?", 1)[0].split("#", 1)[0])
    if decoded.startswith("../"):
        decoded = decoded[3:]
    if decoded.startswith("./"):
        decoded = decoded[2:]
    return Path(decoded)


def copy_refs(html_path: Path, out_dir: Path, refs: list[str]) -> list[dict]:
    copied = []
    for ref in refs:
        if not is_local_ref(ref):
            continue
        decoded = urllib.parse.unquote(ref.split("?", 1)[0].split("#", 1)[0])
        src = (html_path.parent / decoded).resolve()
        if not src.exists() or not src.is_file():
            copied.append({"ref": ref, "status": "missing", "resolved": str(src)})
            continue
        dest_rel = map_dest_rel(ref)
        dest = out_dir / dest_rel
        dest.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src, dest)
        copied.append({"ref": ref, "status": "copied", "dest": str(dest_rel), "bytes": dest.stat().st_size})
    return copied


def ensure_editor_script(html_text: str, slug: str, api_base: str, cache_bust: str = "") -> str:
    """Ensure the editable plugin script has stable project/API metadata."""
    src = "detail-page-editor.js" + (f"?v={cache_bust}" if cache_bust else "")
    attrs = f'src="{src}" data-detail-page-editor data-dpe-project-id="{slug}" data-dpe-api-base="{api_base}"'
    script_tag = f"<script {attrs}></script>"

    def replace_script(match: re.Match[str]) -> str:
        tag = match.group(0)
        if "detail-page-editor.js" not in tag and "data-detail-page-editor" not in tag:
            return tag
        return script_tag

    pattern = re.compile(r"<script\b(?=[^>]*(?:detail-page-editor\.js|data-detail-page-editor))[^>]*>\s*</script>", re.I | re.S)
    html_text, count = pattern.subn(replace_script, html_text, count=1)
    if count:
        return html_text
    if "</body>" in html_text.lower():
        return re.sub(r"</body>", f"  {script_tag}\n</body>", html_text, count=1, flags=re.I)
    return html_text + "\n" + script_tag + "\n"


def rewrite_html(html_text: str, slug: str, api_base: str, cache_bust: str = "") -> str:
    # The deployed index.html sits at package root, while source HTML usually sits in 04_页面制作/.
    html_text = html_text.replace("../00_原始资料/", "00_原始资料/")
    html_text = html_text.replace("./detail-page-editor.js", "detail-page-editor.js")
    html_text = ensure_editor_script(html_text, slug, api_base, cache_bust)
    return html_text


def run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess:
    print("+", " ".join(cmd))
    return subprocess.run(cmd, check=check, text=True)


def optimize_images(out_dir: Path, max_px: int, quality: int) -> None:
    sips = shutil.which("sips")
    if not sips:
        print("warning: sips not found; skip image optimization", file=sys.stderr)
        return
    for p in out_dir.rglob("*"):
        if p.is_file() and p.suffix.lower() in IMAGE_SUFFIXES:
            subprocess.run([sips, "-Z", str(max_px), "-s", "formatOptions", str(quality), str(p)],
                           stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("html", type=Path, help="Path to detail_editable.html or detail.html")
    ap.add_argument("--slug", required=True, help="URL-safe directory name under /detail-pages/. Also used as plugin project_id.")
    ap.add_argument("--host", default=DEFAULT_HOST)
    ap.add_argument("--user", default=DEFAULT_USER)
    ap.add_argument("--remote-root", default=DEFAULT_REMOTE_ROOT)
    ap.add_argument("--url-prefix", default=DEFAULT_URL_PREFIX)
    ap.add_argument("--api-base", default=DEFAULT_API_BASE)
    ap.add_argument("--plugin-source", type=Path, default=Path(__file__).with_name("detail-page-editor.js"), help="Canonical latest plugin JS to copy into the package")
    ap.add_argument("--no-optimize", action="store_true")
    ap.add_argument("--max-image-px", type=int, default=1800)
    ap.add_argument("--quality", type=int, default=78)
    args = ap.parse_args()

    html_path = args.html.expanduser().resolve()
    if not html_path.exists():
        raise SystemExit(f"HTML not found: {html_path}")
    if not re.fullmatch(r"[A-Za-z0-9._-]+", args.slug):
        raise SystemExit("slug must contain only letters, numbers, dot, underscore, or hyphen")
    plugin_source = args.plugin_source.expanduser().resolve()
    if not plugin_source.exists():
        raise SystemExit(f"Plugin source not found: {plugin_source}")

    with tempfile.TemporaryDirectory(prefix="detail-page-public-") as td:
        out = Path(td) / args.slug
        out.mkdir(parents=True)
        text = html_path.read_text(encoding="utf-8")
        refs = collect_refs(text)
        copied = copy_refs(html_path, out, refs)
        shutil.copy2(plugin_source, out / "detail-page-editor.js")
        copied.append({"ref": str(plugin_source), "status": "copied", "dest": "detail-page-editor.js", "bytes": (out / "detail-page-editor.js").stat().st_size})
        cache_bust = str(int(plugin_source.stat().st_mtime))
        (out / "index.html").write_text(rewrite_html(text, args.slug, args.api_base, cache_bust), encoding="utf-8")
        manifest = {
            "source": str(html_path),
            "slug": args.slug,
            "project_id": args.slug,
            "api_base": args.api_base,
            "created_at": int(time.time()),
            "copied": copied,
        }
        (out / "deploy-manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
        if not args.no_optimize:
            optimize_images(out, args.max_image_px, args.quality)
        remote = f"{args.user}@{args.host}:{args.remote_root.rstrip('/')}/{args.slug}/"
        run(["rsync", "-az", "--delete", str(out) + "/", remote])

    url = f"{args.url_prefix.rstrip('/')}/{args.slug}/"
    print(f"URL: {url}")
    print(f"Project ID: {args.slug}")
    print(f"Version API: {args.api_base}/projects/{args.slug}/")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
