"""Hooks management for Claude Code."""

from __future__ import annotations

import json
import logging
import os
import shlex
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from .asset_discovery import find_claude_directories
from .base import _resolve_claude_dir


def _check_hook_executable(command: str) -> Optional[str]:
    """Check if the script in a command exists."""
    try:
        try:
            parts = shlex.split(command)
        except ValueError as e:
            return f"Malformed command: {e}"

        if not parts:
            return "Empty command"

        # If it's interpreter script.py, check script.py
        script_path_str = ""
        interpreters = (
            "python",
            "python3",
            "bash",
            "sh",
            "node",
            "uv",
            "env",
            "ruby",
            "perl",
            "deno",
            "bun",
            "pipx",
        )
        if parts[0] in interpreters and len(parts) > 1:
            # Skip flags if any (naive check)
            idx = 1
            while idx < len(parts) and parts[idx].startswith("-"):
                idx += 1

            # Handle /usr/bin/env python3 script.py
            if idx < len(parts) and parts[idx - 1] == "env" and parts[idx] in interpreters:
                idx += 1
                while idx < len(parts) and parts[idx].startswith("-"):
                    idx += 1

            if idx < len(parts):
                script_path_str = parts[idx]
        else:
            script_path_str = parts[0]

        if not script_path_str:
            return None

        # Resolve path
        try:
            script_path = Path(os.path.expanduser(script_path_str))
        except Exception:
            return None

        if not script_path.is_absolute():
            # We don't know the base dir for relative paths easily here
            return None

        if not script_path.exists():
            return f"Script not found: {script_path}"

        return None
    except Exception as e:
        # Unexpected errors should be logged or reported if they impact usability
        return f"Validation error: {e}"


# Set up file logging for debugging hook installation
_log_path = Path.home() / ".claude" / "hook_install.log"
_log_path.parent.mkdir(parents=True, exist_ok=True)
_logger = logging.getLogger("hooks")
_logger.setLevel(logging.DEBUG)
_handler = logging.FileHandler(_log_path)
_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
_logger.addHandler(_handler)


# Hook event types supported by Claude Code
HOOK_EVENTS = [
    "PreToolUse",
    "PostToolUse",
    "Notification",
    "Stop",
    "SubagentStop",
    "UserPromptSubmit",
    "SessionStart",
    "SessionEnd",
    "PreCompact",
]

# Hook names that cannot be installed together.
MUTUALLY_EXCLUSIVE_HOOK_GROUPS: List[set[str]] = [
    # Add mutually exclusive hook groups here if needed
]


@dataclass
class HookDefinition:
    """Definition of a hook."""

    name: str
    description: str
    event: str
    command: str
    source_path: Optional[Path] = None
    matcher: str = ""
    is_installed: bool = False


@dataclass
class InstalledHook:
    """Represents an installed hook in settings.json."""

    event: str
    matcher: str
    command: str
    hook_type: str = "command"


def get_settings_path() -> Path:
    """Get path to cortex settings.json."""
    claude_dir = _resolve_claude_dir()
    return claude_dir / "settings.json"


def get_settings_scope(settings_path: Path) -> str:
    """Infer scope (project/parent/global/custom) for a settings.json path."""
    for claude_dir in find_claude_directories(Path.cwd()):
        if settings_path.parent.resolve() == claude_dir.path.resolve():
            return claude_dir.scope
    # If not in discovered dirs, mark as custom/override
    return "custom"


def detect_settings_files() -> List[Tuple[Path, str]]:
    """List discovered settings.json files with their scope.

    Returns:
        List of (path, scope) tuples ordered by specificity (project → parent → global).
    """
    results: List[Tuple[Path, str]] = []
    for claude_dir in find_claude_directories(Path.cwd()):
        settings_path = claude_dir.path / "settings.json"
        if settings_path.exists():
            results.append((settings_path, claude_dir.scope))
    return results


