"""Core injection logic for 1Password secret references."""

import json
import re
import secrets
import subprocess
from typing import Any

from scottzach1.onepassword_cli.exceptions import (
    OnePasswordNotAuthenticatedError,
    OnePasswordNotInstalledError,
    SecretInjectionError,
)

# Regex pattern to detect 1Password secret references
_OP_REFERENCE_PATTERN = re.compile(r"op://[a-zA-Z0-9/_\-]+")

# Timeout for subprocess calls (30 seconds)
_SUBPROCESS_TIMEOUT = 30


def _check_cli_available() -> None:
    """Check if the 1Password CLI is installed and available.

    Raises:
        OnePasswordNotInstalledError: If the CLI is not found or not executable
    """
    try:
        result = subprocess.run(
            ["op", "--version"],
            capture_output=True,
            text=True,
            timeout=_SUBPROCESS_TIMEOUT,
            check=False,
        )
        if result.returncode != 0:
            raise OnePasswordNotInstalledError
    except FileNotFoundError as e:
        raise OnePasswordNotInstalledError from e
    except subprocess.TimeoutExpired as e:
        raise SecretInjectionError("Timeout while checking 1Password CLI availability") from e


def _check_authenticated() -> None:
    """Check if the user is authenticated with 1Password CLI.

    Raises:
        OnePasswordNotAuthenticatedError: If the user is not signed in
    """
    try:
        result = subprocess.run(
            ["op", "account", "list"],
            capture_output=True,
            text=True,
            timeout=_SUBPROCESS_TIMEOUT,
            check=False,
        )
        if result.returncode != 0:
            raise OnePasswordNotAuthenticatedError
    except subprocess.TimeoutExpired as e:
        raise SecretInjectionError("Timeout while checking 1Password authentication") from e


def _inject_via_cli(content: str) -> str:
    """Execute the 1Password CLI injection command.

    Args:
        content: The content containing 1Password secret references

    Returns:
        The content with secrets injected

    Raises:
        SecretInjectionError: If the injection fails
    """
    try:
        result = subprocess.run(
            ["op", "inject"],
            input=content,
            capture_output=True,
            text=True,
            timeout=_SUBPROCESS_TIMEOUT,
            check=False,
        )
        if result.returncode != 0:
            error_msg = result.stderr.strip() if result.stderr else "Unknown error"
            raise SecretInjectionError(f"1Password CLI injection failed: {error_msg}")
        return result.stdout
    except subprocess.TimeoutExpired as e:
        raise SecretInjectionError("Timeout while injecting secrets via 1Password CLI") from e


def inject_string_1password(value: str, *, check_cli: bool = True) -> str:
    """Inject 1Password secrets into a string containing op:// references.

    Args:
        value: String that may contain 1Password secret references (e.g., "op://vault/item/field")
        check_cli: Whether to check CLI availability and authentication before injection

    Returns:
        String with all 1Password references replaced with actual secret values

    Raises:
        OnePasswordNotInstalledError: If the 1Password CLI is not installed
        OnePasswordNotAuthenticatedError: If not authenticated with 1Password
        SecretInjectionError: If the injection process fails

    Examples:
        >>> inject_string_1password("My password is op://vault/item/password")
        "My password is secret_value"

        >>> inject_string_1password("No secrets here")
        "No secrets here"
    """
    # Early return if no secrets detected (optimization)
    if not _OP_REFERENCE_PATTERN.search(value):
        return value

    # Check CLI health if requested
    if check_cli:
        _check_cli_available()
        _check_authenticated()

    # Inject secrets via CLI
    return _inject_via_cli(value)


def inject_dictionary_1password(
    data: dict[str, Any],
    *,
    check_cli: bool = True,
    in_place: bool = False,
) -> dict[str, Any]:
    """Inject 1Password secrets into a dictionary containing op:// references.

    This function recursively processes dictionary values, injecting secrets from
    1Password references while preserving non-string types (int, float, bool, None, lists).

    Args:
        data: Dictionary that may contain 1Password secret references in string values
        check_cli: Whether to check CLI availability and authentication before injection
        in_place: Whether to modify the dictionary in place or return a new copy

    Returns:
        Dictionary with all 1Password references replaced with actual secret values

    Raises:
        OnePasswordNotInstalledError: If the 1Password CLI is not installed
        OnePasswordNotAuthenticatedError: If not authenticated with 1Password
        SecretInjectionError: If the injection process fails

    Examples:
        >>> data = {"password": "op://vault/item/password", "port": 5432}
        >>> inject_dictionary_1password(data)
        {"password": "secret_value", "port": 5432}

        >>> data = {"nested": {"secret": "op://vault/item/field"}}
        >>> inject_dictionary_1password(data)
        {"nested": {"secret": "secret_value"}}
    """
    # Check CLI health once before processing
    if check_cli:
        _check_cli_available()
        _check_authenticated()

    # Generate a unique high-entropy marker for JSON serialization
    marker = secrets.token_hex(16)

    # Convert dictionary to JSON with markers for secret references
    def mark_secrets(obj: Any) -> Any:
        """Recursively mark secret references in the data structure."""
        if isinstance(obj, dict):
            return {k: mark_secrets(v) for k, v in obj.items()}
        if isinstance(obj, list):
            return [mark_secrets(item) for item in obj]
        if isinstance(obj, str) and _OP_REFERENCE_PATTERN.search(obj):
            # Mark string as containing secrets for injection
            return f"{marker}{obj}{marker}"
        return obj

    # Mark all secrets in the data structure
    marked_data = mark_secrets(data if not in_place else data.copy())

    # Serialize to JSON
    json_content = json.dumps(marked_data)

    # Only inject if there are secrets to process
    if marker in json_content:
        # Remove markers and inject secrets
        json_content = json_content.replace(f'"{marker}', '"').replace(f'{marker}"', '"')
        injected_content = _inject_via_cli(json_content)

        # Parse the result back to dictionary
        result = json.loads(injected_content)
    else:
        # No secrets to inject, return original or copy
        result = data if in_place else marked_data

    # Update in place if requested
    if in_place:
        data.clear()
        data.update(result)
        return data

    return result
