#!/usr/bin/python3

import argparse
import json
import os
import pathlib
import subprocess
import sys
from dataclasses import dataclass

# Path to the directory of this file
TOOL_PATH = pathlib.Path(__file__).parent.resolve()

# Path to cliff.toml configuration
CLIFF_CONFIG = os.path.join(TOOL_PATH, "cliff.toml")


@dataclass
class RepoInfo:
    """Information about a repository to process."""

    version: str
    path: str
    source: str

    @property
    def repo_name(self) -> str:
        """Get repository name from path basename."""
        return os.path.basename(self.path)


def parse_version_range(version_range):
    """
    Parse version range string like '5.0.3..6.0.0'.

    Args:
        version_range: Version range string

    Returns:
        tuple: (from_version, to_version)

    Raises:
        SystemExit: If format is invalid
    """
    parts = version_range.split("..")
    if len(parts) != 2:
        print(f"ERROR: Invalid version range format: {version_range}", file=sys.stderr)
        print(f"Expected format: FROM..TO (e.g., '5.0.3..6.0.0')", file=sys.stderr)
        sys.exit(1)

    return parts[0], parts[1]


def load_release_json(release_version):
    """
    Load and parse release JSON file.

    Args:
        release_version: Version string (e.g., "5.0.3")

    Returns:
        dict: Parsed JSON data

    Raises:
        SystemExit: If JSON doesn't exist or is malformed
    """
    json_path = os.path.join(
        TOOL_PATH, "..", "subcomponents", "releases", f"{release_version}.json"
    )

    if not os.path.exists(json_path):
        print(f"ERROR: Release file not found: {json_path}", file=sys.stderr)
        print(f"Available releases:", file=sys.stderr)
        releases_dir = os.path.join(TOOL_PATH, "..", "subcomponents", "releases")
        if os.path.exists(releases_dir):
            for file in sorted(os.listdir(releases_dir)):
                if file.endswith(".json"):
                    print(f"  - {file[:-5]}", file=sys.stderr)
        sys.exit(1)

    try:
        with open(json_path, "r") as f:
            return json.load(f)
    except json.JSONDecodeError as e:
        print(f"ERROR: Malformed JSON in {json_path}: {e}", file=sys.stderr)
        sys.exit(1)


def build_repos_map(json_data, repos_dir):
    """
    Build repository list from JSON components.

    Deduplicates by repo name - multiple components can come from same repo.
    We only care about repositories, not individual components.

    Args:
        json_data: Parsed JSON with "components" list
        repos_dir: Base directory containing repositories

    Returns:
        list[RepoInfo]: List of RepoInfo objects
    """
    repos_list = []
    seen = set()

    for component in json_data["components"]:
        # Extract repo name from source URL (e.g., "github.com/.../mender" -> "mender")
        source = component["source"].rstrip("/")
        repo_name = source.split("/")[-1]
        if repo_name not in seen:
            seen.add(repo_name)
            repos_list.append(
                RepoInfo(
                    version=component["version"],
                    path=os.path.join(repos_dir, repo_name),
                    source=source,
                )
            )

    return repos_list


def validate_repository_exists(repo_info):
    """
    Validate that a repository exists at expected path.

    Args:
        repo_info: RepoInfo object

    Raises:
        SystemExit: If repository doesn't exist
    """
    if not os.path.exists(repo_info.path):
        print(f"ERROR: Repository not found: {repo_info.path}", file=sys.stderr)
        print(f"  Repository: {repo_info.repo_name}", file=sys.stderr)
        sys.exit(1)


def get_component_version(json_data, repo_name):
    """
    Get version for a component from release JSON.

    Args:
        json_data: Parsed JSON with "components" list
        repo_name: Repository name to find

    Returns:
        str: Version string or None if not found
    """
    for component in json_data["components"]:
        component_repo = component["source"].rstrip("/").split("/")[-1]
        if component_repo == repo_name:
            return component["version"]
    return None


def get_tag_date(repo_path, tag):
    """
    Get date from git tag.

    Args:
        repo_path: Path to git repository
        tag: Tag name

    Returns:
        str: Date in YYYY-MM-DD format or "unknown"
    """
    try:
        # Try to get the tagger date first (for annotated tags)
        output = subprocess.check_output(
            ["git", "log", "-1", "--format=%cs", tag],
            cwd=repo_path,
            stderr=subprocess.DEVNULL,
        )
        date = output.decode().strip()
        # If we got multiple lines, take the first one
        if "\n" in date:
            date = date.split("\n")[0]
        return date
    except subprocess.CalledProcessError:
        return "unknown"


def generate_changelog(repo_path, from_version, to_version):
    """
    Generate changelog using git-cliff.

    Args:
        repo_path: Path to git repository
        from_version: Starting version tag
        to_version: Ending version tag

    Returns:
        str: Generated changelog content

    Raises:
        SystemExit: If git-cliff fails
    """
    if not os.path.exists(CLIFF_CONFIG):
        print(f"ERROR: cliff.toml not found at {CLIFF_CONFIG}", file=sys.stderr)
        sys.exit(1)

    cmd = [
        "git-cliff",
        "--config",
        CLIFF_CONFIG,
        f"{from_version}..{to_version}",
    ]

    # Show the exact command being executed
    print(f"  Command: {' '.join(cmd)}", file=sys.stderr)

    try:
        result = subprocess.run(cmd, cwd=repo_path, capture_output=True, check=True)
        output = result.stdout.decode().strip()

        # Debug: show stderr if present
        if result.stderr:
            stderr_text = result.stderr.decode().strip()
            if stderr_text:
                print(f"  git-cliff stderr: {stderr_text}", file=sys.stderr)

        return output
    except subprocess.CalledProcessError as e:
        print(
            f"ERROR: git-cliff failed for {os.path.basename(repo_path)}",
            file=sys.stderr,
        )
        print(f"  Range: {from_version}..{to_version}", file=sys.stderr)
        print(f"  Exit code: {e.returncode}", file=sys.stderr)
        if e.stderr:
            print(f"  Error: {e.stderr.decode()}", file=sys.stderr)
        sys.exit(1)


