#!/usr/bin/env python3
"""Automate README/CHANGELOG release docs updates.

Usage:
  scripts/prepare_release_docs.py v0.4.6
  scripts/prepare_release_docs.py v0.4.6 --date 2026-02-12
  scripts/prepare_release_docs.py 0.4.6 --date 2026-02-12
  scripts/prepare_release_docs.py v0.4.6 --check
"""

from __future__ import annotations

import argparse
import datetime as dt
import pathlib
import re
import sys


ROOT = pathlib.Path(__file__).resolve().parents[1]
README = ROOT / "README.md"
CHANGELOG = ROOT / "CHANGELOG.md"
WELCOME_TOUR_SWIFT = ROOT / "Neon Vision Editor" / "UI" / "PanelsAndHelpers.swift"


def normalize_tag(raw: str) -> str:
    raw = raw.strip()
    if not raw:
        raise ValueError("Tag cannot be empty.")
    return raw if raw.startswith("v") else f"v{raw}"


def read_text(path: pathlib.Path) -> str:
    if not path.exists():
        raise FileNotFoundError(f"Missing file: {path}")
    return path.read_text(encoding="utf-8")


def write_text(path: pathlib.Path, content: str) -> None:
    path.write_text(content, encoding="utf-8")


def has_changelog_section(changelog: str, tag: str) -> bool:
    return re.search(rf"^## \[{re.escape(tag)}\] - \d{{4}}-\d{{2}}-\d{{2}}$", changelog, flags=re.M) is not None


def add_changelog_section(changelog: str, tag: str, date: str) -> str:
    heading = f"## [{tag}] - {date}"
    template = (
        f"{heading}\n\n"
        "### Hero Screenshot\n"
        "- ![TODO hero screenshot](docs/images/TODO-release-hero.png)\n\n"
        "### Why Upgrade\n"
        "- TODO\n"
        "- TODO\n"
        "- TODO\n\n"
        "### Highlights\n"
        "- TODO\n\n"
        "### Fixes\n"
        "- TODO\n\n"
        "### Breaking changes\n"
        "- None.\n\n"
        "### Migration\n"
        "- None.\n\n"
    )
    first_release = re.search(r"^## \[", changelog, flags=re.M)
    if not first_release:
        return changelog.rstrip() + "\n\n" + template
    idx = first_release.start()
    return changelog[:idx] + template + changelog[idx:]


def extract_changelog_section_meta(changelog: str, tag: str) -> tuple[str, str]:
    pattern = re.compile(
        rf"^## \[{re.escape(tag)}\] - (?P<date>\d{{4}}-\d{{2}}-\d{{2}})\n(?P<body>.*?)(?=^## \[|\Z)",
        flags=re.M | re.S,
    )
    match = pattern.search(changelog)
    if not match:
        raise ValueError(f"Could not find CHANGELOG section for {tag}")
    return (match.group("date"), match.group("body").strip())


def extract_changelog_section(changelog: str, tag: str) -> str:
    _, body = extract_changelog_section_meta(changelog, tag)
    return body


def summarize_section(section_body: str, limit: int = 5) -> list[str]:
    bullets: list[str] = []
    for line in section_body.splitlines():
        stripped = line.strip()
        if stripped.startswith("- "):
            bullets.append(stripped)
    if not bullets:
        return ["- See CHANGELOG.md entry."]
    return bullets[:limit]


def extract_release_headings(changelog: str) -> list[str]:
    return re.findall(r"^## \[(v[^\]]+)\] - \d{4}-\d{2}-\d{2}$", changelog, flags=re.M)


def is_prerelease_tag(tag: str) -> bool:
    return "-" in tag


def previous_release_tag(changelog: str, tag: str) -> str | None:
    headings = extract_release_headings(changelog)
    if tag not in headings:
        return None
    idx = headings.index(tag)
    # For stable releases, skip prerelease tags when computing "since ...".
    # Example: v0.4.8 should show v0.4.7, not v0.4.4-beta.
    for candidate in headings[idx + 1 :]:
        if not is_prerelease_tag(tag) and is_prerelease_tag(candidate):
            continue
        return candidate
    return None


def swift_string(value: str) -> str:
    return value.replace("\\", "\\\\").replace('"', '\\"')


