"""
Permission Manager

Controls which tools require permission based on rules.

Permission types supported (OpenCode compatible):
- read: File reading
- edit: File modifications
- write: File creation
- bash: Shell execution
- task: Subagent launching
- skill: Skill loading
- webfetch: URL retrieval
- websearch: Web search operations
- codesearch: Code search operations
- external_directory: Access outside project
- sky_loop: Repeated action detection (FSD mode)

Mode Integration:
When a ModeManager is provided, permissions are determined by the current mode:
- FSD: Skip all permission checks
- Normal: Standard permission prompts
- Safe: Read-only operations only
- Audit: Log everything, ask for all
"""

import json
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, Optional

from groknroll.permissions.patterns import PatternMatcher

if TYPE_CHECKING:
    from groknroll.repl.modes import ModeManager

PermissionResult = Literal["allow", "ask", "deny"]

# All permission types recognized by groknroll (OpenCode compatible)
PERMISSION_TYPES = frozenset(
    [
        "read",
        "write",
        "edit",
        "bash",
        "task",
        "skill",
        "webfetch",
        "websearch",
        "codesearch",
        "external_directory",
        "sky_loop",
        "glob",
        "grep",
        "list",
        "lsp",
        "todowrite",
        "todoread",
        "question",
    ]
)


class PermissionManager:
    """
    Manages permissions for tool execution.

    Supports:
    - Global permissions from groknroll.json
    - Per-agent permission overrides
    - Default deny rules for sensitive files
    - Pattern-based permission matching
    - FSD (Full Send Dangerous) mode - skips ALL permission checks
    """

    def __init__(
        self,
        project_path: Path,
        agent_name: Optional[str] = None,
        fsd_mode: bool = False,
        mode_manager: Optional["ModeManager"] = None,
    ):
        """
        Initialize permission manager.

        Args:
            project_path: Path to project root
            agent_name: Name of current agent (for agent-specific permissions)
            fsd_mode: If True, skip ALL permission checks (dangerous!)
            mode_manager: Optional ModeManager for dynamic permissions based on current mode
        """
        self.project_path = project_path
        self.agent_name = agent_name
        self.config_path = project_path / "groknroll.json"
        self.fsd_mode = fsd_mode
        self.mode_manager = mode_manager

        # Initialize pattern matcher
        self.pattern_matcher = PatternMatcher()

        # Load permissions from config
        self.global_permissions: dict[str, Any] = {}
        self.agent_permissions: dict[str, dict[str, Any]] = {}
        self._load_config()

        # Default deny rules for sensitive files (SEC-004: Expanded patterns)
        self.default_deny_patterns = [
            # Environment and config files
            ".env",
            "**/.env*",
            "**/credentials.json",
            "**/config/master.key",
            # Cryptographic keys and certificates
            "*.key",
            "*.pem",
            "*.crt",
            "*.p12",
            "*.pfx",
            "**/id_rsa*",
            "**/id_ed25519*",
            "**/id_dsa*",
            "**/id_ecdsa*",
            "*.token",
            "*.secret",
            # SSH and GPG
            "**/.ssh/**",
            "**/.gnupg/**",
            # Cloud provider configs
            "**/.aws/**",
            "**/.azure/**",
            "**/.gcloud/**",
            "**/.config/gcloud/**",
            # Kubernetes
            "**/kubeconfig",
            "**/.kube/config",
            "**/.kube/**",
            # Password stores
            "**/.password-store/**",
            "**/pass/**",
            # Git config (can contain credentials)
            "**/.git/config",
            "**/.gitconfig",
            # Database files
            "*.sqlite",
            "*.db",
            # Docker and container secrets
            "**/docker-compose*.yml",
            "**/.docker/config.json",
            # Secrets directories
            "**/secrets/**",
            "**/.secrets/**",
            "**/private/**",
            # API keys and tokens files
            "*api_key*",
            "*apikey*",
            "*secret_key*",
            "*secretkey*",
        ]

    def _load_config(self) -> None:
        """Load permissions from groknroll.json"""
        # Reset permissions to default before loading
        # This ensures deleted/invalid configs don't retain old permissions
        self.global_permissions = {}
        self.agent_permissions = {}

        if not self.config_path.exists():
            return

        try:
            with open(self.config_path) as f:
                config = json.load(f)

            # Load global permissions (must be dict)
            permissions = config.get("permission", {})
            if isinstance(permissions, dict):
                self.global_permissions = permissions

            # Load per-agent permissions (agent must be dict)
            agent_config = config.get("agent")
            if isinstance(agent_config, dict):
                for agent_name, agent_perms in agent_config.items():
                    if isinstance(agent_perms, dict) and "permission" in agent_perms:
                        self.agent_permissions[agent_name] = agent_perms["permission"]

        except (json.JSONDecodeError, IOError):
            # If config is invalid, use empty permissions (already reset above)
            pass

    def check(self, tool_name: str, **kwargs) -> PermissionResult:
        """
        Check if tool execution is allowed.

        Resolution order:
        0. ModeManager - if provided, use mode-based permissions
        1. FSD mode - if enabled, always allow (skip all checks)
        2. Default deny patterns (highest priority for security)
        3. Agent-specific permissions (if agent_name set)
        4. Global permissions
        5. Default: "ask"

        Args:
            tool_name: Name of tool being executed
            **kwargs: Tool arguments (used for pattern matching)

        Returns:
            "allow", "ask", or "deny"
        """
        # ModeManager-based permissions (highest priority)
        if self.mode_manager is not None:
            permissions = self.mode_manager.get_permissions()

            # FSD mode via ModeManager
            if permissions.skip_all_checks:
                return "allow"

            # Get permission for the specific tool type
            perm = self._get_mode_permission(permissions, tool_name)
            if perm == "deny":
                return "deny"

            # Still check default deny patterns for security
            if self._matches_deny_pattern(tool_name, **kwargs):
                return "deny"

            return perm

        # Legacy FSD mode: skip ALL permission checks
        if self.fsd_mode:
            return "allow"

        # Check default deny patterns first (highest priority for security)
        if self._matches_deny_pattern(tool_name, **kwargs):
            return "deny"

        # Check agent-specific permissions
        if self.agent_name and self.agent_name in self.agent_permissions:
            agent_perms = self.agent_permissions[self.agent_name]
            result = self._check_permissions(agent_perms, tool_name, **kwargs)
            if result is not None:
                return result

        # Check global permissions
        result = self._check_permissions(self.global_permissions, tool_name, **kwargs)
        if result is not None:
            return result

        # Default: ask for permission
        return "ask"

    def _get_mode_permission(self, permissions, tool_name: str) -> PermissionResult:
        """
        Get permission for a tool based on ModePermissions.

        Args:
            permissions: ModePermissions object
            tool_name: Tool name

        Returns:
            Permission result
        """
        # Map tool names to permission attributes
        tool_mapping = {
            "read": permissions.read,
            "write": permissions.write,
            "edit": permissions.edit,
            "bash": permissions.bash,
            # Default tools to read permission for safety
            "glob": permissions.read,
            "grep": permissions.read,
            "list": permissions.read,
            "lsp": permissions.read,
            "todoread": permissions.read,
            # Write-like tools
            "todowrite": permissions.write,
            "patch": permissions.edit,
            # External tools default to ask
            "webfetch": "ask",
            "websearch": "ask",
            "codesearch": "ask",
            "task": "ask",
            "skill": "ask",
            "question": "allow",  # Questions are always allowed
        }

        return tool_mapping.get(tool_name, "ask")

    def _check_permissions(
        self, permissions: dict[str, Any], tool_name: str, **kwargs
    ) -> Optional[PermissionResult]:
        """
        Check permissions dict for tool.

        Args:
            permissions: Permission dict to check
            tool_name: Tool name
            **kwargs: Tool arguments

        Returns:
            Permission result or None if no match
        """
        # Exact tool name match
        if tool_name in permissions:
            perm = permissions[tool_name]
            if isinstance(perm, str) and perm in ("allow", "ask", "deny"):
                return perm  # type: ignore

        # Wildcard match: "*"
        if "*" in permissions:
            perm = permissions["*"]
            if isinstance(perm, str) and perm in ("allow", "ask", "deny"):
                return perm  # type: ignore

        return None

    def _matches_deny_pattern(self, tool_name: str, **kwargs) -> bool:
        """
        Check if tool execution matches default deny patterns.

        Args:
            tool_name: Tool name
            **kwargs: Tool arguments

        Returns:
            True if matches deny pattern
        """
        # Only check file-related tools
        if tool_name not in ("read", "write", "edit"):
            return False

        # Get file path from kwargs
        path = kwargs.get("path")
        if not path:
            return False

        # Convert to Path for pattern matching
        file_path = Path(path)

        # Check against deny patterns
        for pattern in self.default_deny_patterns:
            if self._match_pattern(str(file_path), pattern):
                return True

        return False

    def _match_pattern(self, text: str, pattern: str) -> bool:
        """
        Pattern matching with wildcards using PatternMatcher.

        Supports:
        - * (zero or more characters)
        - ? (exactly one character)
        - ** (directory wildcard)

        Args:
            text: Text to match
            pattern: Pattern with wildcards

        Returns:
            True if text matches pattern
        """
        # Handle ** directory wildcards by checking if pattern parts are in path
        if "**" in pattern:
            # Split pattern into parts
            parts = pattern.split("**")

            # For pattern like "**/secrets/**", check if "secrets" is in the path
            if len(parts) == 3:  # Before, middle, after
                before, middle, after = parts
                middle = middle.strip("/")

                # Check if middle part is in the path
                if middle and middle in text:
                    return True

            # For pattern like "**/.env*", check if filename matches
            elif len(parts) == 2:
                before, after = parts
                after = after.lstrip("/")

                if after:
                    filename = Path(text).name
                    if self.pattern_matcher.match(filename, after):
                        return True
                    # Also check full path
                    if self.pattern_matcher.match(text, f"*{after}"):
                        return True

        # Check if filename matches
        filename = Path(text).name
        if self.pattern_matcher.match(filename, pattern):
            return True

        # Check if full path matches
        if self.pattern_matcher.match(text, pattern):
            return True

        return False

    def set_agent(self, agent_name: Optional[str]) -> None:
        """
        Set current agent for permission checks.

        Args:
            agent_name: Agent name or None for no agent
        """
        self.agent_name = agent_name

    def set_mode_manager(self, mode_manager: Optional["ModeManager"]) -> None:
        """
        Set mode manager for dynamic permissions.

        Args:
            mode_manager: ModeManager instance or None
        """
        self.mode_manager = mode_manager

    def reload_config(self) -> None:
        """Reload permissions from config file"""
        self._load_config()

    def check_skill(self, skill_name: str) -> PermissionResult:
        """
        Check if skill execution is allowed.

        Resolution order:
        0. FSD mode - if enabled, always allow
        1. Agent-specific skill permissions (if agent_name set)
        2. Global skill permissions
        3. Pattern matching with wildcards
        4. Default: "ask"

        Args:
            skill_name: Name of skill being executed

        Returns:
            "allow", "ask", or "deny"

        Example config:
            {
              "permission": {
                "skill": {
                  "git-release": "allow",
                  "dangerous-*": "deny",
                  "*": "ask"
                }
              }
            }
        """
        # FSD mode: skip ALL permission checks
        if self.fsd_mode:
            return "allow"

        # Check agent-specific skill permissions
        if self.agent_name and self.agent_name in self.agent_permissions:
            agent_perms = self.agent_permissions[self.agent_name]
            result = self._check_skill_permissions(agent_perms, skill_name)
            if result is not None:
                return result

        # Check global skill permissions
        result = self._check_skill_permissions(self.global_permissions, skill_name)
        if result is not None:
            return result

        # Default: ask for permission
        return "ask"

    def _check_skill_permissions(
        self,
        permissions: dict[str, Any],
        skill_name: str,
    ) -> Optional[PermissionResult]:
        """
        Check skill permissions in a permission dict.

        Args:
            permissions: Permission dict (may contain "skill" key)
            skill_name: Skill name to check

        Returns:
            Permission result or None if no match
        """
        skill_perms = permissions.get("skill", {})
        if not isinstance(skill_perms, dict):
            return None

        # Exact skill name match (highest priority)
        if skill_name in skill_perms:
            perm = skill_perms[skill_name]
            if isinstance(perm, str) and perm in ("allow", "ask", "deny"):
                return perm  # type: ignore

        # Pattern matching with wildcards
        for pattern, perm in skill_perms.items():
            if pattern == "*":
                continue  # Handle wildcard last
            if pattern == skill_name:
                continue  # Already handled above

            # Use pattern matcher for wildcards
            if self.pattern_matcher.match(skill_name, pattern):
                if isinstance(perm, str) and perm in ("allow", "ask", "deny"):
                    return perm  # type: ignore

        # Wildcard match: "*" (lowest priority)
        if "*" in skill_perms:
            perm = skill_perms["*"]
            if isinstance(perm, str) and perm in ("allow", "ask", "deny"):
                return perm  # type: ignore

        return None

    def check_websearch(self, query: Optional[str] = None) -> PermissionResult:
        """
        Check if web search is allowed.

        OpenCode compatible permission type for web search operations.

        Args:
            query: Optional search query (for logging/auditing)

        Returns:
            "allow", "ask", or "deny"

        Example config:
            {
              "permission": {
                "websearch": "allow"
              }
            }
        """
        return self.check("websearch")

    def check_codesearch(self, query: Optional[str] = None) -> PermissionResult:
        """
        Check if code search is allowed.

        OpenCode compatible permission type for code search operations.

        Args:
            query: Optional search query (for logging/auditing)

        Returns:
            "allow", "ask", or "deny"

        Example config:
            {
              "permission": {
                "codesearch": "allow"
              }
            }
        """
        return self.check("codesearch")

    def check_external_directory(self, path: str) -> PermissionResult:
        """
        Check if accessing a path outside the project is allowed.

        OpenCode compatible permission type for external directory access.
        This is checked when a tool tries to access files outside the
        project root directory.

        Args:
            path: The absolute path being accessed

        Returns:
            "allow", "ask", or "deny"

        Example config:
            {
              "permission": {
                "external_directory": "ask"
              }
            }
        """
        # Convert to absolute path
        abs_path = Path(path).resolve()

        # Check if path is within project
        try:
            abs_path.relative_to(self.project_path.resolve())
            # Path is within project, no external_directory check needed
            return "allow"
        except ValueError:
            # Path is outside project, check external_directory permission
            pass

        # Check agent-specific permissions
        if self.agent_name and self.agent_name in self.agent_permissions:
            agent_perms = self.agent_permissions[self.agent_name]
            result = self._check_permissions(agent_perms, "external_directory")
            if result is not None:
                return result

        # Check global permissions
        result = self._check_permissions(self.global_permissions, "external_directory")
        if result is not None:
            return result

        # Default: ask for permission on external directories
        return "ask"

    def is_external_path(self, path: str) -> bool:
        """
        Check if a path is outside the project directory.

        Args:
            path: Path to check

        Returns:
            True if path is outside project directory
        """
        abs_path = Path(path).resolve()
        try:
            abs_path.relative_to(self.project_path.resolve())
            return False
        except ValueError:
            return True
