"""
Symlink creation and validation operations.

This module provides symlink functionality for editable mode with security
validation and cross-platform support (Windows, macOS, Linux).

Key Functions:
    - create_symlink_with_fallback: Create symlinks with relative/absolute options
    - validate_source_path: Prevent symlink escape attacks
    - should_use_symlink_for_file: Determine if file should be symlinked vs copied

Usage Example:
    >>> from pathlib import Path
    >>> from persuade.symlink_ops import create_symlink_with_fallback, validate_source_path
    >>>
    >>> # Create relative symlink (recommended for portability)
    >>> source = Path("/repo/.claude/scripts/sync.py")
    >>> target = Path("/project/.claude/scripts/sync.py")
    >>> source_root = Path("/repo")
    >>> create_symlink_with_fallback(source, target, source_root, relative=True)
    >>>
    >>> # Validate source path is within repository bounds (security)
    >>> validate_source_path(source, source_root)  # Passes
    >>>
    >>> # This would raise ValueError (security violation):
    >>> # validate_source_path(Path("/etc/passwd"), source_root)

Security:
    All symlink operations validate that source paths remain within the source
    repository bounds to prevent symlink escape attacks that could expose
    sensitive files.

Cross-Platform Notes:
    - Windows: Requires Developer Mode for symlink creation
    - macOS/Linux: Works with standard permissions
    - Relative symlinks preferred for portability (but may fail cross-drive on Windows)
"""

import os
import shutil
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from persuade.categorization import FileInfo

from persuade.constants import GITHUB_WORKFLOWS_PATH
from persuade.logging import get_logger

__all__ = [
    "create_symlink_with_fallback",
    "validate_source_path",
    "should_use_symlink_for_file",
]

logger = get_logger()


def validate_source_path(source: Path, source_root: Path) -> None:
    """
    Validate that source path is within source repository bounds.

    This prevents security issues where a malicious source repository could
    contain symlinks pointing outside the repository, potentially exposing
    sensitive files in the target system.

    Args:
        source: Source file path to validate
        source_root: Root of source repository

    Raises:
        ValueError: If source path escapes source repository bounds
    """
    try:
        # Resolve both paths to handle symlinks in the source
        resolved_source = source.resolve()
        resolved_root = source_root.resolve()

        # Check if source is within source_root
        resolved_source.relative_to(resolved_root)
    except ValueError as e:
        raise ValueError(
            f"Security violation: Source path {source} escapes source repository {source_root}. "
            f"This could expose sensitive files and is not allowed."
        ) from e