def update_welcome_tour_release_page(swift_source: str, tag: str, bullets: list[str], prev_tag: str | None) -> str:
    if not bullets:
        bullet_lines = ['                "See CHANGELOG.md for details"']
    else:
        bullet_lines = [f'                "{swift_string(bullet[2:])}"' for bullet in bullets]

    if prev_tag:
        subtitle = f"Major changes since {prev_tag}:"
    else:
        subtitle = f"Highlights for {tag}:"

    new_block = (
        "        TourPage(\n"
        '            title: "What\u2019s New in This Release",\n'
        f'            subtitle: "{swift_string(subtitle)}",\n'
        "            bullets: [\n"
        + ",\n".join(bullet_lines)
        + "\n"
        "            ],\n"
        '            iconName: "sparkles.rectangle.stack",\n'
        "            colors: [Color(red: 0.40, green: 0.28, blue: 0.90), Color(red: 0.96, green: 0.46, blue: 0.55)],\n"
        "            toolbarItems: []\n"
        "        ),"
    )

    pattern = re.compile(
        r'        TourPage\(\n'
        r'            title: "What[^\n]*This Release",\n'
        r"            subtitle: [^\n]*\n"
        r"            bullets: \[\n"
        r".*?"
        r"            \],\n"
        r'            iconName: "sparkles\.rectangle\.stack",\n'
        r"            colors: \[Color\(red: 0\.40, green: 0\.28, blue: 0\.90\), Color\(red: 0\.96, green: 0\.46, blue: 0\.55\)\],\n"
        r"            toolbarItems: \[\]\n"
        r"        \),",
        flags=re.S,
    )

    if not pattern.search(swift_source):
        raise ValueError("Could not find Welcome Tour 'What's New' page block to update.")
    return pattern.sub(new_block, swift_source, count=1)


def extract_heading_bullets(section_body: str, heading: str, limit: int = 5) -> list[str]:
    bullets: list[str] = []
    in_heading = False
    target = f"### {heading}"
    for line in section_body.splitlines():
        stripped = line.strip()
        if stripped.startswith("### "):
            in_heading = stripped == target
            continue
        if in_heading and stripped.startswith("- "):
            bullets.append(stripped[2:].strip())
            if len(bullets) >= limit:
                break
    return bullets


def compact_bullets(items: list[str], default: str) -> str:
    if not items:
        return default
    return "; ".join(items)


def clean_release_cell_item(item: str) -> str:
    text = item.strip()
    text = re.sub(r"^(Added|Improved|Fixed|Fixes)\s+", "", text, flags=re.I)
    return text.rstrip(".")


def normalize_none_value(value: str, default: str) -> str:
    compact = value.strip().rstrip(".")
    if not compact:
        return default
    if compact.lower() in {"none", "none noted", "none required", "n/a", "not applicable"}:
        return default
    return value


def parse_version_key(tag: str) -> tuple[int, int, int, int, str]:
    """
    Sort key for tags like v1.2.3 and v1.2.3-beta.
    Stable releases sort above prereleases of the same numeric version.
    """
    match = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?", tag)
    if not match:
        return (0, 0, 0, 0, tag)
    major = int(match.group(1))
    minor = int(match.group(2))
    patch = int(match.group(3))
    prerelease = match.group(4)
    stability_rank = 1 if prerelease is None else 0
    prerelease_text = prerelease or ""
    return (major, minor, patch, stability_rank, prerelease_text)


def sorted_latest_tags(tags: list[str], limit: int, ensure_tag: str | None = None) -> list[str]:
    unique: list[str] = []
    seen: set[str] = set()
    for tag in tags:
        if tag in seen:
            continue
        seen.add(tag)
        unique.append(tag)
    ordered = sorted(unique, key=parse_version_key, reverse=True)
    top = ordered[:limit]
    if ensure_tag and ensure_tag not in top:
        top = [ensure_tag] + top[: max(0, limit - 1)]
    return top


def build_readme_release_row(tag: str, date: str, section_body: str) -> str:
    highlights_items = extract_heading_bullets(section_body, "Highlights", limit=4)
    if not highlights_items:
        highlights_items = extract_heading_bullets(section_body, "Added", limit=2) + extract_heading_bullets(
            section_body, "Improved", limit=1
        )
    fixes_items = extract_heading_bullets(section_body, "Fixes", limit=3)
    if not fixes_items:
        fixes_items = extract_heading_bullets(section_body, "Fixed", limit=3)

    highlights = compact_bullets([clean_release_cell_item(x) for x in highlights_items], "See CHANGELOG.")
    fixes = compact_bullets([clean_release_cell_item(x) for x in fixes_items], "None noted")
    breaking_items = extract_heading_bullets(section_body, "Breaking changes", limit=1)
    migration_items = extract_heading_bullets(section_body, "Migration", limit=1)

    breaking = normalize_none_value(breaking_items[0], "None noted") if breaking_items else "None noted"
    migration = normalize_none_value(migration_items[0], "None required") if migration_items else "None required"

    return (
        f"| [`{tag}`](https://github.com/h3pdesign/Neon-Vision-Editor/releases/tag/{tag}) | "
        f"{date} | {highlights} | {fixes} | {breaking} | {migration} |"
    )


