#!/usr/bin/env python3
"""Update Dart source package automatically.

Detects the latest stable Dart release, checks if it's already packaged or
pending in a PR, and if not, creates a new PR with the updated source ebuild
and virtual package.
"""

from __future__ import annotations
import sys

import argparse
import atexit
import json
import logging
import os
import re
import shutil
import subprocess
import tempfile
import urllib.request
from pathlib import Path

from source_update_utils import (
    carry_forward_versioned_files,
    inherit_previous_patch_files,
)

def parse_gentoo_version(version: str):
    import re
    match = re.match(r'^(\d+(?:\.\d+)*)(?:-r(\d+))?$', version)
    if not match: return ([0], 0)
    return ([int(x) for x in match.group(1).split('.')], int(match.group(2)) if match.group(2) else 0)

def compare_versions(v1: str, v2: str) -> int:
    p1 = parse_gentoo_version(v1)
    p2 = parse_gentoo_version(v2)
    if p1[0] > p2[0]: return 1
    if p1[0] < p2[0]: return -1
    if p1[1] > p2[1]: return 1
    if p1[1] < p2[1]: return -1
    return 0


# Adjust path so we can import the generator module if needed
sys.path.insert(0, str(Path(__file__).parent))

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")

DART_VERSION_URL = "https://storage.googleapis.com/dart-archive/channels/stable/release/latest/VERSION"
REPO_ROOT = Path(__file__).resolve().parents[1]

