#!/usr/bin/env python3
"""
Apply branch protection rules to all Gitea repositories.

This script uses the Gitea API to apply consistent branch protection rules
across all repositories owned by a user or organization.
"""

import os
import sys
import requests
from typing import Optional
from dataclasses import dataclass


@dataclass
class BranchProtectionConfig:
    """Configuration for branch protection rules."""

    # Branch to protect
    branch_name: str = "main"

    # Push settings
    enable_push: bool = False  # Disable direct pushes (force PR workflow)
    enable_push_whitelist: bool = False
    push_whitelist_usernames: list[str] = None

    # Pull request approval settings
    required_approvals: int = 1
    enable_approvals_whitelist: bool = True
    approvals_whitelist_usernames: list[str] = None
    dismiss_stale_approvals: bool = True

    # Status check settings
    enable_status_check: bool = True
    status_check_contexts: list[str] = None  # Empty for now, add when CI/CD is set up

    # Merge settings
    enable_merge_whitelist: bool = True
    merge_whitelist_usernames: list[str] = None
    block_on_rejected_reviews: bool = True
    block_on_outdated_branch: bool = True  # Critical for database migrations

    # Other settings
    require_signed_commits: bool = False
    protected_file_patterns: str = ""
    unprotected_file_patterns: str = ""

    def __post_init__(self):
        """Initialize mutable default values."""
        if self.push_whitelist_usernames is None:
            self.push_whitelist_usernames = []
        if self.approvals_whitelist_usernames is None:
            self.approvals_whitelist_usernames = []
        if self.merge_whitelist_usernames is None:
            self.merge_whitelist_usernames = []
        if self.status_check_contexts is None:
            self.status_check_contexts = []


class GiteaBranchProtectionManager:
    """Manages branch protection rules across Gitea repositories."""

    def __init__(self, gitea_url: str, api_token: str, owner: str):
        """
        Initialize the manager.

        Args:
            gitea_url: Base URL of your Gitea instance (e.g., https://git.example.com)
            api_token: Gitea API token with repo access
            owner: Username or organization name
        """
        self.gitea_url = gitea_url.rstrip('/')
        self.api_token = api_token
        self.owner = owner
        self.headers = {
            'Authorization': f'token {api_token}',
            'Content-Type': 'application/json'
        }

    def get_repositories(self) -> list[dict]:
        """Get all repositories for the authenticated user."""
        # Use /user/repos for authenticated requests (works with API token)
        url = f'{self.gitea_url}/api/v1/user/repos'
        params = {'limit': 100}  # Adjust if you have more than 100 repos

        try:
            response = requests.get(url, headers=self.headers, params=params)
            response.raise_for_status()
            repos = response.json()
            # Filter to only repos owned by the specified owner if needed
            if self.owner:
                repos = [r for r in repos if r.get('owner', {}).get('login') == self.owner]
            return repos
        except requests.exceptions.RequestException as e:
            print(f"❌ Error fetching repositories: {e}")
            sys.exit(1)

    def get_existing_branch_protections(self, repo_name: str) -> list[dict]:
        """Get existing branch protections for a repository."""
        url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections'

        try:
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            print(f"   ⚠️  Could not fetch existing protections: {e}")
            return []

    def delete_branch_protection(self, repo_name: str, protection_name: str) -> bool:
        """Delete an existing branch protection rule."""
        url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections/{protection_name}'

        try:
            response = requests.delete(url, headers=self.headers)
            response.raise_for_status()
            return True
        except requests.exceptions.RequestException as e:
            print(f"   ⚠️  Could not delete existing protection: {e}")
            return False

    def apply_branch_protection(self, repo_name: str, config: BranchProtectionConfig) -> bool:
        """
        Apply branch protection rules to a repository.

        Args:
            repo_name: Name of the repository
            config: Branch protection configuration

        Returns:
            True if successful, False otherwise
        """
        url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections'

        payload = {
            'branch_name': config.branch_name,
            'enable_push': config.enable_push,
            'enable_push_whitelist': config.enable_push_whitelist,
            'push_whitelist_usernames': config.push_whitelist_usernames,
            'required_approvals': config.required_approvals,
            'enable_approvals_whitelist': config.enable_approvals_whitelist,
            'approvals_whitelist_usernames': config.approvals_whitelist_usernames,
            'dismiss_stale_approvals': config.dismiss_stale_approvals,
            'enable_status_check': config.enable_status_check,
            'status_check_contexts': config.status_check_contexts,
            'enable_merge_whitelist': config.enable_merge_whitelist,
            'merge_whitelist_usernames': config.merge_whitelist_usernames,
            'block_on_rejected_reviews': config.block_on_rejected_reviews,
            'block_on_outdated_branch': config.block_on_outdated_branch,
            'require_signed_commits': config.require_signed_commits,
            'protected_file_patterns': config.protected_file_patterns,
            'unprotected_file_patterns': config.unprotected_file_patterns,
        }

        try:
            response = requests.post(url, headers=self.headers, json=payload)
            response.raise_for_status()
            return True
        except requests.exceptions.RequestException as e:
            print(f"   ❌ Error: {e}")
            if hasattr(e.response, 'text'):
                print(f"   Response: {e.response.text}")
            return False

    def apply_to_all_repos(self, config: BranchProtectionConfig, dry_run: bool = False):
        """
        Apply branch protection rules to all repositories.

        Args:
            config: Branch protection configuration
            dry_run: If True, only show what would be done without making changes
        """
        repos = self.get_repositories()

        if not repos:
            print("No repositories found.")
            return

        print(f"\n{'DRY RUN - ' if dry_run else ''}Applying branch protection to {len(repos)} repositories...\n")

        success_count = 0
        skip_count = 0
        error_count = 0

        for repo in repos:
            repo_name = repo['name']
            print(f"📦 {repo_name}")

            # Check if branch exists
            default_branch = repo.get('default_branch', 'main')
            if config.branch_name != default_branch:
                print(f"   ℹ️  Default branch is '{default_branch}', protecting '{config.branch_name}' instead")

            if dry_run:
                print(f"   ✓ Would apply protection to '{config.branch_name}' branch")
                skip_count += 1
                continue

            # Check for existing protection
            existing_protections = self.get_existing_branch_protections(repo_name)
            existing_main_protection = None

            for protection in existing_protections:
                if protection.get('branch_name') == config.branch_name:
                    existing_main_protection = protection.get('id') or protection.get('branch_name')
                    break

            # Delete existing protection if it exists
            if existing_main_protection:
                print(f"   🔄 Updating existing protection...")
                self.delete_branch_protection(repo_name, existing_main_protection)

            # Apply new protection
            if self.apply_branch_protection(repo_name, config):
                print(f"   ✅ Successfully applied branch protection")
                success_count += 1
            else:
                print(f"   ❌ Failed to apply branch protection")
                error_count += 1

        print(f"\n{'=' * 60}")
        print(f"Summary:")
        print(f"  ✅ Successful: {success_count}")
        if dry_run:
            print(f"  ⏭️  Skipped (dry run): {skip_count}")
        if error_count > 0:
            print(f"  ❌ Errors: {error_count}")
        print(f"{'=' * 60}\n")


