"""
Merge Gate - Protected branch controls with locking.

Implements:
- Protected branch enforcement
- Merge locking (one at a time)
- Pre-merge readiness validation
- Post-merge hooks
"""

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Optional, Dict, List, Any, Callable, Set
import asyncio
import subprocess
import logging

from .readiness import MergeReadiness, ReadinessReport, CheckStatus

logger = logging.getLogger(__name__)


class MergeStatus(Enum):
    """Status of a merge operation."""
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"
    BLOCKED = "blocked"


class MergeStrategy(Enum):
    """Git merge strategy to use."""
    MERGE = "merge"
    SQUASH = "squash"
    REBASE = "rebase"
    FAST_FORWARD = "ff-only"


@dataclass
class MergeRequest:
    """A request to merge branches."""

    request_id: str
    source_branch: str
    target_branch: str

    # Merge options
    strategy: MergeStrategy = MergeStrategy.MERGE
    commit_message: Optional[str] = None
    squash_commits: bool = False
    delete_source_branch: bool = False

    # Metadata
    requested_by: str = "system"
    requested_at: datetime = field(default_factory=datetime.now)

    # Status
    status: MergeStatus = MergeStatus.PENDING
    readiness_report: Optional[ReadinessReport] = None

    # Result
    merge_commit: Optional[str] = None
    error_message: Optional[str] = None
    completed_at: Optional[datetime] = None


@dataclass
class MergeResult:
    """Result of a merge operation."""

    success: bool
    request: MergeRequest

    # Git info
    merge_commit: Optional[str] = None
    commits_merged: int = 0
    files_changed: int = 0

    # Timing
    started_at: datetime = field(default_factory=datetime.now)
    completed_at: Optional[datetime] = None
    duration_ms: int = 0

    # Error info
    error_message: Optional[str] = None
    error_type: Optional[str] = None


