"""Debug logger for JSON Lines diagnostic output.

This module provides the DebugLogger class that writes structured debug logs
in JSON Lines format to a specified file. It includes secret masking to ensure
sensitive information never appears in logs.
"""

from __future__ import annotations

import json
import re
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


# Patterns for secret masking
_BEARER_TOKEN_PATTERN = re.compile(r"Bearer [A-Za-z0-9._-]+")
_CLIENT_SECRET_PATTERN = re.compile(
    r"(client[_-]?secret[\"']?\s*[:=]\s*[\"']?)[A-Za-z0-9._~+/-]+"
)


def mask_secrets(value: str) -> str:
    """Mask secrets in a string value.

    Masks:
    - Bearer tokens in Authorization headers
    - Client secrets in configuration strings

    Args:
        value: The string value that may contain secrets.

    Returns:
        The value with secrets replaced by [REDACTED].
    """
    result = _BEARER_TOKEN_PATTERN.sub("Bearer [REDACTED]", value)
    result = _CLIENT_SECRET_PATTERN.sub(r"\1[REDACTED]", result)
    return result


def mask_upn(upn: str) -> str:
    """Mask a User Principal Name (UPN) for logging.

    Truncates UPN to show only first character of local part and domain.
    Example: john.doe@contoso.com -> j***@contoso.com

    Args:
        upn: The User Principal Name to mask.

    Returns:
        The masked UPN.
    """
    if "@" not in upn:
        # Not a UPN format, just mask most of it
        if len(upn) > 1:
            return upn[0] + "***"
        return "***"

    local_part, domain = upn.rsplit("@", 1)
    if local_part:
        return f"{local_part[0]}***@{domain}"
    return f"***@{domain}"


def mask_headers(headers: dict[str, str]) -> dict[str, str]:
    """Mask sensitive values in HTTP headers.

    Args:
        headers: Dictionary of HTTP headers.

    Returns:
        Copy of headers with sensitive values masked.
    """
    masked = {}
    for key, value in headers.items():
        if key.lower() == "authorization":
            masked[key] = mask_secrets(value)
        else:
            masked[key] = value
    return masked


