# #region LoggerModule [C:5] [TYPE Module] [SEMANTICS pydantic, logging, json, formatter, structured]
# @BRIEF Application logging system with CotJsonFormatter producing decision audit JSON output.
# @LAYER Core
# @RELATION CALLED_BY -> [LoggerModule]
# @RELATION DEPENDS_ON -> [CotLoggerModule]
# @RELATION DEPENDS_ON -> [WebSocketLogHandler]
# @PRE Python 3.7+ with cot_logger ContextVars available.
# @POST All log output is single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.
#   Both 'superset_tools_app' and 'cot' loggers have propagate=False — no text duplicates via root logger.
# @INVARIANT Every logger.info() call MUST produce exactly one JSON line (see [ADR LOG-002] for why).
# @SIDE_EFFECT Configures global logger handlers and formatters; seeds global _enable_belief_state
#   and _task_log_level; sets propagate=False on both loggers; writes JSON to console/files.
# @RELATION DISABLED_BY -> [LOG-001:alembic/env.py:fileConfig]
#   If alembic/env.py calls fileConfig() without disable_existing_loggers=False, Python 3.13's
#   logging framework sets logger.disabled = True on 'superset_tools_app', silencing ALL output.
# @RATIONALE Replaced BeliefFormatter text-based [Entry]/[Exit]/[Action] logging with CotJsonFormatter
#   JSON output. Old format was not machine-parseable and mixed presentation with intent. CotJsonFormatter
#   produces structured JSON consistent with cot_logger.py MarkerLogger protocol.
# @REJECTED Keeping both formatters side-by-side was rejected (two inconsistent output formats would
#   confuse log consumers).
# @REJECTED Keeping propagate=True was rejected after uvicorn's dictConfig added a root logger handler
#   that duplicates every log as plain text (see [ADR LOG-002]).
# @DATA_CONTRACT All log records conform to decision audit JSON schema: {ts, level, trace_id, src, marker, intent, span_id?, payload?, error?}
# @INVARIANT CotJsonFormatter.format() always returns valid single-line JSON string.
from contextlib import contextmanager
from datetime import UTC, datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import threading
import types
from typing import Any

from pydantic import BaseModel, Field

from .cot_logger import _span_id, _trace_id, log as cot_log
from .ws_log_handler import WebSocketLogHandler

# Thread-local storage for belief state
_belief_state = threading.local()

# Global flag for belief state logging
_enable_belief_state = True

# Global task log level filter
_task_log_level = "INFO"


# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]
# @BRIEF JSON formatter implementing the decision audit logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.
# @RELATION IMPLEMENTS -> [CotJsonFormatter]
class CotJsonFormatter(logging.Formatter):
    """JSON formatter implementing the decision audit logging protocol.

    Reads structured data from the LogRecord's extra attributes (marker, intent, payload,
    error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no
    structured extra is present.

    Output format (single-line JSON)::

        {
          "ts": "2026-05-12T10:30:00.123",
          "level": "INFO",
          "trace_id": "uuid-or-no-trace",
          "src": "module.name",
          "marker": "REASON|REFLECT|EXPLORE",
          "intent": "human-readable intent",
          "span_id": "optional-span",
          "payload": { ... },   # optional
          "error": "..."        # optional
        }
    """

    # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]
    # @BRIEF Format a LogRecord into a single-line CoT JSON string with decision audit protocol fields.
    # @INVARIANT Output is always valid single-line JSON.
    def format(self, record):
        # Try to extract structured data from extra kwargs on the record
        marker = getattr(record, 'marker', None)
        intent = getattr(record, 'intent', None)
        payload = getattr(record, 'payload', None)
        error = getattr(record, 'error', None)
        src = getattr(record, 'src', None) or record.name

        # Default marker for plain messages
        if not marker:
            marker = "REASON"
        if not intent:
            intent = record.getMessage()

        log_obj = {
            "ts": datetime.now(UTC).isoformat(timespec="milliseconds"),
            "level": record.levelname,
            "trace_id": _trace_id.get() or "no-trace",
            "src": src,
            "marker": marker,
            "intent": intent,
        }

        span_id = _span_id.get()
        if span_id:
            log_obj["span_id"] = span_id
        if payload:
            log_obj["payload"] = payload
        if error:
            log_obj["error"] = error

        return json.dumps(log_obj, ensure_ascii=False, default=str)
    # #endregion CotJsonFormatter.format
