"""Git hook installation and management for MergeGuide.

This module provides functionality to install, uninstall, and manage
MergeGuide git hooks in a repository. Git hooks enable automatic policy
validation at key points in the git workflow (pre-commit, pre-push).

Supported Hooks:
    - pre-commit: Validates staged files before commit
    - pre-push: Validates files in commits being pushed

The hooks integrate with the `mergeguide` CLI to run policy checks. They
can be bypassed using git's --no-verify flag, though bypass events may
be logged for audit purposes.

Example Usage:
    # Install hooks (typically via CLI)
    installer = HookInstaller()
    installed = installer.install("all")  # Installs pre-commit and pre-push

    # Check hook status
    status = installer.status()
    # {'pre-commit': 'installed', 'pre-push': 'not_installed'}

    # Uninstall hooks
    uninstalled = installer.uninstall("pre-commit")

Module Constants:
    HookType: Type alias for valid hook type strings.
    ALL_HOOK_TYPES: List of all supported hook types.
    MERGEGUIDE_MARKER: Comment marker used to identify MergeGuide-installed hooks.
    PRE_COMMIT_TEMPLATE: Bash script template for pre-commit hook.
    PRE_PUSH_TEMPLATE: Bash script template for pre-push hook.
"""

from __future__ import annotations

import stat
from pathlib import Path
from typing import Literal

from mergeguide.hooks.git_utils import get_git_hooks_dir, is_git_repo

# Type alias for valid hook types
HookType = Literal["pre-commit", "pre-push"]

# List of all supported hook types
ALL_HOOK_TYPES: list[HookType] = ["pre-commit", "pre-push"]

# Marker comment used to identify hooks installed by MergeGuide.
# This enables safe uninstallation without removing user-created hooks.
MERGEGUIDE_MARKER = "# MergeGuide git hook - installed by: mergeguide hooks install"

# Bash script template for the pre-commit hook.
# This script runs before each commit and validates staged files.
# Key behaviors:
# - Only checks files that are staged (Added, Copied, Modified)
# - Gracefully handles missing mergeguide command
# - Provides clear instructions for bypassing when needed
PRE_COMMIT_TEMPLATE = '''#!/bin/bash
# MergeGuide git hook - installed by: mergeguide hooks install
# Pre-commit: Validates staged files against policies
#
# Bypass with: git commit --no-verify
# (Bypass events may be logged for audit)

set -e

# Get staged files (Added, Copied, Modified only)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null)

if [ -z "$STAGED_FILES" ]; then
    exit 0
fi

# Check if mergeguide is available
if ! command -v mergeguide &> /dev/null; then
    echo "Warning: mergeguide command not found, skipping policy check"
    echo "Install with: pip install mergeguide"
    exit 0
fi

# Run mergeguide check with --staged flag
if mergeguide check --staged --hook-mode; then
    exit 0
else
    echo ""
    echo "Fix violations above, or use 'git commit --no-verify' to bypass."
    exit 1
fi
'''

# Bash script template for the pre-push hook.
# This script runs before each push and validates files in the commits.
# Key behaviors:
# - Reads push info from stdin (git provides local/remote refs)
# - Handles new branches (remote SHA is all zeros)
# - Handles branch deletion gracefully (local SHA is all zeros)
# - Determines correct file range based on push context
PRE_PUSH_TEMPLATE = '''#!/bin/bash
# MergeGuide git hook - installed by: mergeguide hooks install
# Pre-push: Validates files in commits being pushed
#
# Bypass with: git push --no-verify
# (Bypass events may be logged for audit)

set -e

# Check if mergeguide is available
if ! command -v mergeguide &> /dev/null; then
    echo "Warning: mergeguide command not found, skipping policy check"
    exit 0
fi

# Read push info from stdin
# Format: <local_ref> <local_sha> <remote_ref> <remote_sha>
while read local_ref local_sha remote_ref remote_sha; do
    # Skip branch deletion
    if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
        continue
    fi

    # Determine range for changed files
    if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
        # New branch - check all files in the commit(s)
        CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r "$local_sha" 2>/dev/null | tr '\\n' ' ')
    else
        # Existing branch - check files changed since remote
        CHANGED_FILES=$(git diff --name-only "$remote_sha..$local_sha" 2>/dev/null | tr '\\n' ' ')
    fi

    if [ -z "$CHANGED_FILES" ]; then
        continue
    fi

    echo "MergeGuide: Checking files in push..."

    # shellcheck disable=SC2086
    if ! mergeguide check --hook-mode $CHANGED_FILES; then
        echo ""
        echo "Fix violations above, or use 'git push --no-verify' to bypass."
        exit 1
    fi
done

exit 0
'''