def rebuild_readme_changelog_table(readme: str, changelog: str, current_tag: str, limit: int = 3) -> str:
    pattern = re.compile(
        r"(### Recent Releases \(At a glance\)\n\n"
        r"\| Version \| Date \| Highlights \| Fixes \| Breaking changes \| Migration \|\n"
        r"\|---\|---\|---\|---\|---\|---\|\n)"
        r"(?P<rows>.*?)(?=\n- Full release history:)",
        flags=re.S,
    )
    match = pattern.search(readme)
    if not match:
        raise ValueError("README missing changelog at-a-glance table block")

    tags = extract_release_headings(changelog)
    top_tags = sorted_latest_tags(tags, limit=limit, ensure_tag=current_tag)
    rows: list[str] = []
    for tag in top_tags:
        date, section = extract_changelog_section_meta(changelog, tag)
        rows.append(build_readme_release_row(tag, date, section))

    rows_block = "\n".join(rows) + "\n"
    return readme[: match.start("rows")] + rows_block + readme[match.end("rows") :]


def update_readme_latest_stable_line(readme: str, tag: str, changelog: str) -> str:
    date, _ = extract_changelog_section_meta(changelog, tag)
    return re.sub(
        r"(?m)^Latest stable: \*\*.*\*\* \(\d{4}-\d{2}-\d{2}\)$",
        f"Latest stable: **{tag}** ({date})",
        readme,
    )


def update_readme_release_refs(readme: str, tag: str) -> str:
    readme = re.sub(
        r"(?m)^> Latest release: \*\*.*\*\*$",
        f"> Latest release: **{tag}**",
        readme,
    )
    readme = re.sub(
        r"(?m)^- Latest release: \*\*.*\*\*$",
        f"- Latest release: **{tag}**",
        readme,
    )
    readme = re.sub(
        r"(?m)^- Tag: `.*`$",
        f"- Tag: `{tag}`",
        readme,
    )
    readme = re.sub(
        r"(?m)^git rev-parse --verify .*$",
        f"git rev-parse --verify {tag}",
        readme,
    )
    return readme


def update_readme_whats_new_heading(readme: str, previous_tag: str | None, current_tag: str) -> str:
    if previous_tag:
        replacement = f"## What's New Since {previous_tag}"
    else:
        replacement = f"## What's New in {current_tag}"
    return re.sub(
        r"(?m)^## What's New Since [^\n]+$|^## What's New in [^\n]+$",
        replacement,
        readme,
    )


def parse_stable_semver(tag: str) -> tuple[int, int, int] | None:
    match = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)", tag)
    if not match:
        return None
    return int(match.group(1)), int(match.group(2)), int(match.group(3))


def update_readme_roadmap_windows(readme: str, tag: str) -> str:
    stable = parse_stable_semver(tag)
    if stable is None:
        return readme

    major, minor, patch = stable
    now_start = max(0, patch - 2)
    now_end = patch
    next_start = patch + 1
    next_end = patch + 3

    now_badge = (
        f'<img alt="Now" src="https://img.shields.io/badge/NOW-v{major}.{minor}.{now_start}%20to%20'
        f'v{major}.{minor}.{now_end}-22C55E?style=for-the-badge">'
    )
    next_badge = (
        f'<img alt="Next" src="https://img.shields.io/badge/NEXT-v{major}.{minor}.{next_start}%20to%20'
        f'v{major}.{minor}.{next_end}-F59E0B?style=for-the-badge">'
    )

    readme = re.sub(
        r'(?m)^  <img alt="Now" src="https://img\.shields\.io/badge/NOW-v[^"]+">$',
        f"  {now_badge}",
        readme,
    )
    readme = re.sub(
        r'(?m)^  <img alt="Next" src="https://img\.shields\.io/badge/NEXT-v[^"]+">$',
        f"  {next_badge}",
        readme,
    )
    readme = re.sub(
        r"(?m)^### Now \(v\d+\.\d+\.\d+ - v\d+\.\d+\.\d+\)$",
        f"### Now (v{major}.{minor}.{now_start} - v{major}.{minor}.{now_end})",
        readme,
    )
    readme = re.sub(
        r"(?m)^### Next \(v\d+\.\d+\.\d+ - v\d+\.\d+\.\d+\)$",
        f"### Next (v{major}.{minor}.{next_start} - v{major}.{minor}.{next_end})",
        readme,
    )
    return readme


def update_readme_compare_link(readme: str, prev_tag: str | None, current_tag: str) -> str:
    if not prev_tag:
        return readme
    return re.sub(
        r"(?m)^- Compare recent changes: \[v[^]]+\.\.\.v[^]]+\]\(https://github\.com/h3pdesign/Neon-Vision-Editor/compare/v[^)]+\)$",
        (
            f"- Compare recent changes: [{prev_tag}...{current_tag}]"
            f"(https://github.com/h3pdesign/Neon-Vision-Editor/compare/{prev_tag}...{current_tag})"
        ),
        readme,
    )