def create_symlink_with_fallback(
    source: Path, target: Path, source_root: Path, relative: bool = True
) -> None:
    """
    Create symlink from target to source, with cross-platform handling.

    Cross-Platform Notes:
        - Windows: Requires Developer Mode. Directory symlinks need target_is_directory=True
        - macOS/Linux: Works with standard permissions
        - Relative symlinks preferred but may fail cross-drive on Windows (falls back to absolute)

    Args:
        source: Source file path (what the symlink points to)
        target: Target symlink path (where to create the symlink)
        relative: If True, create relative symlink; if False, create absolute
        source_root: Root of source repository (required for security validation)

    Raises:
        FileNotFoundError: If source does not exist
        PermissionError: If insufficient permissions (common on Windows)
        ValueError: If source escapes source repository bounds (security)
        OSError: If symlink creation fails
    """
    # Validate source exists
    if not source.exists():
        raise FileNotFoundError(f"Cannot create symlink to non-existent source: {source}")

    # Security: Validate source is within source repository bounds (always required)
    validate_source_path(source, source_root)

    # If source is itself a symlink, resolve it to avoid symlink chains
    if source.is_symlink():
        resolved = source.resolve()
        logger.info("resolved_symlink_source", original=str(source), resolved=str(resolved))
        source = resolved

    # Create parent directory if needed
    target.parent.mkdir(parents=True, exist_ok=True)

    # Remove existing file/directory/symlink at target
    # NOTE: TOCTOU race condition exists between exists() and unlink().
    # Mitigation: try/except below handles race gracefully (PermissionError, OSError).
    # Acceptable for single-user dev tool (not high-concurrency server).
    # Use is_symlink() to also catch broken symlinks (exists() returns False for broken symlinks)
    if target.exists() or target.is_symlink():
        try:
            if target.is_dir() and not target.is_symlink():
                # Target is a real directory (not a directory symlink) - remove it
                shutil.rmtree(target)
            else:
                # Target is a file or symlink (including broken symlinks) - unlink it
                target.unlink()
        except PermissionError as e:
            raise PermissionError(
                f"Cannot replace existing target {target}: Permission denied. "
                f"Ensure the file/directory is not in use and you have write permissions."
            ) from e
        except OSError as e:
            raise OSError(f"Cannot remove existing target {target}: {e}") from e

    # Determine symlink target
    if relative:
        # Calculate relative path from target to source
        try:
            symlink_target = os.path.relpath(source, target.parent)
        except ValueError:
            # Can't create relative path (e.g., different drives on Windows)
            logger.warning(
                "cannot_create_relative_symlink",
                source=str(source),
                target=str(target),
                fallback="using_absolute",
            )
            symlink_target = str(source.resolve())
    else:
        symlink_target = str(source.resolve())

    # Create symlink
    try:
        # On Windows, directory symlinks need target_is_directory=True
        is_dir = source.is_dir()
        os.symlink(symlink_target, target, target_is_directory=is_dir)
        logger.info(
            "symlink_created",
            source=str(source),
            target=str(target),
            relative=relative,
        )
    except PermissionError as e:
        # More specific for Windows permission issues
        logger.error(
            "symlink_permission_denied",
            source=str(source),
            target=str(target),
            error=str(e),
        )
        raise PermissionError(
            f"Insufficient permissions to create symlink on Windows.\n\n"
            f"To enable symlinks, follow these steps:\n"
            f"1. Open Settings → Update & Security → For Developers\n"
            f"2. Enable 'Developer Mode'\n"
            f"3. Restart your terminal\n\n"
            f"See: https://learn.microsoft.com/windows/dev-environment\n\n"
            f"Original error: {e}"
        ) from e
    except FileExistsError as e:
        # Should not happen after unlink, but be defensive
        logger.error(
            "symlink_target_exists",
            source=str(source),
            target=str(target),
            error=str(e),
        )
        raise OSError(f"Target already exists: {target}") from e
    except OSError as e:
        # Generic symlink creation failure
        logger.error(
            "symlink_creation_failed",
            source=str(source),
            target=str(target),
            error=str(e),
        )
        raise OSError(f"Failed to create symlink: {e}") from e


def should_use_symlink_for_file(
    use_symlink: bool, file_info: "FileInfo | None", target: Path
) -> bool:
    """
    Determine if file should be symlinked based on category and destination.

    Args:
        use_symlink: Whether symlinking is requested
        file_info: File information with category and destination (None if not categorized)
        target: Target file path (for logging)

    Returns:
        True if file should be symlinked, False if it should be copied
    """
    if not use_symlink or not file_info:
        return False

    # Check if this file category can be symlinked
    if not file_info.category.can_symlink():
        logger.debug(
            "symlink_skipped_for_category",
            path=str(target),
            category=file_info.category.value,
            reason="category_cannot_symlink",
        )
        return False

    # Never symlink files in .github/workflows/ (GitHub Actions doesn't follow symlinks)
    if file_info.destination and GITHUB_WORKFLOWS_PATH in file_info.destination:
        logger.debug(
            "symlink_skipped_for_destination",
            path=str(target),
            destination=file_info.destination,
            reason="github_workflows_no_symlink",
        )
        return False

    return True