def run_cmd(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
    logging.info(f"Running: {' '.join(cmd)}")
    kwargs.setdefault("check", True)
    kwargs.setdefault("text", True)
    return subprocess.run(cmd, **kwargs)

def get_dart_release(version: str = None) -> dict:
    logging.info(f"Fetching Dart release metadata from {DART_VERSION_URL}")
    # Dart version URL actually just returns JSON for the latest version!
    # e.g. {"date": "2024-10-23", "version": "3.5.4", "revision": "bc37... "}
    # If a specific version is requested, we can't easily query a single file for it unless we use the archive list,
    # but the API allows fetching version info directly if we construct the URL.
    # Actually, we can fetch the specific version JSON from:
    # https://storage.googleapis.com/dart-archive/channels/stable/release/{version}/VERSION
    if version:
        url = f"https://storage.googleapis.com/dart-archive/channels/stable/release/{version}/VERSION"
    else:
        url = DART_VERSION_URL

    try:
        with urllib.request.urlopen(url) as response:
            data = json.loads(response.read().decode("utf-8"))
            ret_version = data.get("version")
            if not ret_version:
                raise ValueError("Failed to parse version from Dart VERSION file")
            if version and ret_version != version:
                raise ValueError(f"Requested Dart version {version} but metadata returned {ret_version}")
            if not data.get("revision"):
                raise ValueError("Dart VERSION metadata is missing revision field")
            return data
    except Exception as e:
        raise ValueError(f"Could not resolve Dart stable release metadata for {version or 'latest'}: {e}")

def get_latest_dart_version() -> str:
    return get_dart_release()["version"]

def ebuild_exists(version: str, orig_root) -> bool:
    """Check if the Dart ebuild for the version already exists."""
    dart_dir = orig_root / "dev-lang" / "dart"
    if not dart_dir.exists():
        return False
    for p in dart_dir.glob(f"dart-{version}*.ebuild"):
        return True
    return False

def check_dart_compatibility(required_sdk: str) -> bool:
    '''Enforce Flutter SDK constraints against the current main Dart installation.'''
    # In a full implementation, we'd parse the SDK range from pubspec.lock
    # and compare it against our ebuild tree. For now, we return True as a placeholder.
    return True

def check_superseding_release(pkg: str, version: str) -> bool:
    '''Detect if older updates are pending.'''
    try:
        cmd = ["gh", "pr", "list", "--state", "open", "--search", f"auto-update-{pkg}-", "--json", "title,url,headRefName"]
        result = run_cmd(cmd, capture_output=True)
        import json
        prs = json.loads(result.stdout)

        for pr in prs:
            pr_branch = pr["headRefName"]
            if not pr_branch.startswith(f"auto-update-{pkg}-"):
                continue
            pr_version = pr_branch.split(f"auto-update-{pkg}-")[-1]
            if compare_versions(pr_version, version) == -1:
                logging.info(f"Found older PR: {pr['url']} (version: {pr_version}) that will be superseded.")
            elif compare_versions(pr_version, version) == 1:
                logging.info(f"Found newer PR: {pr['url']} (version: {pr_version}). This run is superseded.")
                return True
        return False
    except subprocess.CalledProcessError as e:
        logging.error(f"gh CLI error: {e}")
        raise
    except FileNotFoundError:
        logging.error("gh CLI not found")
        raise

def close_superseding_releases(pkg: str, version: str):
    '''Close older updates.'''
    try:
        cmd = ["gh", "pr", "list", "--state", "open", "--search", f"auto-update-{pkg}-", "--json", "title,url,headRefName"]
        result = run_cmd(cmd, capture_output=True)
        import json
        prs = json.loads(result.stdout)

        for pr in prs:
            pr_branch = pr["headRefName"]
            if not pr_branch.startswith(f"auto-update-{pkg}-"):
                continue
            pr_version = pr_branch.split(f"auto-update-{pkg}-")[-1]
            if compare_versions(pr_version, version) == -1:
                logging.info(f"Closing superseded PR: {pr['url']} (version: {pr_version})")
                run_cmd(["gh", "pr", "close", pr_branch, "--comment", f"Superseded by {version}"])
    except subprocess.CalledProcessError as e:
        logging.error(f"gh CLI error: {e}")
        raise
    except FileNotFoundError:
        logging.error("gh CLI not found")
        raise

def check_branch_exists(branch_name: str) -> bool:
    try:
        res = run_cmd(["git", "ls-remote", "--exit-code", "--heads", "origin", f"refs/heads/{branch_name}"], check=False, capture_output=True)
        if res.returncode == 0:
            return True
        elif res.returncode == 2:
            return False
        else:
            logging.error("Failed to query remote branches.")
            sys.exit(1)
    except Exception as e:
        logging.error(f"Failed to query remote branches: {e}")
        sys.exit(1)

def check_pr_exists(branch_name: str) -> bool:
    try:
        res = run_cmd(["gh", "pr", "list", "--head", branch_name, "--json", "url", "--state", "open"], check=True, capture_output=True)
        prs = json.loads(res.stdout)
        return len(prs) > 0
    except subprocess.CalledProcessError as e:
        logging.error(f"gh CLI error: {e}")
        raise
    except FileNotFoundError:
        logging.error("gh CLI not available. Failing closed.")
        sys.exit(1)

def create_virtual_dart(version: str, work_root):
    # Resolve only providers actually packaged at this exact SDK version.
    # A newer virtual must not claim an older binary is compatible.
    from maintain_virtuals import ensure_virtual
    return ensure_virtual(work_root, "dart", version)


def create_dart_ebuild(version: str, work_root):
    # Find existing ebuild to copy from
    dart_dir = work_root / "dev-lang" / "dart"
    existing_ebuilds = list(dart_dir.glob("dart-*.ebuild"))
    if not existing_ebuilds:
        raise FileNotFoundError("No existing Dart ebuild found to copy from")

    def sort_key(ebuild_path):
        import re
        m = re.match(r'^dart-(.*)\.ebuild$', ebuild_path.name)
        if m:
            return parse_gentoo_version(m.group(1))
        return ([0], 0)

    # Sort by version/revision to get the latest
    existing_ebuild = sorted(existing_ebuilds, key=sort_key)[-1]

    new_ebuild_path = dart_dir / f"dart-{version}.ebuild"
    shutil.copy2(existing_ebuild, new_ebuild_path)
    logging.info(f"Copied {existing_ebuild.name} to {new_ebuild_path.name}")

    return existing_ebuild, new_ebuild_path

def commit_and_push(branch_name: str, old_version: str, version: str, ref: str, dry_run: bool, carried_files: list[str] | None = None, inherited_patches: list[str] | None = None):
    if dry_run:
        logging.info("Dry run: Skipping git add, commit, branch checkout, push, and PR creation.")
        return

    run_cmd(["git", "add", "dev-lang/dart", "virtual/dart"])
    # If no changes, exit
    res = run_cmd(["git", "status", "--porcelain"], capture_output=True)
    if not res.stdout.strip():
        logging.info("No changes to commit.")
        return

    commit_msg = f"dev-lang/dart: bump to {version} (source)\n\nAutomated source package update."
    run_cmd(["git", "commit", "-m", commit_msg])

    try:
        run_cmd(["git", "push", "origin", branch_name])
    except subprocess.CalledProcessError:
        logging.error("Failed to push branch.")
        raise

    carried_files = carried_files or []
    carried_summary = ", ".join(f"`{path}`" for path in carried_files) or "None"
    inherited_patches = inherited_patches or []
    patch_summary = ", ".join(f"`{path}`" for path in inherited_patches) or "None"

    body = (
        f"Automated source package update for Dart {version}.\n\n"
        f"**Old Packaged Version:** {old_version}\n"
        f"**New Upstream Version:** {version}\n"
        f"**Authoritative Source:** https://storage.googleapis.com/dart-archive/channels/stable/release/{version}/VERSION\n"
        f"**Authoritative Revision:** {ref}\n"
        f"**Host-Dart compatibility/prerequisite:** Independent Dart stable update; no Flutter prerequisite.\n"
        f"**Virtual-provider decision:** Exact-version virtual is generated using only packaged, matching source/binary providers; compatibility is never invented.\n"
        f"**Manifest Generation:** Completed successfully.\n"
        f"**Dependency review result:** Needs human review.\n"
        f"**Version-qualified non-patch FILESDIR carry-forward:** {carried_summary}\n"
        f"**Inherited immutable patches:** {patch_summary}\n"
        f"**Patch refresh/review result:** Exact patch revisions are inherited from the previous ebuild; source CI must confirm they still apply.\n"
        f"**Tests Performed:** Pending CI runs for g2 lint, pkgcheck, and integration verification.\n\n"
        f"Related to #939\n"
    )

    try:
        run_cmd([
            "gh", "pr", "create",
            "--title", f"dev-lang/dart: bump to {version} (source)",
            "--body", body,
            "--head", branch_name,
            "--base", "main"
        ])
        logging.info("PR created successfully.")
        close_superseding_releases("dart-source", version)
    except (subprocess.CalledProcessError, FileNotFoundError):
        logging.error("Failed to create PR using gh CLI.")
        raise

def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--dry-run", action="store_true", help="Do not push or create PR")
    parser.add_argument("--ref", type=str, help="Force a specific upstream git ref/hash")
    parser.add_argument("--version", type=str, help="Force a specific Dart version to update to")
    args = parser.parse_args()

    try:
        if args.version:
            release_obj = get_dart_release(args.version)
        else:
            release_obj = get_dart_release()

        version = release_obj["version"]
        auth_hash = release_obj.get("revision")

        if args.ref:
            if args.ref != auth_hash:
                logging.error(f"Supplied ref '{args.ref}' does not match authoritative stable Dart revision '{auth_hash}' for version {version}. Failing closed.")
                sys.exit(1)
            ref = args.ref
        else:
            ref = auth_hash

        orig_root = REPO_ROOT
        work_root = orig_root

        if args.dry_run:
            tmpdir = tempfile.mkdtemp(prefix="dart_update_dry_run_")
            atexit.register(lambda: shutil.rmtree(tmpdir, ignore_errors=True))
            work_root = Path(tmpdir)

            # Copy skeleton
            import os
            os.makedirs(work_root / "dev-lang" / "dart", exist_ok=True)
            os.makedirs(work_root / "virtual" / "dart", exist_ok=True)

            for file in (orig_root / "dev-lang" / "dart").glob("*.ebuild"):
                shutil.copy2(file, work_root / "dev-lang" / "dart")
            for file in (orig_root / "virtual" / "dart").glob("*.ebuild"):
                shutil.copy2(file, work_root / "virtual" / "dart")

        # Check if version is a prerelease
        if not re.match(r"^\d+\.\d+\.\d+$", version):
            logging.info(f"Dart version {version} is not a stable release (e.g. beta/dev). Skipping.")
            return 0

        logging.info(f"Target Dart version: {version}")

        if ebuild_exists(version, orig_root):
            logging.info(f"Dart {version} is already packaged. Exiting.")
            return 0

        branch_name = f"auto-update-dart-source-{version}"

        # In a real GH action, we might just be on detached HEAD, but locally we check branches
        branch_exists = check_branch_exists(branch_name)
        pr_exists = check_pr_exists(branch_name)
        if pr_exists:
            logging.info(f"Open PR for {branch_name} already exists. Exiting (dedup).")
            return 0
        if branch_exists and not pr_exists:
            logging.error(f"Remote branch {branch_name} exists but no open PR is found.")
            logging.error("This indicates a previous run failed between pushing the branch and creating the PR.")
            logging.error("Please manually recover the PR using `gh pr create` or delete the stranded branch.")
            sys.exit(1)

        if check_superseding_release("dart-source", version):
            return 0

        if not args.dry_run:
            run_cmd(["git", "checkout", "-b", branch_name])

        # Create main ebuild
        previous_ebuild, new_ebuild_path = create_dart_ebuild(version, work_root)
        previous_version = previous_ebuild.name.removeprefix("dart-").removesuffix(".ebuild")
        inherited_patches = inherit_previous_patch_files(
            previous_ebuild,
            new_ebuild_path,
            orig_root,
            work_root,
            "dev-lang/dart",
        )
        carried_files = carry_forward_versioned_files(
            previous_ebuild,
            version,
            orig_root,
            work_root,
            "dev-lang/dart",
        )
        if inherited_patches:
            logging.info("Inherited immutable patch files: %s", ", ".join(inherited_patches))
        if carried_files:
            logging.info("Carried version-qualified non-patch FILESDIR assets: %s", ", ".join(carried_files))

        # Run generator to update DEPS
        logging.info("Running generate_dart_ebuild.py to update DEPS")
        gen_cmd = [
            sys.executable, str(REPO_ROOT / "scripts" / "generate_dart_ebuild.py"),
            "--version", version,
            "--ebuild-revision", "0",
            "--ebuild", str(new_ebuild_path)
        ]
        # Run it but if it fails (e.g., new unreviewed deps), we fail closed
        try:
            run_cmd(gen_cmd)
        except subprocess.CalledProcessError as e:
            logging.error(f"Generator failed. Unreviewed DEPS changes? Error: {e}")
            raise

        # Check that it works via check mode
        try:
            run_cmd(gen_cmd + ["--check"])
        except subprocess.CalledProcessError as e:
            logging.error(f"Generator check mode failed. Error: {e}")
            raise

        try:
            logging.info("Updating manifests via verify_manifest.py")
            verify_manifest_cmd = [
                sys.executable,
                str(orig_root / "scripts" / "verify_manifest.py"),
                str(work_root / "dev-lang" / "dart"),
            ]
            run_cmd(verify_manifest_cmd, cwd=str(work_root))
        except subprocess.CalledProcessError:
            logging.error("verify_manifest.py failed. Failing.")
            sys.exit(1)

        # The source ebuild now exists: publish an exact-version virtual with
        # only providers present in this tree (source and/or binary).
        create_virtual_dart(version, work_root)

        commit_and_push(
            branch_name,
            previous_version,
            version,
            ref,
            args.dry_run,
            carried_files,
            inherited_patches,
        )

    except Exception as e:
        logging.error(f"Update failed: {e}")
        return 1

    return 0

if __name__ == "__main__":
    sys.exit(main())
