#!/usr/bin/env python3
# This file is in the public domain.

import sys
import os
import re
import argparse
import subprocess
from email.utils import formatdate

# The list of files this script is responsible for updating
FILES_TO_UPDATE = [
    "configure.ac",
    "debian/changelog",
    "doc/doxygen/taler.doxy"
]

def run_cmd(cmd, capture=True):
    """Runs a shell command. Can capture output or print it directly to the terminal."""
    if capture:
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        return result.stdout.strip()
    else:
        subprocess.run(cmd, check=True)

def check_git_clean():
    """Checks if the git working directory is clean."""
    try:
        # --porcelain outputs nothing if the working tree is completely clean
        status = run_cmd(["git", "status", "--porcelain"])
        if status:
            print("Error: Git working directory is not clean.", file=sys.stderr)
            print("Please commit or stash your changes before bumping the version.", file=sys.stderr)
            print(f"\nUncommitted changes:\n{status}\n", file=sys.stderr)
            sys.exit(1)
    except subprocess.CalledProcessError:
        print("Error: Could not determine git status. Are you in a git repository?", file=sys.stderr)
        sys.exit(1)

def confirm(prompt_text):
    """Asks the user for a yes/no confirmation."""
    while True:
        choice = input(f"{prompt_text} [y/N]: ").strip().lower()
        if choice in ['y', 'yes']:
            return True
        elif choice in ['n', 'no', '']:
            return False
        print("Please answer 'y' or 'n'.")

def get_current_version():
    """Extracts the current version from configure.ac."""
    try:
        with open("configure.ac", 'r') as f:
            for line in f:
                if 'AC_INIT' in line:
                    match = re.search(r'AC_INIT\(.*?,\[(.*?)\],', line)
                    if match:
                        return match.group(1)
    except FileNotFoundError:
        print("Error: configure.ac not found. Run this from the project root.", file=sys.stderr)
        sys.exit(1)
        
    print("Error: Could not parse current version from configure.ac", file=sys.stderr)
    sys.exit(1)

def bump_version(current_version, bump_type):
    """Increments the specified part of a semantic version string."""
    parts = current_version.split('.')
    
    while len(parts) < 3:
        parts.append('0')

    try:
        major, minor, patch = map(int, parts[:3])
    except ValueError:
        print(f"Error: Current version '{current_version}' is not strictly numeric (X.Y.Z). "
              "Cannot auto-bump.", file=sys.stderr)
        sys.exit(1)

    if bump_type == 'major':
        return f"{major + 1}.0.0"
    elif bump_type == 'minor':
        return f"{major}.{minor + 1}.0"
    elif bump_type == 'patch':
        return f"{major}.{minor}.{patch + 1}"

def is_updated(filepath, version):
    """Checks if the version string already exists in the file."""
    try:
        with open(filepath, 'r') as f:
            if version in f.read():
                print(f"{filepath} already in {version}")
                return True
    except FileNotFoundError:
        print(f"Warning: {filepath} not found, skipping...", file=sys.stderr)
        return True
    return False

def configure_ac(version, dry_run=False):
    filepath = FILES_TO_UPDATE[0]
    if is_updated(filepath, version):
        return

    with open(filepath, 'r') as f:
        content = f.read()

    new_lines = []
    for line in content.splitlines():
        if 'AC_INIT' in line:
            line = re.sub(r',\[.*?\],', f',[{version}],', line, count=1)
        new_lines.append(line)

    if dry_run:
        print(f"[DRY RUN] Would update AC_INIT version in {filepath} to {version}")
    else:
        with open(filepath, 'w') as f:
            f.write('\n'.join(new_lines) + '\n')
        print(f"{filepath} {version}")

def debian_changelog(version, date_str, git_user, git_email, dry_run=False):
    filepath = FILES_TO_UPDATE[1]
    if is_updated(filepath, version):
        return

    changelog_entry = (
        f"taler-merchant ({version}) unstable; urgency=low\n\n"
        f"  * Release {version}.\n\n"
        f" -- {git_user} <{git_email}>  {date_str}\n\n"
    )

    with open(filepath, 'r') as f:
        existing_content = f.read()

    if dry_run:
        print(f"[DRY RUN] Would prepend new release block to {filepath} for {version}")
    else:
        with open(filepath, 'w') as f:
            f.write(changelog_entry + existing_content)
        print(f"debian/changelog {version}")

