"""Structured debug snapshots for LLM-assisted debugging."""

from __future__ import annotations

import contextlib
import functools
import sys
from collections.abc import Callable, Iterable
from re import Pattern
from typing import Any

from .capture import capture_exception
from .config import DetailLevel, LocalsMode, OutputFormat, SnapshotConfig, SourceMode
from .detail_filter import filter_snapshot
from .hooks import PII_PATTERNS, install_hooks, uninstall_hooks
from .hypotheses import Hypothesis, generate_hypotheses
from .log_capture import install_log_handler as enable_log_capture
from .middleware import LLMDebugASGIMiddleware, LLMDebugWSGIMiddleware
from .output import get_latest_snapshot
from .redaction_policy import (
    RedactionProfile,
    normalize_redaction_profile,
    resolve_redaction_policy,
)

__version__ = "2.8.0"
__all__ = [
    "PII_PATTERNS",
    "DetailLevel",
    "Hypothesis",
    "LLMDebugASGIMiddleware",
    "LLMDebugWSGIMiddleware",
    "RedactionProfile",
    "SnapshotConfig",
    "debug_snapshot",
    "enable_log_capture",
    "filter_snapshot",
    "generate_hypotheses",
    "get_latest_snapshot",
    "install_hooks",
    "load_ipython_extension",
    "load_jupyter",
    "resolve_redaction_policy",
    "snapshot_section",
    "uninstall_hooks",
    "unload_ipython_extension",
]


def _build_config(
    *,
    out_dir: str = ".llmdebug",
    frames: int = 5,
    source_context: int = 3,
    source_mode: SourceMode = "all",
    locals_mode: LocalsMode = "safe",
    max_str: int = 500,
    max_items: int = 50,
    redaction_profile: RedactionProfile | str | None = None,
    redact: Iterable[str | Pattern[str]] | None = None,
    redact_keys: bool | None = None,
    redact_traceback: bool | None = None,
    redact_exception_strings: bool | None = None,
    include_env: bool = True,
    debug: bool = False,
    max_snapshots: int = 50,
    include_modules: Iterable[str] | None = None,
    max_exception_depth: int = 5,
    lock_timeout: float = 5.0,
    include_git: bool = True,
    include_array_stats: bool = False,
    include_args: bool = True,
    categorize_errors: bool = True,
    include_async_context: bool = True,
    capture_logs: bool = False,
    log_max_records: int = 20,
    output_format: OutputFormat = "json_compact",
    include_coverage: bool = True,
) -> SnapshotConfig:
    """Build a SnapshotConfig from the public-API keyword arguments."""
    profile_value = normalize_redaction_profile(redaction_profile)
    policy = resolve_redaction_policy(
        redaction_profile=profile_value,
        redact=tuple(redact) if redact is not None else None,
        redact_keys=redact_keys,
        redact_traceback=redact_traceback,
        redact_exception_strings=redact_exception_strings,
        legacy_redact=(),
        legacy_redact_keys=False,
        legacy_redact_traceback=False,
        legacy_redact_exception_strings=False,
    )
    return SnapshotConfig(
        out_dir=out_dir,
        frames=frames,
        source_context=source_context,
        source_mode=source_mode,
        locals_mode=locals_mode,
        max_str=max_str,
        max_items=max_items,
        redaction_profile=policy.profile,
        redact=policy.patterns,
        redact_keys=policy.redact_keys,
        redact_traceback=policy.redact_traceback,
        redact_exception_strings=policy.redact_exception_strings,
        include_env=include_env,
        debug=debug,
        max_snapshots=max_snapshots,
        include_modules=tuple(include_modules) if include_modules else None,
        max_exception_depth=max_exception_depth,
        lock_timeout=lock_timeout,
        include_git=include_git,
        include_array_stats=include_array_stats,
        include_args=include_args,
        categorize_errors=categorize_errors,
        include_async_context=include_async_context,
        capture_logs=capture_logs,
        log_max_records=log_max_records,
        output_format=output_format,
        include_coverage=include_coverage,
    )


def _try_capture(name: str, exc: Exception, cfg: SnapshotConfig) -> None:
    """Attempt to capture an exception snapshot, swallowing capture errors."""
    _, _, tb = sys.exc_info()
    try:
        capture_exception(name, exc, tb, cfg)
    except Exception as capture_exc:
        # Always log — a broken safety net is worse than a noisy one
        sys.stderr.write(
            f"llmdebug: capture failed for {name}: {type(capture_exc).__name__}: {capture_exc}\n"
        )