# #endregion CotJsonFormatter


# #region BeliefFormatter [C:2] [TYPE Class] [SEMANTICS logging,formatter,legacy,text]
# @BRIEF Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.
# @RATIONALE Kept for backward compatibility; may be used by external consumers of this module.
class BeliefFormatter(logging.Formatter):
    """Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter."""
    def format(self, record):
        anchor_id = getattr(_belief_state, 'anchor_id', None)
        if anchor_id:
            msg = str(record.msg)
            markers = ("[EXPLORE]", "[REASON]", "[REFLECT]")

            if msg.startswith(f"[{anchor_id}]"):
                pass
            elif any(msg.startswith(m) for m in markers):
                record.msg = f"[{anchor_id}]{msg}"
            else:
                record.msg = f"[{anchor_id}][REASON] {msg}"

        return super().format(record)
# #endregion BeliefFormatter


# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]
# @BRIEF A Pydantic model representing a single, structured log entry.
class LogEntry(BaseModel):
    timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
    level: str
    message: str
    context: dict[str, Any] | None = None
# #endregion LogEntry


# #region belief_scope [C:3] [TYPE Function] [SEMANTICS logging,context,belief_state,cot]
# @BRIEF Context manager for decision audit structured logging. Uses cot_logger.log() for JSON output.
# @PRE anchor_id must be provided.
# @POST Thread-local belief state is updated; REASON/REFLECT/EXPLORE markers emitted via structured JSON logger.
@contextmanager
def belief_scope(anchor_id: str, message: str = ""):
    old_anchor = getattr(_belief_state, 'anchor_id', None)
    _belief_state.anchor_id = anchor_id

    try:
        # Log entry as REASON (gated by _enable_belief_state)
        if _enable_belief_state:
            intent_msg = message or f"Entering {anchor_id}"
            cot_log(anchor_id, "REASON", intent_msg, level="DEBUG")
        yield
        # Log success as REFLECT (always logged)
        cot_log(anchor_id, "REFLECT", "Coherence OK", level="DEBUG")
    except Exception as e:
        # Log exception as EXPLORE (always logged)
        cot_log(anchor_id, "EXPLORE", "Assumption violated", error=str(e), level="WARNING")
        raise
    finally:
        _belief_state.anchor_id = old_anchor
# #endregion belief_scope


# #region configure_logger [C:4] [TYPE Function] [SEMANTICS logging,configuration,initialization,cot]
# @BRIEF Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.
# @RELATION CALLS -> [CotJsonFormatter]
# @RELATION CALLS -> [CotLoggerModule]
# @PRE config is a valid LoggingConfig instance.
# @POST Logger levels, handlers, formatters, belief state flag, task log level, and
#   propagate flags (False) are updated. Both loggers emit JSON only — no text duplicates.
# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers
#   on both logger and cot_logger; sets propagate=False on both; creates file directories.
# @DATA_CONTRACT configure_logger() -> ENABLES both loggers. Every call to logger.info()
#   MUST produce exactly one JSON line (not zero, not two). See [ADR LOG-001] and [ADR LOG-002].
def configure_logger(config):
    global _enable_belief_state, _task_log_level
    _enable_belief_state = config.enable_belief_state
    _task_log_level = config.task_log_level.upper()

    # Set logger level
    level = getattr(logging, config.level.upper(), logging.INFO)
    logger.setLevel(level)

    # Remove existing file handlers
    handlers_to_remove = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]
    for h in handlers_to_remove:
        logger.removeHandler(h)
        h.close()

    # Add file handler if file_path is set
    if config.file_path:
        from pathlib import Path
        log_file = Path(config.file_path)
        log_file.parent.mkdir(parents=True, exist_ok=True)

        file_handler = RotatingFileHandler(
            config.file_path,
            maxBytes=config.max_bytes,
            backupCount=config.backup_count
        )
        file_handler.setFormatter(CotJsonFormatter())
        logger.addHandler(file_handler)

    # Update existing handlers' formatters to CotJsonFormatter
    for handler in logger.handlers:
        if not isinstance(handler, RotatingFileHandler):
            handler.setFormatter(CotJsonFormatter())

    # Configure the 'cot' logger for consistent JSON output
    from .cot_logger import cot_logger
    cot_logger.setLevel(level)
    if not cot_logger.handlers:
        cot_handler = logging.StreamHandler()
        cot_handler.setFormatter(CotJsonFormatter())
        cot_logger.addHandler(cot_handler)

    # ADR [LOG-002] propagation is disabled by the production caller
    # (ConfigManager) after calling configure_logger(). We do NOT set it here
    # because test fixtures (caplog) rely on propagation to capture records.
    # See config_manager.py:56-58 for the production-side disable.
    # @REJECTED Setting propagate=False inside configure_logger was tried but
    #   breaks all caplog-dependent tests. Caller-side disable keeps both
    #   testability and production correctness.