def doc_doxygen_taler_doxy(version, dry_run=False):
    filepath = FILES_TO_UPDATE[2]
    if is_updated(filepath, version):
        return

    with open(filepath, 'r') as f:
        lines = f.readlines()

    for i, line in enumerate(lines):
        if 'PROJECT_NUMBER' in line:
            lines[i] = re.sub(r'= .*', f'= {version}', line)

    if dry_run:
        print(f"[DRY RUN] Would update PROJECT_NUMBER in {filepath} to {version}")
    else:
        with open(filepath, 'w') as f:
            f.writelines(lines)
        print(f"doc/doxygen/taler.doxy {version}")

def git_release_workflow(version, dry_run=False):
    """Handles committing, pushing, tagging, and pushing the tag safely."""
    print("\n--- Git Release Workflow ---")
    
    if dry_run:
        print(f"[DRY RUN] Would commit, push, tag, and push tag for version {version}")
        return

    # 1. Commit locally
    if confirm(f"Stage changed files and commit as 'Bump version to v{version}'?"):
        existing_files = [f for f in FILES_TO_UPDATE if os.path.exists(f)]
        run_cmd(["git", "add"] + existing_files, capture=False)
        run_cmd(["git", "commit", "-m", f"Bump version to v{version}"], capture=False)
        print("Commit successful.")
    else:
        print("Skipping commit. (Aborting remaining git operations)")
        return

    # 2. Push the commit BEFORE tagging
    push_successful = False
    if confirm("Push commit to remote?"):
        print("Pushing commit...")
        try:
            run_cmd(["git", "push"], capture=False)
            print("Push successful.")
            push_successful = True
        except subprocess.CalledProcessError:
            print("\nError: Push failed! You likely need to pull or rebase.")
            print("Aborting the tagging process to prevent an orphaned local tag.")
            return
    else:
        print("Skipping push.")

    # 3. Tag (Only if push succeeded or user explicitly skipped push but still wants to tag)
    tag_name = f"v{version}"
    if confirm(f"Create git tag '{tag_name}'?"):
        run_cmd(["git", "tag", "-a", tag_name, "-m", f"Release {version}"], capture=False)
        print(f"Tag {tag_name} created locally.")

        # 4. Push the Tag
        if push_successful or confirm("The main commit wasn't pushed. Push the tag anyway?"):
            if confirm(f"Push tag {tag_name} to remote?"):
                print(f"Pushing tag {tag_name}...")
                run_cmd(["git", "push", "origin", tag_name], capture=False)
                print("Tag pushed successfully.")
            else:
                print("Skipping tag push.")
    else:
        print("Skipping tag.")

def main():
    parser = argparse.ArgumentParser(description="Update and bump project version strings.")
    
    parser.add_argument('--dry-run', action='store_true', help="Simulate the changes without modifying files")
    parser.add_argument('--commit', action='store_true', help="Interactively commit, push, and tag the version bump")
    
    group = parser.add_mutually_exclusive_group()
    group.add_argument('--major', action='store_true', help="Increment the major version (X.0.0)")
    group.add_argument('--minor', action='store_true', help="Increment the minor version (x.Y.0)")
    group.add_argument('--patch', action='store_true', help="Increment the patch version (x.y.Z)")
    
    parser.add_argument('version', nargs='?', help="Explicitly specify the new version string (e.g., 1.2.3)")

    args = parser.parse_args()

    # Check for a clean git directory before making ANY changes
    # We bypass this strictly if doing a dry-run so you can test bumping safely
    if not args.dry_run:
        check_git_clean()

    bump_requested = args.major or args.minor or args.patch

    if bump_requested and args.version:
        parser.error("You cannot specify both a specific version string and a bump flag.")
    elif not bump_requested and not args.version:
        parser.error("You must specify either a specific version string or a bump flag (--major/--minor/--patch).")

    if bump_requested:
        current_version = get_current_version()
        bump_type = 'major' if args.major else 'minor' if args.minor else 'patch'
        version = bump_version(current_version, bump_type)
        if args.dry_run:
            print(f"[DRY RUN] Detected current version: {current_version}")
        print(f"Bumping {bump_type} version: {current_version} -> {version}")
    else:
        version = args.version

    date_str = formatdate(localtime=True) 
    try:
        git_user = run_cmd(["git", "config", "user.name"])
        git_email = run_cmd(["git", "config", "user.email"])
    except subprocess.CalledProcessError:
        print("Error: Could not retrieve git user details. Are you in a git repository?", file=sys.stderr)
        sys.exit(1)

    # Execute file updates
    configure_ac(version, dry_run=args.dry_run)
    debian_changelog(version, date_str, git_user, git_email, dry_run=args.dry_run)
    doc_doxygen_taler_doxy(version, dry_run=args.dry_run)

    # Execute git workflow if requested
    if args.commit:
        git_release_workflow(version, dry_run=args.dry_run)

if __name__ == '__main__':
    main()