class DebugLogger:
    """JSON Lines logger for debug output with secret masking.

    Writes structured debug logs to a file in JSON Lines format.
    Each log entry is a self-contained JSON object on a single line.

    Attributes:
        log_path: Path to the debug log file.
        correlation_id: UUID linking all log entries for this session.
    """

    def __init__(self, log_path: Path) -> None:
        """Initialize the DebugLogger.

        Creates a new correlation ID for this logging session.
        Opens the log file in append mode with UTF-8 encoding.

        Args:
            log_path: Path to the debug log file.
        """
        self.log_path = log_path
        self.correlation_id = str(uuid.uuid4())[:8]
        # Ensure parent directory exists
        log_path.parent.mkdir(parents=True, exist_ok=True)

    def _write_entry(self, entry: dict[str, Any]) -> None:
        """Write a log entry to the file.

        Args:
            entry: The log entry dictionary to write.
        """
        with open(self.log_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry, default=str) + "\n")

    def _create_entry(
        self,
        level: str,
        event: str,
        **kwargs: Any,
    ) -> dict[str, Any]:
        """Create a base log entry with common fields.

        Args:
            level: Log level (DEBUG, INFO, WARNING, ERROR).
            event: Event type (command_start, graph_request, etc.).
            **kwargs: Additional fields for the log entry.

        Returns:
            Dictionary with timestamp, correlation_id, level, event, and kwargs.
        """
        entry: dict[str, Any] = {
            "ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
            "correlation_id": self.correlation_id,
            "level": level,
            "event": event,
        }
        entry.update(kwargs)
        return entry

    def log_command_start(
        self,
        command: str,
        *,
        tenant: str | None = None,
    ) -> None:
        """Log the start of a CLI command.

        Args:
            command: The command being executed (e.g., "users list").
            tenant: The tenant name if specified.
        """
        entry = self._create_entry("INFO", "command_start", command=command)
        if tenant:
            entry["tenant"] = tenant
        self._write_entry(entry)

    def log_command_complete(
        self,
        exit_code: int,
        duration_ms: float,
    ) -> None:
        """Log the completion of a CLI command.

        Args:
            exit_code: The command's exit code.
            duration_ms: Command duration in milliseconds.
        """
        entry = self._create_entry(
            "INFO",
            "command_complete",
            exit_code=exit_code,
            duration_ms=round(duration_ms, 1),
        )
        self._write_entry(entry)

    def log_graph_request(
        self,
        method: str,
        url: str,
        *,
        body_size: int | None = None,
    ) -> float:
        """Log a Graph API request being made.

        Args:
            method: HTTP method (GET, POST, etc.).
            url: The request URL (will have query params sanitized).
            body_size: Size of request body in bytes, if applicable.

        Returns:
            Start time from time.perf_counter() for duration calculation.
        """
        # Sanitize URL by removing any potential secrets in query params
        sanitized_url = mask_secrets(url)
        entry = self._create_entry(
            "DEBUG",
            "graph_request",
            method=method,
            url=sanitized_url,
        )
        if body_size is not None:
            entry["body_size"] = body_size
        self._write_entry(entry)
        return time.perf_counter()

    def log_graph_response(
        self,
        status_code: int,
        start_time: float,
        *,
        items_count: int | None = None,
    ) -> None:
        """Log a Graph API response received.

        Args:
            status_code: HTTP status code.
            start_time: The start time from log_graph_request().
            items_count: Number of items in response, if applicable.
        """
        duration_ms = (time.perf_counter() - start_time) * 1000
        entry = self._create_entry(
            "DEBUG",
            "graph_response",
            status=status_code,
            duration_ms=round(duration_ms, 1),
        )
        if items_count is not None:
            entry["items_count"] = items_count
        self._write_entry(entry)

    def log_retry(
        self,
        attempt: int,
        max_attempts: int,
        status_code: int,
        wait_seconds: float,
    ) -> None:
        """Log a retry attempt.

        Args:
            attempt: Current attempt number.
            max_attempts: Maximum number of attempts.
            status_code: HTTP status code that triggered retry.
            wait_seconds: Seconds to wait before retry.
        """
        entry = self._create_entry(
            "WARNING",
            "retry",
            attempt=attempt,
            max_attempts=max_attempts,
            status=status_code,
            wait_seconds=round(wait_seconds, 1),
        )
        self._write_entry(entry)

    def log_cache_hit(self, cache_key: str) -> None:
        """Log a cache hit.

        Args:
            cache_key: The cache key that was hit.
        """
        entry = self._create_entry("DEBUG", "cache_hit", cache_key=cache_key)
        self._write_entry(entry)

    def log_cache_miss(self, cache_key: str) -> None:
        """Log a cache miss.

        Args:
            cache_key: The cache key that was missed.
        """
        entry = self._create_entry("DEBUG", "cache_miss", cache_key=cache_key)
        self._write_entry(entry)

    def log_error(
        self,
        message: str,
        *,
        error_type: str | None = None,
        details: str | None = None,
    ) -> None:
        """Log an error.

        Args:
            message: Error message (will be sanitized for secrets).
            error_type: Type/class of the error.
            details: Additional error details (will be sanitized).
        """
        entry = self._create_entry(
            "ERROR",
            "error",
            message=mask_secrets(message),
        )
        if error_type:
            entry["error_type"] = error_type
        if details:
            entry["details"] = mask_secrets(details)
        self._write_entry(entry)

    def log_debug(self, event: str, **kwargs: Any) -> None:
        """Log a generic debug event.

        Values in kwargs will be converted to strings and have secrets masked.

        Args:
            event: Event name for the log entry.
            **kwargs: Additional fields for the log entry.
        """
        # Mask any string values in kwargs
        masked_kwargs = {}
        for key, value in kwargs.items():
            if isinstance(value, str):
                masked_kwargs[key] = mask_secrets(value)
            else:
                masked_kwargs[key] = value
        entry = self._create_entry("DEBUG", event, **masked_kwargs)
        self._write_entry(entry)