class MergeGate:
    """
    Controls merges to protected branches.

    Features:
    - Merge locking (one at a time)
    - Pre-merge readiness validation
    - Protected branch enforcement
    - Post-merge hooks
    """

    def __init__(
        self,
        db: Any = None,
        readiness_checker: Optional[MergeReadiness] = None,
        config: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize merge gate.

        Args:
            db: Database for persistence
            readiness_checker: MergeReadiness instance
            config: Configuration options
        """
        self.db = db
        self.readiness = readiness_checker or MergeReadiness(db=db)
        self.config = config or {}

        # Protected branches (no direct pushes, require merge gate)
        self._protected_branches: Set[str] = set(
            self.config.get("protected_branches", ["main", "master", "production"])
        )

        # Merge lock
        self._merge_lock = asyncio.Lock()
        self._current_merge: Optional[MergeRequest] = None

        # Request tracking
        self._pending_requests: Dict[str, MergeRequest] = {}
        self._completed_requests: List[MergeRequest] = []

        # Hooks
        self._pre_merge_hooks: List[Callable] = []
        self._post_merge_hooks: List[Callable] = []

        # Settings
        self._require_readiness = self.config.get("require_readiness", True)
        self._run_tests = self.config.get("run_tests", True)
        self._auto_delete_branch = self.config.get("auto_delete_branch", False)
        self._default_strategy = MergeStrategy(
            self.config.get("default_strategy", "merge")
        )

        # ID generation
        self._next_id = 1

    def add_protected_branch(self, branch: str) -> None:
        """Add a branch to protected list."""
        self._protected_branches.add(branch)
        logger.info(f"Added protected branch: {branch}")

    def remove_protected_branch(self, branch: str) -> None:
        """Remove a branch from protected list."""
        self._protected_branches.discard(branch)
        logger.info(f"Removed protected branch: {branch}")

    def is_protected(self, branch: str) -> bool:
        """Check if a branch is protected."""
        return branch in self._protected_branches

    def get_protected_branches(self) -> Set[str]:
        """Get set of protected branches."""
        return self._protected_branches.copy()

    def register_pre_merge_hook(self, hook: Callable) -> None:
        """
        Register a pre-merge hook.

        Hook signature: async def hook(request: MergeRequest) -> bool
        Return False to block the merge.
        """
        self._pre_merge_hooks.append(hook)

    def register_post_merge_hook(self, hook: Callable) -> None:
        """
        Register a post-merge hook.

        Hook signature: async def hook(result: MergeResult) -> None
        """
        self._post_merge_hooks.append(hook)

    def _generate_request_id(self) -> str:
        """Generate a unique request ID."""
        request_id = f"merge-{self._next_id:04d}"
        self._next_id += 1
        return request_id

    async def request_merge(
        self,
        source_branch: str,
        target_branch: str,
        strategy: Optional[MergeStrategy] = None,
        commit_message: Optional[str] = None,
        requested_by: str = "system",
        skip_readiness: bool = False,
        working_dir: Optional[str] = None,
    ) -> MergeResult:
        """
        Request a merge operation.

        Args:
            source_branch: Branch to merge from
            target_branch: Branch to merge into
            strategy: Merge strategy to use
            commit_message: Custom commit message
            requested_by: Who requested the merge
            skip_readiness: Skip readiness checks (not recommended)
            working_dir: Git working directory

        Returns:
            MergeResult with outcome
        """
        request = MergeRequest(
            request_id=self._generate_request_id(),
            source_branch=source_branch,
            target_branch=target_branch,
            strategy=strategy or self._default_strategy,
            commit_message=commit_message,
            requested_by=requested_by,
        )

        self._pending_requests[request.request_id] = request

        # Check if target is protected
        if target_branch not in self._protected_branches:
            logger.warning(f"Merge to non-protected branch: {target_branch}")

        # Run readiness checks
        if self._require_readiness and not skip_readiness:
            logger.info(f"Running readiness checks for {source_branch} → {target_branch}")

            report = await self.readiness.check_readiness(
                branch=source_branch,
                target_branch=target_branch,
                run_tests=self._run_tests,
                working_dir=working_dir,
            )

            request.readiness_report = report

            if not report.is_ready:
                request.status = MergeStatus.BLOCKED
                request.error_message = "Readiness checks failed"

                logger.warning(f"Merge blocked: {report.blocking_issues}")

                return MergeResult(
                    success=False,
                    request=request,
                    error_message="Readiness checks failed",
                    error_type="readiness_failed",
                )

        # Run pre-merge hooks
        for hook in self._pre_merge_hooks:
            try:
                allowed = await hook(request)
                if not allowed:
                    request.status = MergeStatus.BLOCKED
                    request.error_message = "Blocked by pre-merge hook"

                    return MergeResult(
                        success=False,
                        request=request,
                        error_message="Blocked by pre-merge hook",
                        error_type="hook_blocked",
                    )
            except Exception as e:
                logger.error(f"Pre-merge hook failed: {e}")

        # Acquire merge lock
        result = await self._execute_merge_with_lock(request, working_dir)

        # Move to completed
        self._pending_requests.pop(request.request_id, None)
        self._completed_requests.append(request)

        # Run post-merge hooks
        for hook in self._post_merge_hooks:
            try:
                await hook(result)
            except Exception as e:
                logger.error(f"Post-merge hook failed: {e}")

        return result

    async def _execute_merge_with_lock(
        self,
        request: MergeRequest,
        working_dir: Optional[str],
    ) -> MergeResult:
        """Execute merge with lock protection."""
        start_time = datetime.now()

        async with self._merge_lock:
            self._current_merge = request
            request.status = MergeStatus.IN_PROGRESS

            try:
                result = await self._do_merge(request, working_dir)
                result.started_at = start_time
                result.completed_at = datetime.now()
                result.duration_ms = int(
                    (result.completed_at - start_time).total_seconds() * 1000
                )
                return result

            finally:
                self._current_merge = None

    async def _do_merge(
        self,
        request: MergeRequest,
        working_dir: Optional[str],
    ) -> MergeResult:
        """Perform the actual merge operation."""
        try:
            # Build merge command based on strategy
            if request.strategy == MergeStrategy.SQUASH:
                merge_cmd = ["git", "merge", "--squash", request.source_branch]
            elif request.strategy == MergeStrategy.REBASE:
                merge_cmd = ["git", "rebase", request.source_branch]
            elif request.strategy == MergeStrategy.FAST_FORWARD:
                merge_cmd = ["git", "merge", "--ff-only", request.source_branch]
            else:  # Standard merge
                merge_cmd = ["git", "merge", "--no-ff", request.source_branch]

            # Add commit message if provided
            if request.commit_message and request.strategy != MergeStrategy.REBASE:
                merge_cmd.extend(["-m", request.commit_message])

            # Execute merge
            process = await asyncio.create_subprocess_exec(
                *merge_cmd,
                cwd=working_dir,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )

            stdout, stderr = await process.communicate()

            if process.returncode != 0:
                request.status = MergeStatus.FAILED
                error_msg = stderr.decode() if stderr else "Merge failed"
                request.error_message = error_msg

                return MergeResult(
                    success=False,
                    request=request,
                    error_message=error_msg,
                    error_type="git_merge_failed",
                )

            # For squash merges, need to commit
            if request.strategy == MergeStrategy.SQUASH:
                commit_msg = request.commit_message or f"Squash merge {request.source_branch}"
                commit_process = await asyncio.create_subprocess_exec(
                    "git", "commit", "-m", commit_msg,
                    cwd=working_dir,
                    stdout=asyncio.subprocess.PIPE,
                    stderr=asyncio.subprocess.PIPE,
                )
                await commit_process.communicate()

            # Get merge commit hash
            hash_process = await asyncio.create_subprocess_exec(
                "git", "rev-parse", "HEAD",
                cwd=working_dir,
                stdout=asyncio.subprocess.PIPE,
            )
            hash_stdout, _ = await hash_process.communicate()
            merge_commit = hash_stdout.decode().strip() if hash_stdout else None

            # Get stats
            stats = await self._get_merge_stats(request.source_branch, working_dir)

            # Delete source branch if configured
            if request.delete_source_branch or self._auto_delete_branch:
                await self._delete_branch(request.source_branch, working_dir)

            request.status = MergeStatus.COMPLETED
            request.merge_commit = merge_commit
            request.completed_at = datetime.now()

            logger.info(f"Merge completed: {request.source_branch} → {request.target_branch}")

            return MergeResult(
                success=True,
                request=request,
                merge_commit=merge_commit,
                commits_merged=stats.get("commits", 0),
                files_changed=stats.get("files", 0),
            )

        except Exception as e:
            request.status = MergeStatus.FAILED
            request.error_message = str(e)

            logger.error(f"Merge failed: {e}")

            return MergeResult(
                success=False,
                request=request,
                error_message=str(e),
                error_type="exception",
            )

    async def _get_merge_stats(
        self,
        source_branch: str,
        working_dir: Optional[str],
    ) -> Dict[str, int]:
        """Get statistics about the merge."""
        try:
            # Get commit count
            process = await asyncio.create_subprocess_exec(
                "git", "log", "--oneline", f"HEAD...{source_branch}",
                cwd=working_dir,
                stdout=asyncio.subprocess.PIPE,
            )
            stdout, _ = await process.communicate()
            commits = len(stdout.decode().strip().split('\n')) if stdout else 0

            # Get file count
            process = await asyncio.create_subprocess_exec(
                "git", "diff", "--stat", f"HEAD~1",
                cwd=working_dir,
                stdout=asyncio.subprocess.PIPE,
            )
            stdout, _ = await process.communicate()
            # Parse "X files changed" from output
            files = 0
            if stdout:
                output = stdout.decode()
                if "files changed" in output or "file changed" in output:
                    # Last line contains summary
                    last_line = output.strip().split('\n')[-1]
                    parts = last_line.split()
                    if parts:
                        try:
                            files = int(parts[0])
                        except ValueError:
                            pass

            return {"commits": commits, "files": files}

        except Exception:
            return {"commits": 0, "files": 0}

    async def _delete_branch(
        self,
        branch: str,
        working_dir: Optional[str],
    ) -> bool:
        """Delete a branch after merge."""
        try:
            process = await asyncio.create_subprocess_exec(
                "git", "branch", "-d", branch,
                cwd=working_dir,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            await process.communicate()

            if process.returncode == 0:
                logger.info(f"Deleted branch: {branch}")
                return True

            return False

        except Exception as e:
            logger.warning(f"Failed to delete branch {branch}: {e}")
            return False

    def cancel_merge(self, request_id: str) -> bool:
        """
        Cancel a pending merge request.

        Args:
            request_id: ID of request to cancel

        Returns:
            True if cancelled successfully
        """
        if request_id in self._pending_requests:
            request = self._pending_requests[request_id]

            if request.status == MergeStatus.PENDING:
                request.status = MergeStatus.CANCELLED
                self._pending_requests.pop(request_id)
                self._completed_requests.append(request)
                logger.info(f"Cancelled merge request: {request_id}")
                return True

        return False

    def get_pending_requests(self) -> List[MergeRequest]:
        """Get list of pending merge requests."""
        return list(self._pending_requests.values())

    def get_current_merge(self) -> Optional[MergeRequest]:
        """Get the currently executing merge (if any)."""
        return self._current_merge

    def is_merge_in_progress(self) -> bool:
        """Check if a merge is currently in progress."""
        return self._current_merge is not None

    def get_recent_merges(self, limit: int = 10) -> List[MergeRequest]:
        """Get recent completed merge requests."""
        return self._completed_requests[-limit:]

    async def check_branch_protection(
        self,
        branch: str,
        action: str,
        agent_id: Optional[str] = None,
    ) -> bool:
        """
        Check if an action is allowed on a protected branch.

        Args:
            branch: Branch to check
            action: Action being attempted (push, force-push, delete)
            agent_id: Agent attempting the action

        Returns:
            True if action is allowed
        """
        if branch not in self._protected_branches:
            return True

        # Protected branches don't allow direct pushes
        if action == "push":
            logger.warning(f"Direct push to protected branch blocked: {branch}")
            return False

        # Never allow force push to protected branches
        if action == "force-push":
            logger.error(f"Force push to protected branch blocked: {branch}")
            return False

        # Never allow deletion of protected branches
        if action == "delete":
            logger.error(f"Delete of protected branch blocked: {branch}")
            return False

        return True

    def get_gate_status(self) -> Dict[str, Any]:
        """Get current gate status."""
        return {
            "protected_branches": list(self._protected_branches),
            "merge_in_progress": self.is_merge_in_progress(),
            "current_merge": {
                "request_id": self._current_merge.request_id,
                "source": self._current_merge.source_branch,
                "target": self._current_merge.target_branch,
            } if self._current_merge else None,
            "pending_requests": len(self._pending_requests),
            "completed_today": len([
                r for r in self._completed_requests
                if r.completed_at and r.completed_at.date() == datetime.now().date()
            ]),
            "settings": {
                "require_readiness": self._require_readiness,
                "run_tests": self._run_tests,
                "auto_delete_branch": self._auto_delete_branch,
                "default_strategy": self._default_strategy.value,
            },
        }


# Singleton instance
_merge_gate: Optional[MergeGate] = None


def get_merge_gate() -> Optional[MergeGate]:
    """Get the global merge gate instance."""
    return _merge_gate


def set_merge_gate(gate: MergeGate) -> None:
    """Set the global merge gate instance."""
    global _merge_gate
    _merge_gate = gate
