"""Log handlers for SAGE audit logging.

Provides file handlers with rotation and retention policies.
"""

import gzip
import logging
import logging.handlers
import re
from datetime import UTC, datetime, timedelta
from pathlib import Path


def parse_size(size_str: str) -> int:
    """Parse size string to bytes.

    Args:
        size_str: Size string like "10MB", "1GB", "500KB".

    Returns:
        Size in bytes.

    Raises:
        ValueError: If size string is invalid.
    """
    match = re.match(r"^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)?$", size_str.upper())
    if not match:
        raise ValueError(f"Invalid size string: {size_str}")

    value = float(match.group(1))
    unit = match.group(2) or "B"

    multipliers = {
        "B": 1,
        "KB": 1024,
        "MB": 1024 ** 2,
        "GB": 1024 ** 3,
        "TB": 1024 ** 4,
    }

    return int(value * multipliers[unit])


class RotatingAuditHandler(logging.handlers.RotatingFileHandler):
    """Rotating file handler for audit logs with compression and retention.

    Features:
    - Size-based rotation with configurable threshold
    - Automatic gzip compression of rotated files
    - Time-based retention policy
    - Atomic writes for log integrity
    """

    def __init__(
        self,
        filename: str | Path,
        max_bytes: int | str = "10MB",
        backup_count: int = 100,
        retention_days: int = 90,
        compress: bool = True,
        encoding: str = "utf-8",
    ):
        """Initialize rotating audit handler.

        Args:
            filename: Path to audit log file.
            max_bytes: Maximum file size before rotation (int or string like "10MB").
            backup_count: Maximum number of backup files to keep.
            retention_days: Days to retain old log files.
            compress: Whether to gzip rotated files.
            encoding: File encoding.
        """
        # Parse max_bytes if string
        if isinstance(max_bytes, str):
            max_bytes = parse_size(max_bytes)

        # Ensure directory exists
        filepath = Path(filename)
        filepath.parent.mkdir(parents=True, exist_ok=True)

        super().__init__(
            str(filepath),
            maxBytes=max_bytes,
            backupCount=backup_count,
            encoding=encoding,
        )

        self.retention_days = retention_days
        self.compress = compress
        self._cleanup_old_files()

    def doRollover(self) -> None:
        """Perform log rotation with optional compression."""
        super().doRollover()

        if self.compress:
            self._compress_rotated_file()

        self._cleanup_old_files()

    def _compress_rotated_file(self) -> None:
        """Compress the most recently rotated file."""
        # Find the most recent .1 backup file
        backup_path = Path(f"{self.baseFilename}.1")
        if backup_path.exists():
            compressed_path = backup_path.with_suffix(backup_path.suffix + ".gz")
            try:
                with open(backup_path, "rb") as f_in:
                    with gzip.open(compressed_path, "wb") as f_out:
                        f_out.writelines(f_in)
                backup_path.unlink()
            except OSError as e:
                # Log compression failure but don't crash
                logging.getLogger(__name__).warning(
                    f"Failed to compress audit log: {e}"
                )

    def _cleanup_old_files(self) -> None:
        """Remove log files older than retention period."""
        if self.retention_days <= 0:
            return

        cutoff = datetime.now(UTC) - timedelta(days=self.retention_days)
        base_path = Path(self.baseFilename)
        log_dir = base_path.parent
        base_name = base_path.name

        for filepath in log_dir.glob(f"{base_name}*"):
            if filepath.name == base_name:
                # Don't delete current log file
                continue

            try:
                mtime = datetime.fromtimestamp(
                    filepath.stat().st_mtime,
                    tz=UTC,
                )
                if mtime < cutoff:
                    filepath.unlink()
            except OSError:
                # Ignore files we can't stat or delete
                pass


class AsyncAuditHandler(logging.Handler):
    """Async-safe audit handler using a queue.

    Buffers log records and writes them asynchronously to avoid
    blocking the main application thread.
    """

    def __init__(
        self,
        filename: str | Path,
        max_bytes: int | str = "10MB",
        flush_interval: float = 1.0,
        max_buffer_size: int = 1000,
    ):
        """Initialize async audit handler.

        Args:
            filename: Path to audit log file.
            max_bytes: Maximum file size before rotation.
            flush_interval: Seconds between buffer flushes.
            max_buffer_size: Maximum records to buffer before forced flush.
        """
        super().__init__()
        self._buffer: list[logging.LogRecord] = []
        self._max_buffer_size = max_buffer_size
        self._flush_interval = flush_interval

        # Underlying file handler
        self._file_handler = RotatingAuditHandler(
            filename=filename,
            max_bytes=max_bytes,
        )

    def emit(self, record: logging.LogRecord) -> None:
        """Buffer log record for async writing.

        Args:
            record: Log record to buffer.
        """
        self._buffer.append(record)

        if len(self._buffer) >= self._max_buffer_size:
            self.flush()

    def flush(self) -> None:
        """Write all buffered records to file."""
        records = self._buffer
        self._buffer = []

        for record in records:
            try:
                self._file_handler.emit(record)
            except Exception:
                # Silently discard to avoid recursive logging errors.
                # This is intentional - logging handler exceptions must not propagate.
                pass  # nosec B110

        self._file_handler.flush()

    def close(self) -> None:
        """Flush and close handler."""
        self.flush()
        self._file_handler.close()
        super().close()