def debug_snapshot(
    *,
    name: str | None = None,
    out_dir: str = ".llmdebug",
    frames: int = 5,
    source_context: int = 3,
    source_mode: SourceMode = "all",
    locals_mode: LocalsMode = "safe",
    max_str: int = 500,
    max_items: int = 50,
    redaction_profile: RedactionProfile | str | None = None,
    redact: Iterable[str | Pattern[str]] | None = None,
    redact_keys: bool | None = None,
    redact_traceback: bool | None = None,
    redact_exception_strings: bool | None = None,
    include_env: bool = True,
    debug: bool = False,
    max_snapshots: int = 50,
    include_modules: Iterable[str] | None = None,
    max_exception_depth: int = 5,
    lock_timeout: float = 5.0,
    include_git: bool = True,
    include_array_stats: bool = False,
    include_args: bool = True,
    categorize_errors: bool = True,
    include_async_context: bool = True,
    capture_logs: bool = False,
    log_max_records: int = 20,
    output_format: OutputFormat = "json_compact",
    include_coverage: bool = True,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """Decorator that captures debug snapshots on exception.

    Usage:
        @debug_snapshot()
        def main():
            ...

    Args:
        name: Snapshot name (defaults to function name)
        out_dir: Output directory for snapshots
        frames: Number of stack frames to capture
        source_context: Lines of source before/after crash
        source_mode: "all" | "crash_only" | "none"
        locals_mode: "safe" | "meta" | "none"
        max_str: Max string length before truncation
        max_items: Max collection items to capture
        redaction_profile: Built-in redaction profile: "dev", "ci", or "prod"
        redact: Regex patterns to redact from output
        redact_keys: Also redact dict keys (default False to preserve structure)
        redact_traceback: Redact traceback text with active redactors
        redact_exception_strings: Redact exception message/args/notes strings
        include_env: Include Python/platform info
        debug: Warn on capture failure instead of silent
        max_snapshots: Max snapshots to keep (0 = unlimited)
        include_modules: Only capture frames from these module prefixes (None = all)
        max_exception_depth: Max depth for exception chain traversal
        lock_timeout: Seconds to wait for file lock
        include_git: Include git commit/branch/dirty status
        include_array_stats: Include min/max/mean/std for arrays
        include_args: Separate function arguments from locals
        categorize_errors: Auto-classify errors with suggestions
        include_async_context: Include asyncio task info
        capture_logs: Capture recent log records (requires enable_log_capture())
        log_max_records: Max log records to capture
        output_format: Output format: "json", "json_compact", or "toon"
        include_coverage: Include coverage data from pytest-cov (default True)
    """
    cfg = _build_config(
        out_dir=out_dir,
        frames=frames,
        source_context=source_context,
        source_mode=source_mode,
        locals_mode=locals_mode,
        max_str=max_str,
        max_items=max_items,
        redaction_profile=redaction_profile,
        redact=redact,
        redact_keys=redact_keys,
        redact_traceback=redact_traceback,
        redact_exception_strings=redact_exception_strings,
        include_env=include_env,
        debug=debug,
        max_snapshots=max_snapshots,
        include_modules=include_modules,
        max_exception_depth=max_exception_depth,
        lock_timeout=lock_timeout,
        include_git=include_git,
        include_array_stats=include_array_stats,
        include_args=include_args,
        categorize_errors=categorize_errors,
        include_async_context=include_async_context,
        capture_logs=capture_logs,
        log_max_records=log_max_records,
        output_format=output_format,
        include_coverage=include_coverage,
    )

    def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
        snap_name = name or fn.__name__

        @functools.wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            try:
                return fn(*args, **kwargs)
            except Exception as e:
                _try_capture(snap_name, e, cfg)
                raise

        return wrapper

    return decorator


@contextlib.contextmanager
def snapshot_section(
    section_name: str,
    *,
    out_dir: str = ".llmdebug",
    frames: int = 5,
    source_context: int = 3,
    source_mode: SourceMode = "all",
    locals_mode: LocalsMode = "safe",
    max_str: int = 500,
    max_items: int = 50,
    redaction_profile: RedactionProfile | str | None = None,
    redact: Iterable[str | Pattern[str]] | None = None,
    redact_keys: bool | None = None,
    redact_traceback: bool | None = None,
    redact_exception_strings: bool | None = None,
    include_env: bool = True,
    debug: bool = False,
    max_snapshots: int = 50,
    include_modules: Iterable[str] | None = None,
    max_exception_depth: int = 5,
    lock_timeout: float = 5.0,
    include_git: bool = True,
    include_array_stats: bool = False,
    include_args: bool = True,
    categorize_errors: bool = True,
    include_async_context: bool = True,
    capture_logs: bool = False,
    log_max_records: int = 20,
    output_format: OutputFormat = "json_compact",
    include_coverage: bool = True,
):
    """Context manager that captures debug snapshots on exception.

    Usage:
        with snapshot_section("data_loading"):
            ...
    """
    cfg = _build_config(
        out_dir=out_dir,
        frames=frames,
        source_context=source_context,
        source_mode=source_mode,
        locals_mode=locals_mode,
        max_str=max_str,
        max_items=max_items,
        redaction_profile=redaction_profile,
        redact=redact,
        redact_keys=redact_keys,
        redact_traceback=redact_traceback,
        redact_exception_strings=redact_exception_strings,
        include_env=include_env,
        debug=debug,
        max_snapshots=max_snapshots,
        include_modules=include_modules,
        max_exception_depth=max_exception_depth,
        lock_timeout=lock_timeout,
        include_git=include_git,
        include_array_stats=include_array_stats,
        include_args=include_args,
        categorize_errors=categorize_errors,
        include_async_context=include_async_context,
        capture_logs=capture_logs,
        log_max_records=log_max_records,
        output_format=output_format,
        include_coverage=include_coverage,
    )
    try:
        yield
    except Exception as e:
        _try_capture(section_name, e, cfg)
        raise


def load_jupyter(**kwargs: Any) -> None:
    """Install llmdebug into the current IPython/Jupyter session.

    Convenience wrapper around :func:`jupyter_plugin.install`.
    Requires the ``jupyter`` extra: ``pip install llmdebug[jupyter]``
    """
    try:
        from .jupyter_plugin import install
    except ImportError:
        raise RuntimeError(
            "IPython is not installed. Install with: pip install llmdebug[jupyter]"
        ) from None
    install(**kwargs)


def load_ipython_extension(ipython: Any) -> None:
    """IPython extension entry point for ``%load_ext llmdebug``."""
    from .jupyter_plugin import load_ipython_extension as _load

    _load(ipython)


def unload_ipython_extension(ipython: Any) -> None:
    """IPython extension entry point for ``%unload_ext llmdebug``."""
    from .jupyter_plugin import unload_ipython_extension as _unload

    _unload(ipython)