def main():
    """Main entry point."""
    # Configuration - adjust these values or use environment variables
    GITEA_URL = os.getenv('GITEA_URL', 'https://git.manticorum.com')
    GITEA_TOKEN = os.getenv('GITEA_TOKEN')
    GITEA_OWNER = os.getenv('GITEA_OWNER', 'cal')

    # Check for required configuration
    if not GITEA_TOKEN:
        print("❌ Error: GITEA_TOKEN environment variable is required")
        print("\nUsage:")
        print("  export GITEA_TOKEN='your-api-token-here'")
        print("  python apply_branch_protection.py [--dry-run]")
        print("\nOptional environment variables:")
        print("  GITEA_URL (default: https://git.manticorum.com)")
        print("  GITEA_OWNER (default: cal)")
        sys.exit(1)

    # Parse command line arguments
    dry_run = '--dry-run' in sys.argv

    # Configure branch protection rules
    # Adjust these settings as needed
    config = BranchProtectionConfig(
        branch_name="main",
        enable_push=False,  # Disable direct pushes
        required_approvals=1,
        enable_approvals_whitelist=True,
        approvals_whitelist_usernames=[GITEA_OWNER],
        dismiss_stale_approvals=True,
        enable_status_check=True,
        enable_merge_whitelist=True,
        merge_whitelist_usernames=[GITEA_OWNER],
        block_on_rejected_reviews=True,
        block_on_outdated_branch=True,  # Critical for DB repos
        require_signed_commits=False,
    )

    # Display configuration
    print(f"\n{'=' * 60}")
    print(f"Gitea Branch Protection Configuration")
    print(f"{'=' * 60}")
    print(f"Gitea URL: {GITEA_URL}")
    print(f"Owner: {GITEA_OWNER}")
    print(f"Branch: {config.branch_name}")
    print(f"{'=' * 60}")
    print(f"Protection Rules:")
    print(f"  • Direct pushes: {'Disabled' if not config.enable_push else 'Enabled'}")
    print(f"  • Required approvals: {config.required_approvals}")
    print(f"  • Approvals whitelist: {', '.join(config.approvals_whitelist_usernames)}")
    print(f"  • Dismiss stale approvals: {config.dismiss_stale_approvals}")
    print(f"  • Status checks enabled: {config.enable_status_check}")
    print(f"  • Merge whitelist: {', '.join(config.merge_whitelist_usernames)}")
    print(f"  • Block on rejected reviews: {config.block_on_rejected_reviews}")
    print(f"  • Block on outdated branch: {config.block_on_outdated_branch}")
    print(f"  • Require signed commits: {config.require_signed_commits}")
    print(f"{'=' * 60}\n")

    if dry_run:
        print("🔍 DRY RUN MODE - No changes will be made\n")

    # Create manager and apply rules
    manager = GiteaBranchProtectionManager(GITEA_URL, GITEA_TOKEN, GITEA_OWNER)
    manager.apply_to_all_repos(config, dry_run=dry_run)


if __name__ == '__main__':
    main()
