#!/usr/bin/env python3
"""
Commit and push the `private/` and `docs/` git repos under the base directory.

With --archive-ignored, also tarballs each repo's irreplaceable gitignored content
(e.g. .env files, training videos, data exports) to --archive-dir before pushing,
so the folder can be safely deleted afterwards. Regenerable junk like node_modules
and .venv is excluded.
"""

import subprocess
from pathlib import Path
from datetime import datetime


# Gitignored paths that are regenerable and don't need to be archived.
# Matched as substrings against the ignored file's path relative to the repo root.
REGENERABLE_PATTERNS = (
    "node_modules/",
    ".venv/",
    ".next/",
    ".docusaurus/",
    "/build/",
    "__pycache__/",
    ".pytest_cache/",
    "src/generated/",
)

REGENERABLE_SUFFIXES = (
    ".pyc",
    ".tsbuildinfo",
)

REGENERABLE_FILENAMES = frozenset({
    ".DS_Store",
    "next-env.d.ts",
})


def run_command(cmd, cwd=None):
    """Run a shell command and return output."""
    result = subprocess.run(
        cmd,
        cwd=cwd,
        capture_output=True,
        text=True,
        shell=isinstance(cmd, str)
    )
    return result


def is_git_repo(path):
    """Check if a directory is a git repository."""
    return (path / ".git").exists()


def has_changes(repo_path):
    """Check if repo has uncommitted changes or unpushed commits."""
    # Check for uncommitted changes
    result = run_command(["git", "status", "--porcelain"], cwd=repo_path)
    if result.stdout.strip():
        return True, "uncommitted changes"

    # Check for unpushed commits
    result = run_command(["git", "status", "-sb"], cwd=repo_path)
    if "ahead" in result.stdout:
        return True, "unpushed commits"

    return False, None


TARGET_REPOS = ("private", "docs")


def find_git_repos(base_path):
    """Return the target repos (private, docs) that exist as git repos under base_path."""
    base_path = Path(base_path)
    repos = []
    for name in TARGET_REPOS:
        repo_path = base_path / name
        if is_git_repo(repo_path):
            repos.append(repo_path)
        else:
            print(f"⚠️  Skipping {name}: not a git repo at {repo_path}")
    return repos


def list_irreplaceable_ignored(repo_path):
    """Return ignored files in the repo that are not regenerable junk."""
    result = run_command(
        ["git", "ls-files", "--others", "--ignored", "--exclude-standard"],
        cwd=repo_path,
    )
    if result.returncode != 0:
        return []

    files = [line for line in result.stdout.split("\n") if line.strip()]
    keepers = []
    for f in files:
        norm = "/" + f  # so /build/ matches a top-level "build/"
        if any(pat in norm for pat in REGENERABLE_PATTERNS):
            continue
        if f.endswith(REGENERABLE_SUFFIXES):
            continue
        if Path(f).name in REGENERABLE_FILENAMES:
            continue
        keepers.append(f)
    return keepers


def archive_ignored(repo_path, archive_dir, dry_run=False):
    """Tarball irreplaceable ignored files. Returns (file_count, archive_path or None)."""
    files = list_irreplaceable_ignored(repo_path)
    if not files:
        return 0, None

    archive_path = archive_dir / f"{repo_path.name}.tar.gz"

    if dry_run:
        return len(files), archive_path

    archive_dir.mkdir(parents=True, exist_ok=True)
    result = subprocess.run(
        ["tar", "-czf", str(archive_path), "-C", str(repo_path), "-T", "-"],
        input="\n".join(files),
        text=True,
        capture_output=True,
    )
    if result.returncode != 0:
        raise Exception(f"tar failed: {result.stderr}")

    return len(files), archive_path


def commit_and_push(repo_path, commit_message=None):
    """Commit all changes and push to remote."""
    if commit_message is None:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        commit_message = f"Auto-backup: {timestamp}"

    # Stage all changes
    result = run_command(["git", "add", "-A"], cwd=repo_path)
    if result.returncode != 0:
        raise Exception(f"git add failed: {result.stderr}")

    # Commit
    result = run_command(
        ["git", "commit", "-m", commit_message],
        cwd=repo_path
    )
    if result.returncode != 0 and "nothing to commit" not in result.stdout:
        raise Exception(f"git commit failed: {result.stderr}")

    # Push
    result = run_command(["git", "push"], cwd=repo_path)
    if result.returncode != 0:
        raise Exception(f"git push failed: {result.stderr}")