def pick_feature_spotlight(section_body: str) -> str:
    highlight_items = extract_heading_bullets(section_body, "Highlights", limit=8)
    preferred_keywords = ("share shot", "code snapshot", "camera.viewfinder", "snapshot")
    for item in highlight_items:
        lowered = item.lower()
        if any(keyword in lowered for keyword in preferred_keywords):
            cleaned = clean_release_cell_item(item)
            if cleaned:
                return cleaned
    for item in highlight_items:
        cleaned = clean_release_cell_item(item)
        if cleaned:
            return cleaned

    added_items = extract_heading_bullets(section_body, "Added", limit=8)
    for item in added_items:
        lowered = item.lower()
        if any(keyword in lowered for keyword in preferred_keywords):
            cleaned = clean_release_cell_item(item)
            if cleaned:
                return cleaned
    for item in added_items:
        cleaned = clean_release_cell_item(item)
        if cleaned:
            return cleaned

    fallback = summarize_section(section_body, limit=1)
    if fallback:
        return clean_release_cell_item(fallback[0][2:].strip())
    return "See CHANGELOG.md release highlights."


def update_readme_feature_spotlight(readme: str, tag: str, section_body: str) -> str:
    feature_text = pick_feature_spotlight(section_body).rstrip(".")
    badge = f'https://img.shields.io/badge/NEW%20FEATURE-{tag}-F97316?style=for-the-badge'
    featured_line = f"**Featured in {tag}:** {feature_text}."

    readme = re.sub(
        r'(?m)^  <img alt="New Feature Release" src="https://img\.shields\.io/badge/NEW%20FEATURE-v[^"]+">$',
        f'  <img alt="New Feature Release" src="{badge}">',
        readme,
    )
    readme = re.sub(
        r"(?m)^\*\*Featured in v[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.]+)?:\*\* .*$",
        featured_line,
        readme,
    )
    return readme


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Prepare README and CHANGELOG for a release tag.")
    parser.add_argument("tag", help="Release tag, e.g. v0.4.6")
    parser.add_argument(
        "--date",
        help="Release date for a new CHANGELOG section (YYYY-MM-DD). Defaults to today.",
    )
    parser.add_argument(
        "--check",
        action="store_true",
        help="Verify release docs are already up to date without writing files.",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    tag = normalize_tag(args.tag)
    release_date = args.date or dt.date.today().isoformat()

    original_changelog = read_text(CHANGELOG)
    changelog = original_changelog
    if not has_changelog_section(changelog, tag):
        changelog = add_changelog_section(changelog, tag, release_date)
        if not args.check:
            print(f"Added CHANGELOG template for {tag} ({release_date}).")
    elif not args.check:
        print(f"Found existing CHANGELOG section for {tag}.")

    section = extract_changelog_section(changelog, tag)
    bullets = summarize_section(section, limit=5)

    original_readme = read_text(README)
    readme = update_readme_release_refs(original_readme, tag)
    prev_tag = previous_release_tag(changelog, tag)
    readme = update_readme_whats_new_heading(readme, prev_tag, tag)
    readme = update_readme_roadmap_windows(readme, tag)
    readme = update_readme_compare_link(readme, prev_tag, tag)
    readme = update_readme_feature_spotlight(readme, tag, section)
    readme = update_readme_latest_stable_line(readme, tag, changelog)
    readme = rebuild_readme_changelog_table(readme, changelog, tag, limit=3)

    original_welcome_src = read_text(WELCOME_TOUR_SWIFT)
    welcome_src = update_welcome_tour_release_page(original_welcome_src, tag, bullets[:4], prev_tag)

    if args.check:
        outdated_files: list[str] = []
        if changelog != original_changelog:
            outdated_files.append(str(CHANGELOG))
        if readme != original_readme:
            outdated_files.append(str(README))
        if welcome_src != original_welcome_src:
            outdated_files.append(str(WELCOME_TOUR_SWIFT))
        if outdated_files:
            print(f"Release docs are not up to date for {tag}.", file=sys.stderr)
            print("Run: scripts/prepare_release_docs.py {}{}".format(tag, f" --date {release_date}" if args.date else ""), file=sys.stderr)
            print("Outdated files:", file=sys.stderr)
            for path in outdated_files:
                print(f"- {path}", file=sys.stderr)
            return 1
        print(f"Release docs are up to date for {tag}.")
        return 0

    write_text(CHANGELOG, changelog)
    write_text(README, readme)
    write_text(WELCOME_TOUR_SWIFT, welcome_src)
    print("Updated README release references and top 3 release rows.")
    print(f"Updated Welcome Tour release page from CHANGELOG for {tag}.")

    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as exc:  # pragma: no cover - CLI friendly
        print(f"error: {exc}", file=sys.stderr)
        raise SystemExit(1)