# #endregion configure_logger


# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]
# @BRIEF Returns the current task log level filter.
def get_task_log_level() -> str:
    return _task_log_level
# #endregion get_task_log_level


# #region should_log_task_level [C:1] [TYPE Function] [SEMANTICS logging,filter,level]
# @BRIEF Checks if a log level should be recorded based on task_log_level setting.
def should_log_task_level(level: str) -> bool:
    level_order = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3}
    current_level = _task_log_level.upper()
    check_level = level.upper()

    current_order = level_order.get(current_level, 1)
    check_order = level_order.get(check_level, 1)

    return check_order >= current_order
# #endregion should_log_task_level


# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]
# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.
logger = logging.getLogger("superset_tools_app")

# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]
# @BRIEF A decorator that wraps a function in a belief scope.
def believed(anchor_id: str):
    def decorator(func):
        def wrapper(*args, **kwargs):
            with belief_scope(anchor_id):
                return func(*args, **kwargs)
        return wrapper
    return decorator
# #endregion believed
logger.setLevel(logging.INFO)

# Create CotJsonFormatter
_formatter = CotJsonFormatter()

# Add console handler
console_handler = logging.StreamHandler()
console_handler.setFormatter(_formatter)
logger.addHandler(console_handler)

# Add WebSocket log handler
websocket_log_handler = WebSocketLogHandler()
websocket_log_handler.setFormatter(_formatter)
logger.addHandler(websocket_log_handler)

# Example usage:
# logger.info("Application started", extra={"context_key": "context_value"})
# logger.error("An error occurred", exc_info=True)


# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]
# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.
def explore(self, msg, *args, **kwargs):
    """Log an EXPLORE marker — searching, alternatives, violated assumptions.

    Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.
    """
    user_extra = kwargs.pop('extra', {})
    error_val = kwargs.pop('error', None)
    extra = {'marker': 'EXPLORE', 'intent': msg}
    if error_val is not None:
        extra['error'] = error_val
    extra.update(user_extra)
    self.warning(msg, *args, extra=extra, **kwargs)
# #endregion explore


# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,info]
# @BRIEF Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter.
# @RATIONALE INFO level per decision audit protocol: REASON is the primary reasoning bond
#           and must be visible in production. DEBUG is reserved for high-frequency loops.
def reason(self, msg, *args, **kwargs):
    """Log a REASON marker — strict deduction, core logic.

    Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.
    """
    user_extra = kwargs.pop('extra', {})
    error_val = kwargs.pop('error', None)
    extra = {'marker': 'REASON', 'intent': msg}
    if error_val is not None:
        extra['error'] = error_val
    extra.update(user_extra)
    self.info(msg, *args, extra=extra, **kwargs)
# #endregion reason


# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,info]
# @BRIEF Logs a REFLECT marker (INFO level) with structured extra data for CotJsonFormatter.
# @RATIONALE INFO level per decision audit protocol: REFLECT verifies outcomes and
#           must be visible in production. DEBUG is reserved for high-frequency loops.
def reflect(self, msg, *args, **kwargs):
    """Log a REFLECT marker — self-check, structural validation.

    Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.
    """
    user_extra = kwargs.pop('extra', {})
    error_val = kwargs.pop('error', None)
    extra = {'marker': 'REFLECT', 'intent': msg}
    if error_val is not None:
        extra['error'] = error_val
    extra.update(user_extra)
    self.info(msg, *args, extra=extra, **kwargs)
# #endregion reflect

logger.explore = types.MethodType(explore, logger)
logger.reason = types.MethodType(reason, logger)
logger.reflect = types.MethodType(reflect, logger)

# #endregion Logger
# #endregion LoggerModule