def backup_local_repos(
    base_dir,
    commit_message=None,
    dry_run=False,
    archive_ignored_flag=False,
    archive_dir=None,
):
    """Find and back up every local git repo under base_dir."""
    base_path = Path(base_dir)

    if not base_path.exists():
        print(f"❌ Error: Directory not found: {base_dir}")
        return

    print(f"🔍 Scanning for git repositories in: {base_path}")
    if archive_ignored_flag:
        print(f"📦 Archiving irreplaceable ignored files to: {archive_dir}")
    print(f"Started at {datetime.now()}")
    print("-" * 60)

    repos = find_git_repos(base_path)
    print(f"Found {len(repos)} git repositories\n")

    backed_up = 0
    skipped_no_changes = 0
    archived = 0
    errors = []

    for repo_path in repos:
        relative_path = repo_path.relative_to(base_path)
        print(f"\n📁 {relative_path}")

        # Archive ignored files first (independent of whether there are tracked changes)
        if archive_ignored_flag:
            try:
                count, archive_path = archive_ignored(repo_path, archive_dir, dry_run=dry_run)
                if count == 0:
                    print(f"   📦 No irreplaceable ignored files")
                elif dry_run:
                    print(f"   📦 DRY RUN - would archive {count} files to {archive_path.name}")
                    archived += 1
                else:
                    size_mb = archive_path.stat().st_size / 1024 / 1024
                    print(f"   📦 Archived {count} files → {archive_path.name} ({size_mb:.1f} MB)")
                    archived += 1
            except Exception as e:
                error_msg = f"{relative_path} (archive): {e}"
                print(f"   ❌ Archive failed: {e}")
                errors.append(error_msg)

        # Check for tracked changes
        has_change, change_type = has_changes(repo_path)
        if not has_change:
            print(f"   ✓  No changes to commit")
            skipped_no_changes += 1
            continue

        print(f"   📝 Has {change_type}")

        if dry_run:
            print(f"   🔍 DRY RUN - would commit and push")
            backed_up += 1
            continue

        try:
            commit_and_push(repo_path, commit_message)
            print(f"   ✅ Successfully committed and pushed")
            backed_up += 1
        except Exception as e:
            error_msg = f"{relative_path}: {str(e)}"
            print(f"   ❌ Failed: {e}")
            errors.append(error_msg)

    # Summary
    print("\n" + "=" * 60)
    print(f"📊 Summary:")
    print(f"   ✅ Backed up: {backed_up} repos")
    if archive_ignored_flag:
        print(f"   📦 Archived: {archived} repos")
    print(f"   ✓  Skipped (no changes): {skipped_no_changes} repos")
    print(f"   ❌ Errors: {len(errors)} repos")

    if errors:
        print(f"\n❌ Errors encountered:")
        for error in errors:
            print(f"   - {error}")

    print(f"\nCompleted at {datetime.now()}")


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="Commit and push changes for the private/ and docs/ repos under --base-dir"
    )
    parser.add_argument(
        "--base-dir",
        help="Base directory to search for git repos",
        default=str(Path.home() / "github/ariels-org")
    )
    parser.add_argument(
        "--message", "-m",
        help="Commit message (default: timestamped auto-backup message)",
        default=None
    )
    parser.add_argument(
        "--dry-run",
        help="Show what would be done without making changes",
        action="store_true"
    )
    parser.add_argument(
        "--archive-ignored",
        help="Also tarball each repo's irreplaceable gitignored files (e.g. .env, videos) "
             "to --archive-dir. Use this before deleting folders.",
        action="store_true",
    )
    parser.add_argument(
        "--archive-dir",
        help="Where to write tarballs of ignored files (default: ~/backups/ariels-org-YYYY-MM-DD)",
        default=None,
    )

    args = parser.parse_args()

    archive_dir = None
    if args.archive_ignored:
        if args.archive_dir:
            archive_dir = Path(args.archive_dir)
        else:
            date_str = datetime.now().strftime("%Y-%m-%d")
            archive_dir = Path.home() / "backups" / f"ariels-org-{date_str}"

    backup_local_repos(
        args.base_dir,
        args.message,
        args.dry_run,
        archive_ignored_flag=args.archive_ignored,
        archive_dir=archive_dir,
    )