class HookExistsError(Exception):
    """Exception raised when attempting to install over an existing non-MergeGuide hook.

    This exception prevents accidental overwriting of custom hooks that may
    contain important logic. Use force=True in install() to override.

    Attributes:
        message: Description of the conflict including the hook type.
    """

    pass


class NotInRepoError(Exception):
    """Exception raised when a git operation is attempted outside a git repository.

    This is raised when trying to install/uninstall hooks when the current
    directory is not within a git repository.
    """

    pass


class HookInstaller:
    """Install and manage MergeGuide git hooks in a repository.

    This class handles the lifecycle of MergeGuide git hooks including:
    - Installation of pre-commit and pre-push hooks
    - Safe uninstallation (only removes MergeGuide-installed hooks)
    - Status checking for existing hooks

    The installer detects whether existing hooks were installed by MergeGuide
    using a marker comment, ensuring it never accidentally removes custom
    hooks created by the user or other tools.

    Example:
        >>> installer = HookInstaller()
        >>> installer.install("all")
        ['pre-commit', 'pre-push']
        >>> installer.status()
        {'pre-commit': 'installed', 'pre-push': 'installed'}
        >>> installer.uninstall("pre-commit")
        ['pre-commit']

    Attributes:
        hooks_dir: Path to the git hooks directory (.git/hooks).
    """

    def __init__(self, hooks_dir: Path | None = None):
        """Initialize the hook installer.

        Args:
            hooks_dir: Explicit path to the hooks directory. If None,
                the directory is auto-detected from the current git
                repository. Useful for testing or non-standard repo layouts.
        """
        self._hooks_dir = hooks_dir

    @property
    def hooks_dir(self) -> Path:
        """Get the hooks directory path, auto-detecting if needed.

        Lazily determines the hooks directory on first access. If not
        explicitly provided during initialization, queries git to find
        the correct .git/hooks directory.

        Returns:
            Path to the git hooks directory.

        Raises:
            NotInRepoError: If not in a git repository and no explicit
                hooks_dir was provided.
        """
        if self._hooks_dir is None:
            if not is_git_repo():
                raise NotInRepoError("Not in a git repository")
            self._hooks_dir = get_git_hooks_dir()
        return self._hooks_dir

    def install(
        self,
        hook_type: HookType | Literal["all"] = "all",
        force: bool = False,
    ) -> list[str]:
        """Install MergeGuide git hook(s) in the repository.

        Writes hook scripts to the .git/hooks directory and makes them
        executable. By default, refuses to overwrite existing hooks that
        were not installed by MergeGuide (to prevent data loss).

        Args:
            hook_type: Which hook(s) to install. Options:
                - "pre-commit": Install only pre-commit hook
                - "pre-push": Install only pre-push hook
                - "all": Install both hooks (default)
            force: If True, overwrite existing hooks even if they were
                not installed by MergeGuide. Use with caution. Defaults to False.

        Returns:
            List of hook names that were installed (e.g., ["pre-commit", "pre-push"]).

        Raises:
            HookExistsError: If a hook exists that was not installed by
                MergeGuide and force=False. The error message indicates which
                hook caused the conflict.
            NotInRepoError: If the current directory is not in a git repository.

        Example:
            >>> installer = HookInstaller()
            >>> installer.install("pre-commit")
            ['pre-commit']
            >>> installer.install("all", force=True)  # Overwrites existing
            ['pre-commit', 'pre-push']
        """
        hooks_to_install = ALL_HOOK_TYPES if hook_type == "all" else [hook_type]
        installed = []

        # Ensure hooks directory exists (may not exist in fresh repos)
        self.hooks_dir.mkdir(parents=True, exist_ok=True)

        for hook in hooks_to_install:
            hook_path = self.hooks_dir / hook

            # Check for existing hook - protect non-MergeGuide hooks
            if hook_path.exists() and not force:
                if not self._is_mergeguide_hook(hook_path):
                    raise HookExistsError(
                        f"Hook '{hook}' already exists and is not a MergeGuide hook. "
                        f"Use --force to overwrite."
                    )

            # Write hook script from template
            template = self._get_template(hook)
            hook_path.write_text(template)

            # Make executable (owner, group, and others)
            hook_path.chmod(
                hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
            )

            installed.append(hook)

        return installed

    def uninstall(
        self,
        hook_type: HookType | Literal["all"] = "all",
    ) -> list[str]:
        """Uninstall MergeGuide git hook(s) from the repository.

        Removes hook scripts from the .git/hooks directory. Only hooks
        that were installed by MergeGuide (identified by the marker comment)
        are removed. Non-MergeGuide hooks are left untouched.

        Args:
            hook_type: Which hook(s) to uninstall. Options:
                - "pre-commit": Uninstall only pre-commit hook
                - "pre-push": Uninstall only pre-push hook
                - "all": Uninstall both hooks (default)

        Returns:
            List of hook names that were uninstalled. Hooks that don't
            exist or are not MergeGuide hooks are not included.

        Example:
            >>> installer = HookInstaller()
            >>> installer.uninstall("pre-commit")
            ['pre-commit']
            >>> installer.uninstall("all")  # Only removes MergeGuide hooks
            ['pre-push']
        """
        hooks_to_uninstall = ALL_HOOK_TYPES if hook_type == "all" else [hook_type]
        uninstalled = []

        for hook in hooks_to_uninstall:
            hook_path = self.hooks_dir / hook

            # Only remove hooks that MergeGuide installed
            if hook_path.exists() and self._is_mergeguide_hook(hook_path):
                hook_path.unlink()
                uninstalled.append(hook)

        return uninstalled

    def status(self) -> dict[str, str]:
        """Get the installation status of all supported hooks.

        Checks each supported hook type and determines whether it is
        installed by MergeGuide, installed by another tool/user, or not
        installed at all.

        Returns:
            Dictionary mapping hook type to status string:
                - "installed": MergeGuide hook is installed and active
                - "other": A non-MergeGuide hook exists (won't be modified)
                - "not_installed": No hook exists for this type

        Example:
            >>> installer = HookInstaller()
            >>> installer.status()
            {'pre-commit': 'installed', 'pre-push': 'not_installed'}
        """
        status = {}

        for hook in ALL_HOOK_TYPES:
            hook_path = self.hooks_dir / hook

            if not hook_path.exists():
                status[hook] = "not_installed"
            elif self._is_mergeguide_hook(hook_path):
                status[hook] = "installed"
            else:
                status[hook] = "other"

        return status

    def _is_mergeguide_hook(self, path: Path) -> bool:
        """Check if a hook file was installed by MergeGuide.

        Reads the hook file and checks for the presence of the
        MERGEGUIDE_MARKER comment that is included in all MergeGuide hooks.

        Args:
            path: Path to the hook file to check.

        Returns:
            True if the hook contains the MergeGuide marker, False otherwise.
            Returns False if the file cannot be read.
        """
        try:
            content = path.read_text()
            return MERGEGUIDE_MARKER in content
        except Exception:
            return False

    def _get_template(self, hook_type: HookType) -> str:
        """Get the bash script template for a hook type.

        Args:
            hook_type: The type of hook ("pre-commit" or "pre-push").

        Returns:
            The bash script template string for the specified hook type.
        """
        templates = {
            "pre-commit": PRE_COMMIT_TEMPLATE,
            "pre-push": PRE_PUSH_TEMPLATE,
        }
        return templates[hook_type]
