import json
import logging
import os
import sys
from collections import OrderedDict
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

from common import ENV_LOG_MAX_LENGTH, ENV_LOG_TRUNCATE
from cli_logging.logging_utils import (
    STRUCTURED_LOG_FIELDS,
    Operation,
    format_structured_log_entry,
    redact_sensitive_content,
)

_ANSI_COLORS = {
    "debug": "\033[2m",     # dim
    "info": "",             # default
    "warning": "\033[33m",  # yellow
    "error": "\033[31m",    # red
}
_ANSI_RESET = "\033[0m"
_DEFAULT_MAX_MESSAGE_LENGTH = 250
_TRUE_VALUES = frozenset(("1", "true", "yes", "on"))
_FALSE_VALUES = frozenset(("0", "false", "no", "off"))


@dataclass(frozen=True)
class ConsoleLogConfig:
    truncate_messages: bool
    max_message_length: int

    @classmethod
    def from_environment(cls) -> "ConsoleLogConfig":
        truncate_value = os.environ.get(ENV_LOG_TRUNCATE, "true").strip().lower()
        if truncate_value in _TRUE_VALUES:
            truncate_messages = True
        elif truncate_value in _FALSE_VALUES:
            truncate_messages = False
        else:
            raise ValueError(
                f"{ENV_LOG_TRUNCATE} must be one of: "
                "true, false, 1, 0, yes, no, on, off."
            )

        max_length_value = os.environ.get(
            ENV_LOG_MAX_LENGTH,
            str(_DEFAULT_MAX_MESSAGE_LENGTH),
        )
        try:
            max_message_length = int(max_length_value)
        except ValueError as exc:
            raise ValueError(
                f"{ENV_LOG_MAX_LENGTH} must be a positive integer."
            ) from exc
        if max_message_length < 1:
            raise ValueError(
                f"{ENV_LOG_MAX_LENGTH} must be a positive integer."
            )

        return cls(
            truncate_messages=truncate_messages,
            max_message_length=max_message_length,
        )


_CONSOLE_LOG_CONFIG: Optional[ConsoleLogConfig] = None


def set_console_log_config(config: ConsoleLogConfig) -> None:
    """Retain the startup-validated console configuration for log rendering."""
    global _CONSOLE_LOG_CONFIG
    _CONSOLE_LOG_CONFIG = config


def format_diagnostic_record(record: Dict[str, Any]) -> OrderedDict:
    ordered = OrderedDict()
    for field in STRUCTURED_LOG_FIELDS:
        default = False if field == "is-redacted" else None
        ordered[field] = record.get(field, default)
    return ordered


def serialize_diagnostic_record(record: Dict[str, Any]) -> str:
    return json.dumps(format_diagnostic_record(record), ensure_ascii=False)


def format_console_record(
    record: Dict[str, Any],
    config: Optional[ConsoleLogConfig] = None,
) -> str:
    """Format a diagnostic record for human-readable TTY output with ANSI colors."""
    effective_config = config or ConsoleLogConfig.from_environment()
    ts = record.get("timestamp", "")
    # Extract HH:MM:SS from ISO timestamp
    time_part = ts[11:19] if len(ts) >= 19 else ts
    level_name = (record.get("level") or "info").lower()
    level = level_name.upper()
    message = record.get("message", "")
    if (
        effective_config.truncate_messages
        and level_name in {"debug", "info"}
        and len(message) > effective_config.max_message_length
    ):
        message = message[:effective_config.max_message_length] + "…"

    ids = []
    for key in ("request-id", "conversation-id", "message-id", "response-timestamp"):
        val = record.get(key)
        if val:
            ids.append(f"{key}={val}")
    id_suffix = f" ({' | '.join(ids)})" if ids else ""

    color = _ANSI_COLORS.get(level_name, "")
    reset = _ANSI_RESET if color else ""
    return f"{color}[{time_part}] {level} {message}{id_suffix}{reset}"


def render_diagnostic(
    record: Dict[str, Any],
    config: Optional[ConsoleLogConfig] = None,
) -> str:
    """Return TTY-friendly or JSON output depending on whether stdout is a terminal."""
    if sys.stdout.isatty():
        return format_console_record(record, config=config)
    return serialize_diagnostic_record(record)


def emit_structured_log(
    level: str,
    message: str,
    operation: str = Operation.EVALUATE,
    *,
    logger: logging.Logger,
    diagnostic_records: Optional[List[Dict[str, Any]]] = None,
    run_context: Optional[Dict[str, Any]] = None,
    logger_name_override: Optional[str] = None,
) -> None:
    """Emit a structured log entry.

    Formats via format_structured_log_entry, optionally appends to
    diagnostic_records, then logs via render_diagnostic (TTY-friendly or JSON).

    Args:
        level: One of "debug", "info", "warning", "error".
        message: Human-readable log message.
        operation: The CLI operation step (e.g. Operation.SEND_PROMPT).
        logger: Logger to emit through.
        diagnostic_records: If provided, the structured entry is appended here.
        run_context: Full run context override (request-id, conversation-id,
            message-id). Defaults to nulls with the given operation.
        logger_name_override: When set, used as the "logger" field of the
            structured entry instead of logger.name. Lets handlers that fan
            third-party records into CLI_LOGGER preserve the original
            source-logger name (e.g. "msal", "azure.identity").
    """
    log_level_int = getattr(logging, level.upper(), logging.INFO)
    if diagnostic_records is None and not logger.isEnabledFor(log_level_int):
        return

    context = run_context or {
        "request-id": None,
        "conversation-id": None,
        "message-id": None,
        "operation": operation,
    }
    entry = format_structured_log_entry(
        level=level,
        message=message,
        logger_name=logger_name_override or logger.name,
        run_context=context,
    )
    if diagnostic_records is not None:
        diagnostic_records.append(entry)
    # Rendering/redaction/logging failures must never crash the caller's
    # business logic (auth, evaluation, etc.); config-validation failures for
    # RUNEVALS_LOG_TRUNCATE/RUNEVALS_LOG_MAX_LENGTH are surfaced separately as
    # a fail-fast startup error in configure_cli_logging, not on every call
    # here.
    try:
        rendered, _ = redact_sensitive_content(
            render_diagnostic(entry, config=_CONSOLE_LOG_CONFIG)
        )
        logger.log(getattr(logging, level.upper(), logging.INFO), rendered)
    except Exception:
        pass