def load_settings() -> Dict[str, Any]:
    """Load settings.json."""
    settings_path = get_settings_path()
    if not settings_path.exists():
        return {}
    try:
        data = json.loads(settings_path.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {}
    except (json.JSONDecodeError, OSError):
        return {}


def save_settings(settings: Dict[str, Any]) -> Tuple[bool, str]:
    """Save settings.json."""
    settings_path = get_settings_path()
    try:
        settings_path.parent.mkdir(parents=True, exist_ok=True)
        settings_path.write_text(
            json.dumps(settings, indent=2) + "\n", encoding="utf-8"
        )
        return True, "Settings saved"
    except OSError as e:
        return False, f"Failed to save settings: {e}"


def configure_statusline(
    command: str = "cortex statusline --color",
    force: bool = False,
) -> Tuple[int, str]:
    """Configure Claude Code statusline in settings.json.

    Args:
        command: The statusline command to use (default: "cortex statusline --color")
        force: Overwrite existing statusline configuration

    Returns:
        (exit_code, message) tuple
    """
    settings = load_settings()

    # Check if statusline already configured
    if "statusLine" in settings and not force:
        current = settings["statusLine"]
        return 0, f"Statusline already configured: {current}\nUse --force to overwrite"

    # Set the statusline
    settings["statusLine"] = command
    success, msg = save_settings(settings)

    if success:
        settings_path = get_settings_path()
        return 0, f"✓ Configured statusline in {settings_path}\n  Command: {command}"
    else:
        return 1, f"Failed to configure statusline: {msg}"


def _hook_name_matches(hook_name: str, command: str, args: Optional[List[Any]] = None) -> bool:
    """Check whether a hook entry references a hook name."""
    tokens: List[str] = []
    if command:
        tokens.extend(command.split())
    if args:
        tokens.extend(str(arg) for arg in args if arg is not None)

    for token in tokens:
        if token == hook_name:
            return True
        try:
            stem = Path(token).stem
        except OSError:
            stem = ""
        if stem == hook_name:
            return True
        if hook_name in token:
            return True
    return False


def _is_hook_installed(settings: Dict[str, Any], hook_name: str) -> bool:
    """Check if a hook name already exists in settings.json hooks config."""
    hooks_config = settings.get("hooks", {})
    if not isinstance(hooks_config, dict):
        return False
    for matchers in hooks_config.values():
        if not isinstance(matchers, list):
            continue
        for matcher_entry in matchers:
            for hook in matcher_entry.get("hooks", []):
                if _hook_name_matches(
                    hook_name,
                    hook.get("command", ""),
                    hook.get("args", []),
                ):
                    return True
    return False


def _find_hook_conflict(settings: Dict[str, Any], hook_name: str) -> Optional[str]:
    """Return conflicting hook name if hook_name is mutually exclusive with installed hooks."""
    for group in MUTUALLY_EXCLUSIVE_HOOK_GROUPS:
        if hook_name in group:
            for other in group:
                if other != hook_name and _is_hook_installed(settings, other):
                    return other
    return None


def validate_hooks_config(config: Dict[str, Any]) -> Tuple[bool, List[str]]:
    """Validate hook config dict for mutual exclusivity and structure."""
    errors: List[str] = []
    hooks_config = config.get("hooks") if "hooks" in config else config
    if not isinstance(hooks_config, dict):
        return False, ["Hooks config must be a JSON object with a 'hooks' map."]

    present: Dict[str, bool] = {}
    for event, matchers in hooks_config.items():
        if not isinstance(matchers, list):
            continue
        for matcher_entry in matchers:
            if not isinstance(matcher_entry, dict):
                continue
            for hook in matcher_entry.get("hooks", []):
                if not isinstance(hook, dict):
                    continue
                command = hook.get("command", "")
                args = hook.get("args", [])

                # Check executable exists if absolute path is used
                error = _check_hook_executable(command)
                if error:
                    errors.append(f"Hook '{event}': {error}")

                for group in MUTUALLY_EXCLUSIVE_HOOK_GROUPS:
                    for name in group:
                        if _hook_name_matches(name, command, args):
                            present[name] = True

    for group in MUTUALLY_EXCLUSIVE_HOOK_GROUPS:
        found = [name for name in group if present.get(name)]
        if len(found) > 1:
            errors.append(
                "Hooks config contains mutually exclusive hooks: "
                + ", ".join(sorted(found))
                + ". Remove all but one."
            )

    return (len(errors) == 0), errors


def validate_hooks_config_file(path: Path) -> Tuple[bool, List[str]]:
    """Validate a hooks.json file for mutual exclusivity."""
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        return False, [f"Invalid JSON in {path}: {exc}"]
    if not isinstance(data, dict):
        return False, [f"Hooks config {path} must be a JSON object."]
    return validate_hooks_config(data)


def get_installed_hooks() -> List[InstalledHook]:
    """Get list of installed hooks from all available settings.json files.

    In project-scoped setups, checks both project and global settings to find
    all installed hooks. This allows hooks defined in ~/.claude/settings.json
    to be detected even when working in a project-specific .claude directory.
    """
    installed: List[InstalledHook] = []
    seen: set[tuple[str, str, str]] = set()  # Track (event, command) to avoid duplicates

    # Get all available settings files (project → parent → global)
    settings_files = detect_settings_files()

    for settings_path, scope in settings_files:
        try:
            data = json.loads(settings_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        if not isinstance(data, dict):
            continue

        hooks_config = data.get("hooks", {})
        if not isinstance(hooks_config, dict):
            continue

        for event, matchers in hooks_config.items():
            if not isinstance(matchers, list):
                continue
            for matcher_entry in matchers:
                matcher = matcher_entry.get("matcher", "")
                for hook in matcher_entry.get("hooks", []):
                    command = hook.get("command", "")
                    hook_type = hook.get("type", "command")
                    # Avoid duplicates when same hook is in multiple settings files
                    key = (event, command, hook_type)
                    if key not in seen:
                        seen.add(key)
                        installed.append(
                            InstalledHook(
                                event=event,
                                matcher=matcher,
                                command=command,
                                hook_type=hook_type,
                            )
                        )

    return installed


def get_available_hooks(plugin_dir: Optional[Path] = None) -> List[HookDefinition]:
    """Get available hooks from hooks/ (and hooks/examples as fallback).

    Args:
        plugin_dir: Plugin directory. If None, uses package location.

    Returns:
        List of available hook definitions.
    """
    hooks: List[HookDefinition] = []

    if plugin_dir is None:
        # Try to find from package location
        import claude_ctx_py

        pkg_dir = Path(claude_ctx_py.__file__).parent.parent
        base_hooks_dir = pkg_dir / "hooks"
    else:
        base_hooks_dir = plugin_dir / "hooks"

    candidate_dirs = [base_hooks_dir, base_hooks_dir / "examples"]
    seen: set[str] = set()

    # Scan for both Python and shell scripts
    patterns = ["*.py", "*.sh"]

    for hooks_dir in candidate_dirs:
        if not hooks_dir.is_dir():
            continue
        for pattern in patterns:
            for hook_file in sorted(hooks_dir.glob(pattern)):
                if hook_file.stem in seen:
                    continue
                hook_def = parse_hook_file(hook_file)
                if hook_def:
                    hooks.append(hook_def)
                    seen.add(hook_file.stem)

    # Also check user's hooks directory
    claude_dir = _resolve_claude_dir()
    user_hooks_dir = claude_dir / "hooks"
    if user_hooks_dir.is_dir():
        for pattern in patterns:
            for hook_file in sorted(user_hooks_dir.glob(pattern)):
                # Skip if already in examples
                if any(h.name == hook_file.stem for h in hooks):
                    continue
                hook_def = parse_hook_file(hook_file)
                if hook_def:
                    hook_def.is_installed = True
                    hooks.append(hook_def)

    # Mark installed hooks by matching hook name in command
    installed = get_installed_hooks()
    for hook in hooks:
        # Check if this hook name appears in any installed command
        # E.g., "skill_auto_suggester" in "python3 /path/to/skill_auto_suggester.py"
        for installed_hook in installed:
            if hook.name in installed_hook.command:
                hook.is_installed = True
                break

    return hooks


def parse_hook_file(hook_file: Path) -> Optional[HookDefinition]:
    """Parse a hook file to extract metadata.

    For Python files: Expects docstring at top with first line as description.
    For shell scripts: Expects # comments at top with first non-shebang line as description.
    """
    try:
        content = hook_file.read_text(encoding="utf-8")
    except OSError:
        return None

    suffix = hook_file.suffix.lower()
    
    if suffix == ".sh":
        return _parse_shell_hook(hook_file, content)
    else:
        return _parse_python_hook(hook_file, content)


def _parse_shell_hook(hook_file: Path, content: str) -> Optional[HookDefinition]:
    """Parse a shell script hook file."""
    lines = content.split("\n")
    comment_lines: List[str] = []
    
    for line in lines:
        stripped = line.strip()
        # Skip shebang
        if stripped.startswith("#!"):
            continue
        # Collect comment lines
        if stripped.startswith("#"):
            comment_lines.append(stripped[1:].strip())
        elif stripped:  # Stop at first non-comment, non-empty line
            break
    
    # First comment line is description
    description = comment_lines[0] if comment_lines else "No description"
    full_comments = "\n".join(comment_lines).lower()
    
    # Detect event type from comments
    event = _detect_event_from_text(full_comments)
    
    return HookDefinition(
        name=hook_file.stem,
        description=description,
        event=event,
        command=f"bash {hook_file}",
        source_path=hook_file,
    )


def _parse_python_hook(hook_file: Path, content: str) -> Optional[HookDefinition]:
    """Parse a Python hook file."""
    lines = content.split("\n")
    in_docstring = False
    docstring_lines: List[str] = []

    for line in lines:
        if '"""' in line:
            if not in_docstring:
                in_docstring = True
                # Handle single-line docstring
                if line.count('"""') >= 2:
                    start = line.index('"""') + 3
                    end = line.rindex('"""')
                    docstring_lines.append(line[start:end])
                    break
            else:
                break
        elif in_docstring:
            docstring_lines.append(line)

    if not docstring_lines:
        return HookDefinition(
            name=hook_file.stem,
            description="No description",
            event="UserPromptSubmit",
            command=f"python3 {hook_file}",
            source_path=hook_file,
        )

    description = docstring_lines[0].strip() if docstring_lines else "No description"
    full_docstring = "\n".join(docstring_lines).lower()
    event = _detect_event_from_text(full_docstring)

    return HookDefinition(
        name=hook_file.stem,
        description=description,
        event=event,
        command=f"python3 {hook_file}",
        source_path=hook_file,
    )


def _detect_event_from_text(text: str) -> str:
    """Detect hook event type from description text."""
    text = text.lower()
    
    if "sessionend" in text or "session-end" in text or "session end" in text:
        return "SessionEnd"
    if "sessionstart" in text or "session-start" in text or "session start" in text:
        return "SessionStart"
    if "precompact" in text or "pre-compact" in text or "pre compact" in text:
        return "PreCompact"
    if "stop" in text and "subagent" not in text:
        return "Stop"
    if "pretooluse" in text or "pre-tool" in text or "pre tool" in text:
        return "PreToolUse"
    if "posttooluse" in text or "post-tool" in text or "post tool" in text:
        return "PostToolUse"
    if "notification" in text:
        return "Notification"
    if "subagent" in text:
        return "SubagentStop"
    if "userpromptsubmit" in text or "user prompt" in text or "userprompt" in text:
        return "UserPromptSubmit"
    
    return "UserPromptSubmit"  # Default


def install_hook(
    hook: HookDefinition,
    target_dir: Optional[Path] = None,
) -> Tuple[bool, str]:
    """Install a hook by symlinking to ~/.claude/hooks/ and registering in settings.

    Args:
        hook: Hook definition to install
        target_dir: Target directory for hooks. Defaults to ~/.claude/hooks/

    Returns:
        Tuple of (success, message)
    """
    _logger.info(f"install_hook called: hook.name={hook.name}, hook.event={hook.event}, source_path={hook.source_path}")

    try:
        if not hook.source_path or not hook.source_path.exists():
            _logger.error(f"No valid source_path for hook: {hook.source_path}")
            return False, f"Hook source file not found: {hook.source_path}"

        claude_dir = _resolve_claude_dir()
        hooks_dir = target_dir or (claude_dir / "hooks")
        _logger.debug(f"claude_dir={claude_dir}, hooks_dir={hooks_dir}")
        hooks_dir.mkdir(parents=True, exist_ok=True)

        # Determine interpreter from file extension
        suffix = hook.source_path.suffix
        interpreter = "bash" if suffix == ".sh" else "python3"
        _logger.debug(f"suffix={suffix}, interpreter={interpreter}")

        # Create symlink in hooks directory
        installed_path = hooks_dir / hook.source_path.name
        _logger.debug(f"installed_path={installed_path}")

        # Check if source and destination are the same file (e.g., already linked)
        if installed_path.exists() and hook.source_path.resolve() == installed_path.resolve():
            _logger.debug(f"Source and destination are the same file, skipping symlink creation")
        else:
            # Remove existing file or symlink if present
            if installed_path.exists() or installed_path.is_symlink():
                _logger.debug(f"Removing existing file/symlink at {installed_path}")
                installed_path.unlink()

            _logger.debug(f"Creating symlink from {installed_path} to {hook.source_path}")
            installed_path.symlink_to(hook.source_path)
            _logger.debug("Symlink created successfully")

        # Update settings.json
        settings = load_settings()
        _logger.debug(f"Loaded settings, hooks config: {settings.get('hooks', {}).keys()}")

        conflict = _find_hook_conflict(settings, hook.name)
        if conflict:
            _logger.error(f"Hook conflict found: {conflict}")
            return (
                False,
                f"Hook '{hook.name}' conflicts with '{conflict}'. Uninstall '{conflict}' first.",
            )
        if "hooks" not in settings:
            settings["hooks"] = {}

        if hook.event not in settings["hooks"]:
            settings["hooks"][hook.event] = []

        # Register hook command with symlink path
        command = f"{interpreter} {installed_path}"
        _logger.debug(f"Checking if command already installed: {command}")

        for matcher_entry in settings["hooks"][hook.event]:
            for existing_hook in matcher_entry.get("hooks", []):
                if existing_hook.get("command") == command:
                    _logger.info(f"Hook already installed: {hook.name}")
                    return True, f"Hook {hook.name} already installed"

        # Add new hook
        _logger.info(f"Adding new hook entry for {hook.name} on event {hook.event}")
        settings["hooks"][hook.event].append(
            {
                "matcher": hook.matcher,
                "hooks": [{"type": "command", "command": command}],
            }
        )
        _logger.debug("Saving settings...")
        success, msg = save_settings(settings)
        if success:
            _logger.info(f"Successfully installed hook: {hook.name}")
            return True, f"Installed hook: {hook.name}"
        _logger.error(f"Failed to save settings: {msg}")
        return False, msg
    except Exception as e:
        _logger.exception(f"Unexpected error in install_hook: {e}")
        return False, f"Unexpected error: {e}"


def uninstall_hook(hook: HookDefinition) -> Tuple[bool, str]:
    """Uninstall a hook by removing from settings.

    Args:
        hook: Hook to uninstall

    Returns:
        Tuple of (success, message)
    """
    settings = load_settings()
    if "hooks" not in settings or hook.event not in settings["hooks"]:
        return False, f"Hook {hook.name} not found"

    # Find and remove the hook
    modified = False
    new_matchers = []
    for matcher_entry in settings["hooks"][hook.event]:
        new_hooks = []
        for existing_hook in matcher_entry.get("hooks", []):
            cmd = existing_hook.get("command", "")
            if hook.name not in cmd:
                new_hooks.append(existing_hook)
            else:
                modified = True

        if new_hooks:
            matcher_entry["hooks"] = new_hooks
            new_matchers.append(matcher_entry)

    if modified:
        settings["hooks"][hook.event] = new_matchers
        success, msg = save_settings(settings)
        if success:
            return True, f"Uninstalled hook: {hook.name}"
        return False, msg

    return False, f"Hook {hook.name} not found in settings"


def get_hook_events() -> List[str]:
    """Get list of supported hook events."""
    return HOOK_EVENTS.copy()


def create_hook_template(name: str, event: str, target_dir: Optional[Path] = None) -> Tuple[bool, str, Optional[Path]]:
    """Create a new hook from template.

    Args:
        name: Hook name (without .py extension)
        event: Hook event type
        target_dir: Target directory. Defaults to ~/.claude/hooks/

    Returns:
        Tuple of (success, message, path)
    """
    claude_dir = _resolve_claude_dir()
    hooks_dir = target_dir or (claude_dir / "hooks")
    hooks_dir.mkdir(parents=True, exist_ok=True)

    hook_path = hooks_dir / f"{name}.py"
    if hook_path.exists():
        # File exists - return the path so caller can still register it in settings
        return True, f"Hook file already exists at {hook_path}", hook_path

    template = f'''#!/usr/bin/env python3
"""Custom hook: {name}

This hook is triggered on {event} events.

Usage:
    Register this hook in ~/.claude/settings.json:

    {{
      "hooks": {{
        "{event}": [
          {{
            "matcher": "",
            "hooks": [
              {{
                "type": "command",
                "command": "python3 ~/.claude/hooks/{name}.py"
              }}
            ]
          }}
        ]
      }}
    }}

Environment Variables:
    CLAUDE_HOOK_PROMPT: The user's prompt (for UserPromptSubmit)
    CLAUDE_SESSION_CONTEXT: Session context (for Stop)
    CLAUDE_CHANGED_FILES: Colon-separated list of changed files
"""

from __future__ import annotations

import os
import sys


def main() -> int:
    """Main entry point for the hook."""
    # Get environment variables
    prompt = os.getenv("CLAUDE_HOOK_PROMPT", "")
    context = os.getenv("CLAUDE_SESSION_CONTEXT", "")

    # Add your hook logic here
    print(f"Hook {name} triggered!")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
'''

    try:
        hook_path.write_text(template, encoding="utf-8")
        hook_path.chmod(0o755)
        return True, f"Created hook template: {hook_path}", hook_path
    except OSError as e:
        return False, f"Failed to create hook: {e}", None
