#!/usr/bin/env python3
"""Build frontend assets with content hashes for cache-immutable PWA deploy."""

from __future__ import annotations

import hashlib
import json
import re
import shutil
import tomllib
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
FRONTEND = ROOT / "frontend"
PUBLIC = ROOT / "public"
ASSETS_DIR = PUBLIC / "assets"

HASH_LEN = 8
IMPORT_RE = re.compile(r'''from\s+["'](\./(?:lib/)?[^"']+\.js)["']''')

STATIC_COPY = ("manifest.json",)
STATIC_ICONS = (
    "icon.png",
    "icon-72x72.png",
    "icon-192x192.png",
    "icon-512x512.png",
)


def short_hash(content: bytes) -> str:
    return hashlib.sha256(content).hexdigest()[:HASH_LEN]


def js_sources() -> list[tuple[str, str]]:
    sources: list[tuple[str, str]] = [("app.js", "app")]
    lib_dir = FRONTEND / "lib"
    if lib_dir.is_dir():
        for path in sorted(lib_dir.glob("*.js")):
            sources.append((f"lib/{path.name}", path.stem))
    return sources


def read_version() -> str:
    pyproject = ROOT / "pyproject.toml"
    with pyproject.open("rb") as fh:
        data = tomllib.load(fh)
    return str(data["project"]["version"])


def rewrite_imports(source: str, stem_to_filename: dict[str, str]) -> str:
    def replace(match: re.Match[str]) -> str:
        import_path = match.group(1)
        stem = Path(import_path).stem
        hashed = stem_to_filename.get(stem)
        if not hashed:
            raise ValueError(f"Unknown import stem {stem!r} in {import_path!r}")
        quote = match.group(0).split("from", 1)[1].strip()[0]
        return f'from {quote}./{hashed}{quote}'

    return IMPORT_RE.sub(replace, source)


def clean_assets_dir() -> None:
    ASSETS_DIR.mkdir(parents=True, exist_ok=True)
    for path in ASSETS_DIR.iterdir():
        if path.name == ".gitkeep":
            continue
        if path.is_file():
            path.unlink()


def copy_static_files() -> list[str]:
    copied: list[str] = []
    for name in STATIC_COPY:
        src = FRONTEND / name
        if not src.is_file():
            raise FileNotFoundError(f"Missing frontend source: {src}")
        shutil.copy2(src, PUBLIC / name)
        copied.append(f"/{name}")

    for name in STATIC_ICONS:
        src = FRONTEND / name
        if src.is_file():
            shutil.copy2(src, PUBLIC / name)
            copied.append(f"/{name}")
    return copied


def build() -> dict[str, object]:
    if not FRONTEND.is_dir():
        raise FileNotFoundError(f"Frontend source directory not found: {FRONTEND}")

    PUBLIC.mkdir(parents=True, exist_ok=True)
    clean_assets_dir()

    hashed_assets: dict[str, str] = {}
    stem_to_filename: dict[str, str] = {}
    file_hashes: list[str] = []

    styles_src = FRONTEND / "styles.css"
    if not styles_src.is_file():
        raise FileNotFoundError(f"Missing frontend source: {styles_src}")
    styles_bytes = styles_src.read_bytes()
    styles_hash = short_hash(styles_bytes)
    styles_filename = f"styles.{styles_hash}.css"
    (ASSETS_DIR / styles_filename).write_bytes(styles_bytes)
    hashed_assets["styles"] = f"/assets/{styles_filename}"
    file_hashes.append(styles_hash)

    for rel_path, stem in js_sources():
        src = FRONTEND / rel_path
        if not src.is_file():
            raise FileNotFoundError(f"Missing frontend source: {src}")
        content = src.read_text(encoding="utf-8")
        content_hash = short_hash(content.encode("utf-8"))
        filename = f"{stem}.{content_hash}.js"
        stem_to_filename[stem] = filename
        file_hashes.append(content_hash)

    for rel_path, stem in js_sources():
        src = FRONTEND / rel_path
        content = rewrite_imports(src.read_text(encoding="utf-8"), stem_to_filename)
        filename = stem_to_filename[stem]
        (ASSETS_DIR / filename).write_text(content, encoding="utf-8")
        hashed_assets[stem] = f"/assets/{filename}"

    build_hash = short_hash("".join(file_hashes).encode("utf-8"))
    version = read_version()
    build_time = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    cache_version = f"tym-v{version}-{build_hash}"

    static_paths = copy_static_files()
    asset_paths = sorted(hashed_assets.values())
    sw_assets = ["/", "/index.html", *asset_paths, *static_paths, "/version.json"]

    index_template = (FRONTEND / "index.template.html").read_text(encoding="utf-8")
    index_html = (
        index_template.replace("{{STYLES_HREF}}", hashed_assets["styles"])
        .replace("{{APP_SCRIPT_SRC}}", hashed_assets["app"])
        .replace("{{BUILD_HASH}}", build_hash)
    )
    (PUBLIC / "index.html").write_text(index_html, encoding="utf-8")

    sw_template = (FRONTEND / "sw.template.js").read_text(encoding="utf-8")
    sw_js = (
        sw_template.replace("{{CACHE_VERSION}}", cache_version)
        .replace("{{ASSETS_JSON}}", json.dumps(sw_assets, indent=2))
    )
    (PUBLIC / "sw.js").write_text(sw_js, encoding="utf-8")

    version_json = {
        "version": version,
        "buildTime": build_time,
        "buildHash": build_hash,
    }
    (PUBLIC / "version.json").write_text(
        json.dumps(version_json, indent=2) + "\n",
        encoding="utf-8",
    )

    return {
        "version": version,
        "buildHash": build_hash,
        "cacheVersion": cache_version,
        "assets": hashed_assets,
        "static": static_paths,
    }


def main() -> None:
    result = build()
    print(f"Built frontend v{result['version']} ({result['buildHash']})")
    print(f"Cache: {result['cacheVersion']}")
    for stem, url in sorted(result["assets"].items()):
        print(f"  {stem}: {url}")


if __name__ == "__main__":
    main()
