"""Service for executing hooks (UI-agnostic)."""

from __future__ import annotations

import asyncio
import os
import re
import socket
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any

from portal.core.domain.models import (
    Condition,
    Hook,
    HookErrorStrategy,
    HookResult,
    HookStage,
    HookType,
)

if TYPE_CHECKING:
    from portal.core.domain.models import Worktree
    from portal.core.events import EventBus
    from portal.shared.protocols.infrastructure_protocols import (
        FileSystemOperationsProtocol,
        ShellExecutorProtocol,
        TemplateEngineProtocol,
    )


class HookService:
    """Service for executing hooks (UI-agnostic)."""

    def __init__(
        self,
        shell_executor: ShellExecutorProtocol,
        fs_operations: FileSystemOperationsProtocol,
        template_engine: TemplateEngineProtocol,
        event_bus: EventBus,
    ):
        self.shell_executor = shell_executor
        self.fs_operations = fs_operations
        self.template_engine = template_engine
        self.event_bus = event_bus

    async def execute_hooks(
        self,
        stage: HookStage,
        hooks: list[Hook],
        worktree: Worktree,
        context: dict[str, Any] | None = None,
    ) -> HookResult:
        """Execute hooks for a given stage.

        Args:
            stage: Hook stage being executed
            hooks: List of hooks to execute
            worktree: Worktree being operated on
            context: Additional context for hook execution

        Returns:
            HookResult with execution details
        """
        start_time = time.time()
        result = HookResult(success=True, duration_ms=0)

        # Build complete context
        context = self._build_context(worktree, context or {})

        for hook in hooks:
            # Check condition
            if hook.condition and not await self._check_condition(hook.condition, worktree):
                result.skipped.append(self._hook_name(hook))
                continue

            # Emit pre-execution event
            self.event_bus.emit(
                "hook.executing", {"hook": hook, "stage": stage, "worktree": worktree}
            )

            try:
                # Execute hook with timeout
                hook_result = await asyncio.wait_for(
                    self._execute_single_hook(hook, worktree, context), timeout=hook.timeout
                )

                if hook_result:
                    result.executed.append(self._hook_name(hook))

                    # Emit success event
                    self.event_bus.emit("hook.executed", {"hook": hook, "success": True})
                else:
                    # Hook returned False (soft failure)
                    self._handle_hook_failure(hook, result, "Hook returned false")

            except TimeoutError:
                error_msg = f"Hook timed out after {hook.timeout}s"
                self._handle_hook_failure(hook, result, error_msg)

            except Exception as e:
                self._handle_hook_failure(hook, result, str(e))

                if hook.on_error == HookErrorStrategy.FAIL:
                    result.success = False
                    break  # Stop executing further hooks

        # Calculate duration
        result.duration_ms = int((time.time() - start_time) * 1000)

        return result

    async def _execute_single_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute a single hook based on its type."""
        if hook.type == HookType.COPY:
            return await self._execute_copy_hook(hook, worktree, context)

        elif hook.type == HookType.COMMAND:
            return await self._execute_command_hook(hook, worktree, context)

        elif hook.type == HookType.TEMPLATE:
            return await self._execute_template_hook(hook, worktree, context)

        elif hook.type == HookType.SYMLINK:
            return await self._execute_symlink_hook(hook, worktree, context)

        elif hook.type == HookType.MKDIR:
            return await self._execute_mkdir_hook(hook, worktree, context)

        elif hook.type == HookType.ENV_UPDATE:
            return await self._execute_env_update_hook(hook, worktree, context)

        elif hook.type == HookType.SCRIPT:
            return await self._execute_script_hook(hook, worktree, context)

        elif hook.type == HookType.GIT:
            return await self._execute_git_hook(hook, worktree, context)

        else:
            raise ValueError(f"Unknown hook type: {hook.type}")

    async def _execute_copy_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute a copy hook."""
        # Get main repository path for source resolution
        main_repo_path = self._find_project_root(worktree.path)
        if not main_repo_path:
            # Fallback to parent directory if can't find main repo
            main_repo_path = worktree.path.parent.parent

        # Source is relative to main repository, target is relative to worktree
        source = self._resolve_path(hook.config["from"], main_repo_path, context)
        target = self._resolve_path(
            hook.config["to"], worktree.path, context, relative_to_worktree=True
        )
        return await self.fs_operations.copy_file(source, target)

    async def _execute_command_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute a command hook."""
        command = self._expand_variables(hook.config["command"], context)

        # Security: Basic command injection prevention (unless explicitly allowed)
        if not hook.config.get("allow_shell", False):
            dangerous_patterns = [";", "&&", "||", ">", "<", "&", "`", "$(", "${"]
            for pattern in dangerous_patterns:
                if pattern in command:
                    return False

        env = {k: self._expand_variables(v, context) for k, v in hook.config.get("env", {}).items()}

        result = await self.shell_executor.execute(
            command, cwd=worktree.path, env=env, capture_output=True
        )
        return result.success

    async def _execute_template_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute a template hook."""
        template_str = self._expand_variables(hook.config["template"], context)
        template_path = Path(template_str)

        if not template_path.is_absolute():
            project_template = worktree.path / ".portal" / "templates" / template_path
            if project_template.exists():
                template_path = project_template
            else:
                template_path = Path.home() / ".portal" / "templates" / template_path

        output_path = self._resolve_path(
            hook.config["output"], worktree.path, context, relative_to_worktree=True
        )

        template_context = context.copy()
        if "variables" in hook.config:
            template_context.update(hook.config["variables"])

        return await self.template_engine.render_to_file(
            template_path, output_path, template_context
        )

    async def _execute_env_update_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute an environment file update hook."""
        env_file = self._resolve_path(
            hook.config["file"], worktree.path, context, relative_to_worktree=True
        )

        updates = {
            k: self._expand_variables(str(v), context) for k, v in hook.config["updates"].items()
        }

        for key, value in updates.items():
            if value == "auto" and "PORT" in key:
                updates[key] = str(await self._find_available_port())

        return await self.fs_operations.update_env_file(env_file, updates)

    async def _execute_mkdir_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute a mkdir hook."""
        paths = hook.config.get("paths", [hook.config.get("path")])
        if not paths:
            return False

        for path_str in paths:
            path = self._resolve_path(path_str, worktree.path, context, relative_to_worktree=True)
            if not await self.fs_operations.create_directory(path):
                return False
        return True

    async def _execute_symlink_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute a symlink hook."""
        source = self._resolve_path(hook.config["source"], worktree.path, context)
        target = self._resolve_path(
            hook.config["target"], worktree.path, context, relative_to_worktree=True
        )
        return await self.fs_operations.create_symlink(source, target)

    async def _execute_script_hook(
        self, hook: Hook, worktree: Worktree, context: dict[str, Any]
    ) -> bool:
        """Execute a custom script hook."""
        script_str = self._expand_variables(hook.config["script"], context)
        script_path = Path(script_str)

        if not script_path.is_absolute():
            script_path = worktree.path / ".portal" / "hooks" / script_path

        if not script_path.exists() or not script_path.is_file():
            return False

        if not await self.fs_operations.make_executable(script_path):
            return False

        result = await self.shell_executor.execute(
            str(script_path),
            cwd=worktree.path,
            env={"WORKTREE_PATH": str(worktree.path)},
            capture_output=True,
        )
        return result.success

    async def _execute_git_hook(
        self,
        hook: Hook,
        worktree: Worktree,
        context: dict[str, Any],  # noqa: ARG002
    ) -> bool:
        """Execute a git operation hook."""
        operation = hook.config["operation"]

        if operation == "fetch":
            command = "git fetch --all"
        elif operation == "pull":
            command = f"git pull origin {worktree.branch}"
        elif operation == "submodule_update":
            command = "git submodule update --init --recursive"
        else:
            command = f"git {operation}"

        result = await self.shell_executor.execute(command, cwd=worktree.path, capture_output=True)

        return result.success

    async def _check_condition(self, condition_str: str, worktree: Worktree) -> bool:
        """Check if a condition is met."""
        condition = Condition.parse(condition_str)
        result = False

        # For file/dir existence checks in the context of copy hooks,
        # check relative to main repository, not worktree
        main_repo_path = self._find_project_root(worktree.path)
        if not main_repo_path:
            # Fallback to parent directory if can't find main repo
            main_repo_path = worktree.path.parent.parent

        if condition.type == "file_exists":
            # Check in main repository for copy source validation
            path = main_repo_path / condition.value
            result = path.is_file()

        elif condition.type == "file_not_exists":
            path = worktree.path / condition.value
            result = not path.exists()

        elif condition.type == "dir_exists":
            # Check in main repository for copy source validation
            path = main_repo_path / condition.value
            result = path.is_dir()

        elif condition.type == "env":
            # Check environment variable
            if "=" in condition.value:
                var, expected = condition.value.split("=", 1)
                result = os.environ.get(var) == expected
            else:
                result = condition.value in os.environ

        elif condition.type == "platform":
            result = os.name.startswith(condition.value)

        elif condition.type == "command_success":
            shell_result = await self.shell_executor.execute(
                condition.value, cwd=worktree.path, capture_output=True
            )
            result = shell_result.success

        # Apply negation if needed
        if condition.negate:
            result = not result

        return result

    def _expand_variables(self, text: str, context: dict[str, Any]) -> str:
        """Expand variables in text using context."""

        # Replace {{variable}} patterns
        pattern = r"\{\{(\w+)\}\}"

        def replacer(match: re.Match[str]) -> str:
            key = match.group(1)
            return str(context.get(key, match.group(0)))

        return re.sub(pattern, replacer, text)

    def _build_context(self, worktree: Worktree, base_context: dict[str, Any]) -> dict[str, Any]:
        """Build complete context for variable expansion."""
        context = base_context.copy()

        # Derive project name from worktree path if not already in context
        # Assuming structure: /path/to/project_worktrees/worktree_name
        if "project" not in context:
            project = worktree.path.parent.name
            if project.endswith("_worktrees"):
                project = project[:-10]  # Remove '_worktrees' suffix
        else:
            project = context["project"]

        context.update(
            {
                "worktree": str(worktree.path),
                "worktree_name": worktree.name,
                "worktree_path": str(worktree.path),
                "branch": worktree.branch,
                "project": project,
                "home": str(Path.home()),
                "user": os.environ.get("USER", "unknown"),
            }
        )

        return context

    def _hook_name(self, hook: Hook) -> str:
        """Get display name for hook."""
        if hook.name:
            return hook.name

        # Generate name from type and config
        if hook.type == HookType.COPY:
            return f"Copy: {hook.config['from']} → {hook.config['to']}"
        elif hook.type == HookType.COMMAND:
            cmd = hook.config["command"]
            if len(cmd) > 50:
                cmd = cmd[:47] + "..."
            return f"Command: {cmd}"
        else:
            return f"{hook.type.value} hook"

    def _handle_hook_failure(self, hook: Hook, result: HookResult, error_msg: str) -> None:
        """Handle hook failure based on error strategy."""
        result.failed.append(self._hook_name(hook))

        if hook.on_error == HookErrorStrategy.FAIL:
            result.errors.append(error_msg)
            result.success = False
        elif hook.on_error == HookErrorStrategy.WARN:
            result.warnings.append(error_msg)
        # IGNORE: do nothing

        # Emit failure event
        self.event_bus.emit("hook.failed", {"hook": hook, "error": error_msg})

    async def _find_available_port(self, start: int = 3000, end: int = 9999) -> int:
        """Find an available port in the given range."""
        for port in range(start, end):
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
                try:
                    sock.bind(("", port))
                    return port
                except OSError:
                    continue
        raise ValueError(f"No available ports in range {start}-{end}")

    def _resolve_path(
        self,
        path_str: str,
        base_path: Path,
        context: dict[str, Any],
        relative_to_worktree: bool = False,
    ) -> Path:
        """Resolve and validate a path with variable expansion."""
        expanded = self._expand_variables(path_str, context)
        path = Path(expanded)

        # Security: Prevent directory traversal attacks
        if ".." in path.parts:
            raise ValueError(f"Path traversal detected in: {expanded}")

        if relative_to_worktree or not path.is_absolute():
            return base_path / path
        return path

    def _find_project_root(self, start_path: Path) -> Path | None:
        """Find project root by looking for .git directory or file.

        For regular repositories, this finds the .git directory.
        For worktrees, this follows the .git file to find the main repository.

        Args:
            start_path: Starting directory to search from

        Returns:
            Project root path or None if not found
        """
        current_path = start_path
        while current_path != current_path.parent:
            git_path = current_path / ".git"
            if git_path.exists():
                # Check if it's a worktree (file) or regular repo (directory)
                if git_path.is_file():
                    # It's a worktree - read the gitdir reference
                    try:
                        with open(git_path, "r") as f:
                            gitdir_line = f.read().strip()
                            if gitdir_line.startswith("gitdir: "):
                                # Extract path: "gitdir: /path/to/main/repo/.git/worktrees/name"
                                gitdir = Path(gitdir_line[8:])  # Remove 'gitdir: ' prefix
                                # Go up from .git/worktrees/name to find main repo
                                if gitdir.parts[-2] == "worktrees" and gitdir.parts[-3] == ".git":
                                    # Return the main repository root
                                    return gitdir.parent.parent.parent
                    except (IOError, IndexError, ValueError):
                        pass
                else:
                    # It's a regular repository
                    return current_path
            current_path = current_path.parent
        return None
