#!/usr/bin/env python3
"""
MarkiTect Release Management Tool

This script automates the release process for MarkiTect, including:
- Version management and validation
- Changelog generation
- Git tagging and repository management
- Package building and distribution
- Release artifact creation

Usage:
    python release.py [command] [options]

Commands:
    prepare     Prepare a new release (bump version, update changelog)
    build       Build release packages
    tag         Create git tag for release
    publish     Publish release (build + tag + distribute)
    status      Show current release status
    validate    Validate current state for release

Options:
    --version VERSION    Target version (e.g., 1.0.0, 1.0.1-rc1)
    --pre-release       Mark as pre-release
    --dry-run           Show what would be done without making changes
    --force             Force operation even with warnings
    --help              Show help message
"""

import os
import re
import sys
import json
import subprocess
import argparse
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple
import tempfile


class ReleaseManager:
    """Manages the MarkiTect release process."""

    def __init__(self, dry_run=False, force=False):
        self.dry_run = dry_run
        self.force = force
        self.project_root = Path(__file__).parent.absolute()
        self.pyproject_toml = self.project_root / "pyproject.toml"
        self.version_file = self.project_root / "markitect" / "__version__.py"
        self.changelog_file = self.project_root / "CHANGELOG.md"

    def run_command(self, cmd: List[str], capture=True, check=True, skip_dry_run=False) -> subprocess.CompletedProcess:
        """Run a command with optional dry-run support."""
        if self.dry_run and not skip_dry_run:
            print(f"[DRY RUN] Would run: {' '.join(cmd)}")
            return subprocess.CompletedProcess(cmd, 0, "", "")

        return subprocess.run(cmd, capture_output=capture, text=True, check=check)

    def get_current_version(self) -> str:
        """Get current version from pyproject.toml."""
        with open(self.pyproject_toml, 'r') as f:
            content = f.read()

        match = re.search(r'version\s*=\s*"([^"]+)"', content)
        if not match:
            raise ValueError("Could not find version in pyproject.toml")

        return match.group(1)

    def validate_version(self, version: str) -> bool:
        """Validate version format (semantic versioning)."""
        pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.?(\d+))?$'
        return bool(re.match(pattern, version))

    def compare_versions(self, v1: str, v2: str) -> int:
        """Compare two versions. Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
        def version_tuple(v):
            parts = v.split('-')[0].split('.')
            main = tuple(int(x) for x in parts)

            if '-' in v:
                pre = v.split('-')[1]
                if 'alpha' in pre:
                    pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
                    return main + (0, pre_num)
                elif 'beta' in pre:
                    pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
                    return main + (1, pre_num)
                elif 'rc' in pre:
                    pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
                    return main + (2, pre_num)

            return main + (3, 0)  # Release version

        t1, t2 = version_tuple(v1), version_tuple(v2)
        if t1 < t2:
            return -1
        elif t1 > t2:
            return 1
        else:
            return 0

    def update_version(self, new_version: str):
        """Update version in pyproject.toml and __version__.py."""
        print(f"📝 Updating version to {new_version}")

        # Update pyproject.toml
        with open(self.pyproject_toml, 'r') as f:
            content = f.read()

        new_content = re.sub(
            r'version\s*=\s*"[^"]+"',
            f'version = "{new_version}"',
            content
        )

        if not self.dry_run:
            with open(self.pyproject_toml, 'w') as f:
                f.write(new_content)

        # Update __version__.py
        with open(self.version_file, 'r') as f:
            version_content = f.read()

        new_version_content = re.sub(
            r'__version__\s*=\s*"[^"]+"',
            f'__version__ = "{new_version}"',
            version_content
        )

        if not self.dry_run:
            with open(self.version_file, 'w') as f:
                f.write(new_version_content)

    def get_git_status(self) -> Dict[str, any]:
        """Get current git repository status."""
        try:
            # Check if in git repo
            result = self.run_command(['git', 'rev-parse', '--git-dir'], skip_dry_run=True)

            # Get current branch
            branch_result = self.run_command(['git', 'branch', '--show-current'], skip_dry_run=True)
            current_branch = branch_result.stdout.strip()

            # Check for uncommitted changes
            status_result = self.run_command(['git', 'status', '--porcelain'], skip_dry_run=True)
            has_changes = bool(status_result.stdout.strip())

            # Get latest commit
            commit_result = self.run_command(['git', 'rev-parse', '--short', 'HEAD'], skip_dry_run=True)
            latest_commit = commit_result.stdout.strip()

            # Get latest tag
            try:
                tag_result = self.run_command(['git', 'describe', '--tags', '--abbrev=0'], skip_dry_run=True)
                latest_tag = tag_result.stdout.strip()
            except subprocess.CalledProcessError:
                latest_tag = None

            return {
                'is_repo': True,
                'branch': current_branch,
                'has_changes': has_changes,
                'latest_commit': latest_commit,
                'latest_tag': latest_tag
            }
        except subprocess.CalledProcessError:
            return {'is_repo': False}

    def generate_changelog_entry(self, version: str, since_tag: str = None) -> str:
        """Generate changelog entry from git commits."""
        print(f"📋 Generating changelog for {version}")

        # Get commits since last tag or all commits
        if since_tag:
            cmd = ['git', 'log', f'{since_tag}..HEAD', '--oneline', '--no-merges']
        else:
            cmd = ['git', 'log', '--oneline', '--no-merges']

        try:
            result = self.run_command(cmd)
            commits = result.stdout.strip().split('\n') if result.stdout.strip() else []
        except subprocess.CalledProcessError:
            commits = []

        # Categorize commits
        features = []
        fixes = []
        docs = []
        other = []

        for commit in commits:
            if not commit:
                continue

            commit_msg = commit.split(' ', 1)[1] if ' ' in commit else commit

            if commit_msg.startswith(('feat:', 'feature:')):
                features.append(commit_msg)
            elif commit_msg.startswith(('fix:', 'bugfix:')):
                fixes.append(commit_msg)
            elif commit_msg.startswith(('docs:', 'doc:')):
                docs.append(commit_msg)
            else:
                other.append(commit_msg)

        # Generate changelog entry
        date = datetime.now().strftime('%Y-%m-%d')
        entry = f"## [{version}] - {date}\n\n"

        if features:
            entry += "### Added\n"
            for feat in features:
                entry += f"- {feat}\n"
            entry += "\n"

        if fixes:
            entry += "### Fixed\n"
            for fix in fixes:
                entry += f"- {fix}\n"
            entry += "\n"

        if docs:
            entry += "### Documentation\n"
            for doc in docs:
                entry += f"- {doc}\n"
            entry += "\n"

        if other:
            entry += "### Other\n"
            for oth in other:
                entry += f"- {oth}\n"
            entry += "\n"

        return entry

    def update_changelog(self, version: str, since_tag: str = None):
        """Update CHANGELOG.md with new version entry."""
        entry = self.generate_changelog_entry(version, since_tag)

        # Read existing changelog or create new one
        if self.changelog_file.exists():
            with open(self.changelog_file, 'r') as f:
                existing_content = f.read()
        else:
            existing_content = "# Changelog\n\nAll notable changes to MarkiTect will be documented in this file.\n\n"

        # Insert new entry after header
        lines = existing_content.split('\n')
        header_end = 0
        for i, line in enumerate(lines):
            if line.startswith('## [') or (i > 0 and not line.startswith('#')):
                header_end = i
                break

        new_lines = lines[:header_end] + entry.split('\n') + lines[header_end:]
        new_content = '\n'.join(new_lines)

        if not self.dry_run:
            with open(self.changelog_file, 'w') as f:
                f.write(new_content)

    def validate_release_state(self) -> Tuple[bool, List[str]]:
        """Validate that the repository is ready for release."""
        issues = []

        git_status = self.get_git_status()

        if not git_status['is_repo']:
            issues.append("Not in a git repository")
        else:
            if git_status['has_changes'] and not self.force:
                issues.append("Repository has uncommitted changes")

            if git_status['branch'] != 'main' and not self.force:
                issues.append(f"Not on main branch (currently on {git_status['branch']})")

        # Check if tests pass (skip for dry run)
        if not self.dry_run:
            try:
                print("🧪 Running tests...")
                test_result = self.run_command(['make', 'test'], capture=False)
                if test_result.returncode != 0:
                    issues.append("Tests are failing")
            except subprocess.CalledProcessError:
                issues.append("Could not run tests (make test failed)")
            except FileNotFoundError:
                # Try pytest directly
                try:
                    test_result = self.run_command(['python', '-m', 'pytest'])
                    if test_result.returncode != 0:
                        issues.append("Tests are failing")
                except (subprocess.CalledProcessError, FileNotFoundError):
                    issues.append("Could not run tests")
        else:
            print("🧪 Skipping tests in dry run mode")

        return len(issues) == 0, issues

    def build_packages(self, version: str):
        """Build release packages."""
        print(f"📦 Building packages for version {version}")

        # Clean previous builds
        build_dirs = ['build', 'dist', '*.egg-info']
        for pattern in build_dirs:
            self.run_command(['rm', '-rf'] + [str(self.project_root / pattern)])

        # Build source distribution
        print("Building source distribution...")
        self.run_command(['python', '-m', 'build', '--sdist'], capture=False)

        # Build wheel
        print("Building wheel...")
        self.run_command(['python', '-m', 'build', '--wheel'], capture=False)

        print("✅ Packages built successfully")

    def create_git_tag(self, version: str, message: str = None):
        """Create and push git tag."""
        tag_name = f"v{version}"
        tag_message = message or f"Release {version}"

        print(f"🏷️  Creating git tag {tag_name}")

        # Create annotated tag
        self.run_command(['git', 'tag', '-a', tag_name, '-m', tag_message])

        # Push tag
        print(f"📤 Pushing tag to origin...")
        self.run_command(['git', 'push', 'origin', tag_name])

    def show_status(self):
        """Show current release status."""
        print("🔍 MarkiTect Release Status")
        print("=" * 50)

        current_version = self.get_current_version()
        print(f"Current Version: {current_version}")

        git_status = self.get_git_status()
        if git_status['is_repo']:
            print(f"Git Branch: {git_status['branch']}")
            print(f"Latest Commit: {git_status['latest_commit']}")
            print(f"Latest Tag: {git_status['latest_tag'] or 'None'}")
            print(f"Uncommitted Changes: {'Yes' if git_status['has_changes'] else 'No'}")
        else:
            print("Git Repository: Not available")

        # Check build tools
        print("\nBuild Tools:")
        try:
            self.run_command(['python', '-m', 'build', '--help'])
            print("✅ build module available")
        except (subprocess.CalledProcessError, FileNotFoundError):
            print("❌ build module not available (pip install build)")

        # Check if packages exist
        dist_dir = self.project_root / "dist"
        if dist_dir.exists():
            packages = list(dist_dir.glob("*"))
            print(f"\nExisting Packages: {len(packages)} files in dist/")
            for pkg in packages:
                print(f"  - {pkg.name}")
        else:
            print("\nExisting Packages: None")

    def prepare_release(self, version: str, pre_release: bool = False):
        """Prepare a new release."""
        print(f"🚀 Preparing release {version}")

        # Validate version format
        if not self.validate_version(version):
            raise ValueError(f"Invalid version format: {version}")

        # Check if version is newer than current
        current_version = self.get_current_version()
        if self.compare_versions(version, current_version) <= 0 and not self.force:
            raise ValueError(f"New version {version} must be greater than current {current_version}")

        # Validate release state
        is_valid, issues = self.validate_release_state()
        if not is_valid:
            print("❌ Release validation failed:")
            for issue in issues:
                print(f"  - {issue}")
            if not self.force:
                sys.exit(1)
            else:
                print("⚠️  Continuing with --force flag")

        # Update version
        self.update_version(version)

        # Update changelog
        git_status = self.get_git_status()
        since_tag = git_status.get('latest_tag') if git_status['is_repo'] else None
        self.update_changelog(version, since_tag)

        print(f"✅ Release {version} prepared successfully")
        print("Next steps:")
        print("1. Review and edit CHANGELOG.md if needed")
        print("2. Commit changes: git add -A && git commit -m 'Prepare release {version}'")
        print("3. Run: python release.py publish --version {version}")

    def publish_release(self, version: str):
        """Publish a complete release."""
        print(f"📢 Publishing release {version}")

        # Validate state
        is_valid, issues = self.validate_release_state()
        if not is_valid and not self.force:
            print("❌ Cannot publish release due to validation issues:")
            for issue in issues:
                print(f"  - {issue}")
            sys.exit(1)

        # Build packages
        self.build_packages(version)

        # Create git tag
        self.create_git_tag(version)

        print(f"✅ Release {version} published successfully!")
        print(f"📦 Packages available in dist/")
        print(f"🏷️  Git tag v{version} created and pushed")


def main():
    parser = argparse.ArgumentParser(
        description="MarkiTect Release Management Tool",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__.split('\n\n')[1]
    )

    parser.add_argument('command', choices=['prepare', 'build', 'tag', 'publish', 'status', 'validate'],
                       help='Release command to execute')
    parser.add_argument('--version', type=str, help='Target version (e.g., 1.0.0)')
    parser.add_argument('--pre-release', action='store_true', help='Mark as pre-release')
    parser.add_argument('--dry-run', action='store_true', help='Show what would be done')
    parser.add_argument('--force', action='store_true', help='Force operation despite warnings')

    args = parser.parse_args()

    manager = ReleaseManager(dry_run=args.dry_run, force=args.force)

    try:
        if args.command == 'status':
            manager.show_status()

        elif args.command == 'validate':
            is_valid, issues = manager.validate_release_state()
            if is_valid:
                print("✅ Repository is ready for release")
            else:
                print("❌ Release validation failed:")
                for issue in issues:
                    print(f"  - {issue}")
                sys.exit(1)

        elif args.command == 'prepare':
            if not args.version:
                print("❌ --version is required for prepare command")
                sys.exit(1)
            manager.prepare_release(args.version, args.pre_release)

        elif args.command == 'build':
            version = args.version or manager.get_current_version()
            manager.build_packages(version)

        elif args.command == 'tag':
            if not args.version:
                print("❌ --version is required for tag command")
                sys.exit(1)
            manager.create_git_tag(args.version)

        elif args.command == 'publish':
            if not args.version:
                print("❌ --version is required for publish command")
                sys.exit(1)
            manager.publish_release(args.version)

    except Exception as e:
        print(f"❌ Error: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()