"""
Utilities for logging.
"""

# Needed for loguru annotations
from __future__ import annotations

import inspect

# import logging - needed for InterceptHandler compatibility
import logging
import os
import queue
import sys
import threading
from functools import wraps
from typing import Any, Callable

import loguru
from flask import g, has_app_context
from loguru import logger

from ..config.paths import get_logs_directory
from ..database.models import ResearchLog
from ..web.services.socket_service import SocketIOService

_LOG_DIR = get_logs_directory()
_LOG_DIR.mkdir(parents=True, exist_ok=True)

# Thread-safe queue for database logs from background threads
_log_queue = queue.Queue(maxsize=1000)
_queue_processor_thread = None
_stop_queue = threading.Event()
"""
Default log directory to use.
"""


class InterceptHandler(logging.Handler):
    """
    Intercepts logging messages and forwards them to Loguru's logger.
    """

    def emit(self, record: logging.LogRecord) -> None:
        # Get corresponding Loguru level if it exists.
        try:
            level: str | int = logger.level(record.levelname).name
        except ValueError:
            level = record.levelno

        # Find caller from where originated the logged message.
        frame, depth = inspect.currentframe(), 0
        while frame:
            filename = frame.f_code.co_filename
            is_logging = filename == logging.__file__
            is_frozen = "importlib" in filename and "_bootstrap" in filename
            if depth > 0 and not (is_logging or is_frozen):
                break
            frame = frame.f_back
            depth += 1

        logger.opt(depth=depth, exception=record.exc_info).log(
            level, record.getMessage()
        )


def log_for_research(
    to_wrap: Callable[[str, ...], Any],
) -> Callable[[str, ...], Any]:
    """
    Decorator for a function that's part of the research process. It expects the function to
    take the research ID (UUID) as the first parameter, and configures all log
    messages made during this request to include the research ID.

    Args:
        to_wrap: The function to wrap. Should take the research ID as the first parameter.

    Returns:
        The wrapped function.

    """

    @wraps(to_wrap)
    def wrapped(research_id: str, *args: Any, **kwargs: Any) -> Any:
        g.research_id = research_id
        result = to_wrap(research_id, *args, **kwargs)
        g.pop("research_id")
        return result

    return wrapped


def _get_research_id(record=None) -> str | None:
    """
    Gets the current research ID (UUID), if present.

    Args:
        record: Optional loguru record that might contain bound research_id

    Returns:
        The current research ID (UUID), or None if it does not exist.

    """
    research_id = None

    # First check if research_id is bound to the log record
    if record and "extra" in record and "research_id" in record["extra"]:
        research_id = record["extra"]["research_id"]
    # Then check Flask context
    elif has_app_context():
        research_id = g.get("research_id")

    return research_id


def _process_log_queue():
    """
    Process logs from the queue in a dedicated thread with app context.
    This runs in the main thread context to avoid SQLite thread safety issues.
    """
    while not _stop_queue.is_set():
        try:
            # Wait for logs with timeout to check stop flag
            log_entry = _log_queue.get(timeout=0.1)

            # Skip if no entry
            if log_entry is None:
                continue

            # Write to database if we have app context
            if has_app_context():
                _write_log_to_database(log_entry)
            else:
                # If no app context, put it back in queue for later
                try:
                    _log_queue.put_nowait(log_entry)
                except queue.Full:
                    pass  # Drop log if queue is full

        except queue.Empty:
            continue
        except Exception:
            # Don't let logging errors crash the processor
            pass


def _write_log_to_database(log_entry: dict) -> None:
    """
    Write a log entry to the database. Should only be called from main thread.
    """
    from ..database.session_context import get_user_db_session

    try:
        username = log_entry.get("username")

        with get_user_db_session(username) as db_session:
            if db_session:
                db_log = ResearchLog(
                    timestamp=log_entry["timestamp"],
                    message=log_entry["message"],
                    module=log_entry["module"],
                    function=log_entry["function"],
                    line_no=log_entry["line_no"],
                    level=log_entry["level"],
                    research_id=log_entry["research_id"],
                )
                db_session.add(db_log)
                db_session.commit()
    except Exception:
        # Ignore database errors in logging
        pass


def database_sink(message: loguru.Message) -> None:
    """
    Sink that saves messages to the database.
    Queues logs from background threads for later processing.

    Args:
        message: The log message to save.

    """
    record = message.record
    research_id = _get_research_id(record)

    # Create log entry dict
    log_entry = {
        "timestamp": record["time"],
        "message": record["message"],
        "module": record["name"],
        "function": record["function"],
        "line_no": int(record["line"]),
        "level": record["level"].name,
        "research_id": research_id,
        "username": record.get("extra", {}).get("username"),
    }

    # Check if we're in a background thread
    # Note: Socket.IO handlers run in separate threads even with app context
    if not has_app_context() or threading.current_thread().name != "MainThread":
        # Queue the log for later processing
        try:
            _log_queue.put_nowait(log_entry)
        except queue.Full:
            # Drop log if queue is full to avoid blocking
            pass
    else:
        # We're in the main thread with app context - write directly
        _write_log_to_database(log_entry)


def frontend_progress_sink(message: loguru.Message) -> None:
    """
    Sink that sends messages to the frontend.

    Args:
        message: The log message to send.

    """
    record = message.record
    research_id = _get_research_id(record)
    if research_id is None:
        # If we don't have a research ID, don't send anything.
        # Can't use logger here as it causes deadlock
        return

    frontend_log = dict(
        log_entry=dict(
            message=record["message"],
            type=record["level"].name,  # Keep original case
            time=record["time"].isoformat(),
        ),
    )
    SocketIOService().emit_to_subscribers(
        "progress", research_id, frontend_log, enable_logging=False
    )


def flush_log_queue():
    """
    Flush all pending logs from the queue to the database.
    This should be called from a Flask request context.
    """
    flushed = 0
    while not _log_queue.empty():
        try:
            log_entry = _log_queue.get_nowait()
            _write_log_to_database(log_entry)
            flushed += 1
        except queue.Empty:
            break
        except Exception:
            pass

    if flushed > 0:
        logger.debug(f"Flushed {flushed} queued log entries to database")


def config_logger(name: str, debug: bool = False) -> None:
    """
    Configures the default logger.

    Args:
        name: The name to use for the log file.
        debug: Whether to enable unsafe debug logging.

    """
    logger.enable("local_deep_research")
    logger.remove()

    # Log to console (stderr) and database
    logger.add(sys.stderr, level="INFO", diagnose=debug)
    logger.add(database_sink, level="DEBUG", diagnose=debug)
    logger.add(frontend_progress_sink, diagnose=debug)

    # Optionally log to file if enabled (disabled by default for security)
    # Check environment variable first, then database setting
    enable_file_logging = (
        os.environ.get("LDR_ENABLE_FILE_LOGGING", "").lower() == "true"
    )

    # File logging is controlled only by environment variable for simplicity
    # Database settings are not available at logger initialization time

    if enable_file_logging:
        log_file = _LOG_DIR / f"{name}.log"
        logger.add(
            log_file,
            level="DEBUG",
            rotation="10 MB",
            retention="7 days",
            compression="zip",
            diagnose=debug,
        )
        logger.warning(
            f"File logging enabled - logs will be written to {log_file}. "
            "WARNING: Log files are unencrypted and may contain sensitive data!"
        )

    # Add a special log level for milestones.
    try:
        logger.level("MILESTONE", no=26, color="<magenta><bold>")
    except ValueError:
        # Level already exists, that's fine
        pass