def clean_changelog_output(changelog, version, date):
    """
    Clean git-cliff output by removing header/footer markers and adjusting heading levels.

    Converts:
    - ## version headers to ### (intermediate versions)
    - ### category headers to #### (Bug fixes, Features, etc.)
    - Replaces ## [unreleased] with actual version info

    Args:
        changelog: Raw git-cliff output
        version: Component version (e.g., "1.5.2")
        date: Release date (e.g., "2025-10-11")

    Returns:
        str: Cleaned changelog content
    """
    lines = changelog.split("\n")
    cleaned = []

    for line in lines:
        # Skip --- markers (header/footer)
        if line.strip() == "---":
            continue
        # Replace [unreleased] headers with actual version
        if line.strip() == "## [unreleased]":
            cleaned.append(f"### {version} - {date}")
            continue
        # Convert version headers for intermediate versions
        if line.startswith("## ") or line.startswith("### "):
            cleaned.append("#" + line)
            continue
        cleaned.append(line)

    return "\n".join(cleaned).strip()


def find_latest_release():
    """
    Find the latest release version based on semantic versioning.

    Returns:
        str: Latest release version

    Raises:
        SystemExit: If no releases are found
    """
    releases_dir = os.path.join(TOOL_PATH, "..", "subcomponents", "releases")

    if not os.path.exists(releases_dir):
        print(f"ERROR: Releases directory not found: {releases_dir}\n", file=sys.stderr)
        sys.exit(1)

    # Find all release JSON files
    releases = []
    for filename in os.listdir(releases_dir):
        if filename.endswith(".json") and filename != "next.json":
            version = filename[:-5]  # Remove .json
            try:
                version_parts = tuple(map(int, version.split(".")))
                releases.append((version_parts, version))
            except ValueError:
                # Skip files that don't follow semantic versioning
                continue

    if not releases:
        print(f"ERROR: No release versions found in {releases_dir}\n", file=sys.stderr)
        sys.exit(1)

    # Find the highest version
    releases.sort(reverse=True)
    return releases[0][1]


# Parse arguments
parser = argparse.ArgumentParser(
    description="Generate aggregated changelog for Mender Client subcomponents."
)
version_group = parser.add_mutually_exclusive_group(required=True)
version_group.add_argument(
    "--version-range", help="Version range to process (e.g., '5.0.3..6.0.0')",
)
version_group.add_argument(
    "--version-next",
    action="store_true",
    help="Generate changelog from latest release to next (uses next.json)",
)
parser.add_argument(
    "--repos-dir", required=True, help="Directory containing checked-out repositories",
)
parser.add_argument(
    "--output", help="Output file path (default: stdout)",
)
args = parser.parse_args()

# Determine version range
if args.version_range:
    from_version, to_version = parse_version_range(args.version_range)
else:  # args.version_next
    to_version = "next"
    from_version = find_latest_release()
    print(f"Determined version range: {from_version}..{to_version}\n", file=sys.stderr)

# Load release JSONs
print(f"Loading release data for {from_version} and {to_version}...", file=sys.stderr)
from_json = load_release_json(from_version)
to_json = load_release_json(to_version)

# Build repository list from target release
repos_list = build_repos_map(to_json, args.repos_dir)

# Generate changelog for each repository
output_parts = [f"# Mender Client {to_version}"]

# Add repository table
output_parts.append("\n| Repository | Version |")
output_parts.append("| --- | --- |")
for repo_info in repos_list:
    repo_link = f"[{repo_info.repo_name}](https://{repo_info.source})"
    output_parts.append(f"| {repo_link} | {repo_info.version} |")

for repo_info in repos_list:
    print(f"Processing {repo_info.repo_name}...", file=sys.stderr)

    # Validate repository exists
    validate_repository_exists(repo_info)

    # Get version from source release
    from_repo_version = get_component_version(from_json, repo_info.repo_name)

    # Get date from target version tag
    tag_date = get_tag_date(repo_info.path, repo_info.version)

    # Generate section header
    output_parts.append(f"\n## {repo_info.repo_name} {repo_info.version} ({tag_date})")

    # Handle special cases
    if repo_info.version == "1.0.0":
        output_parts.append(f"\nFirst release of {repo_info.repo_name}")
    elif from_repo_version == repo_info.version:
        output_parts.append("\nNo changes")
    else:
        if from_repo_version is None:
            print(
                f"WARNING: {repo_info.repo_name} not found in {from_version}, "
                f"treating as new component",
                file=sys.stderr,
            )
            output_parts.append(f"\nFirst release of {repo_info.repo_name}")
        else:
            # Generate changelog
            print(
                f"  Generating changelog: {from_repo_version}..{repo_info.version}",
                file=sys.stderr,
            )
            changelog = generate_changelog(
                repo_info.path, from_repo_version, repo_info.version
            )
            cleaned_changelog = clean_changelog_output(
                changelog, repo_info.version, tag_date
            )

            if cleaned_changelog:
                output_parts.append("\n" + cleaned_changelog)
            else:
                output_parts.append("\nNo changelog entries found.")

# Write output
output_text = "\n".join(output_parts) + "\n"

if args.output:
    with open(args.output, "w") as f:
        f.write(output_text)
    print(f"Changelog written to: {args.output}", file=sys.stderr)
else:
    print(output_text)
