"""
TaskLogger: A threadsafe logging system for task-specific logs.
"""

import logging
import sys
from logging.handlers import RotatingFileHandler
import os
import threading
from datetime import datetime
import uuid
import json
from enum import IntEnum, Enum


class LogLevel(IntEnum):
    """Log level constants - use instead of importing logging module.

    Usage:
        from hil_testbench.run.task_logger import LogLevel  # docstring example
        logger.log_struct("event", LogLevel.INFO, ...)
    """

    DEBUG = logging.DEBUG
    INFO = logging.INFO
    WARNING = logging.WARNING
    ERROR = logging.ERROR
    CRITICAL = logging.CRITICAL


class LogScope(Enum):
    """Semantic scope for a log event.

    FRAMEWORK: Runner/framework level (lifecycle, configuration) → main log only
    TASK: High-level task lifecycle (started/completed) → main log only
    COMMAND: Individual command execution output/events → per-command log
    """

    FRAMEWORK = "framework"
    TASK = "task"
    COMMAND = "command"


# Default constants for RotatingFileHandler (can be overridden)
DEFAULT_MAX_BYTES_MAIN = 10 * 1024 * 1024  # 10MB for main logger
DEFAULT_MAX_BYTES_TASK = 5 * 1024 * 1024  # 5MB per task logger
DEFAULT_LOG_FILE_COUNT_MAIN = 10  # Keep 10 rotated files for main logger
DEFAULT_LOG_FILE_COUNT_TASK = 10  # Keep 10 rotated files per task logger


class TaskLogger:
    """
    Threadsafe logger for task-specific and program-wide logs.
    """

    _singleton = None
    _pre_setup_buffer: list[tuple[str, str, str | None, bool | None]] = []

    def __new__(cls, *args, **kwargs):
        """
        Ensure TaskLogger is a singleton.
        """
        if cls._singleton is None:
            cls._singleton = super().__new__(cls)
        return cls._singleton

    def __init__(
        self,
        log_dir: str = "logs",
        max_bytes_main: int = DEFAULT_MAX_BYTES_MAIN,
        max_bytes_task: int = DEFAULT_MAX_BYTES_TASK,
        max_log_file_count_main: int = DEFAULT_LOG_FILE_COUNT_MAIN,
        max_log_file_count_task: int = DEFAULT_LOG_FILE_COUNT_TASK,
        enable_console: bool = True,
        correlation_id: str | None = None,
        log_level: str = "INFO",  # Main logger level
    ):
        """
        Initialize TaskLogger with a log directory and file size limits.

        Args:
            log_dir: Directory for log files
            max_bytes_main: Maximum bytes per main log file before rotation
            max_bytes_task: Maximum bytes per task log file before rotation
            max_log_file_count_main: Number of rotated main log files to keep
            max_log_file_count_task: Number of rotated task log files to keep
            enable_console: Enable console output for main logger
            correlation_id: Unique ID for tracking multi-run sessions
            log_level: Main logger level (DEBUG, INFO, WARNING, ERROR)
        """
        if getattr(self, "_initialized", False):
            return
        self.log_dir = log_dir
        self.max_bytes_main = max_bytes_main
        self.max_bytes_task = max_bytes_task
        self.max_log_file_count_main = max_log_file_count_main
        self.max_log_file_count_task = max_log_file_count_task
        self.enable_console = enable_console
        self.log_level = getattr(
            logging, log_level.upper(), logging.INFO
        )  # Convert string to level
        self.correlation_id = correlation_id or uuid.uuid4().hex[:12]
        self._lock = threading.Lock()
        self._loggers: dict[str, logging.Logger] = {}
        self._files: dict[str, str] = {}
        self._task_counts: dict[str, int] = {}
        self._display_backend = None
        self._initialized = False

        # Create root logs directory
        os.makedirs(log_dir, exist_ok=True)

        # Create per-execution directory with human-readable timestamp
        ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[
            :-3
        ]  # YYYY-MM-DD_HH-MM-SS_mmm
        exec_dir = os.path.join(self.log_dir, ts)
        os.makedirs(exec_dir, exist_ok=True)
        self.execution_dir = exec_dir

        # Create main program logger
        self._setup_main_logger()
        self._initialized = True
        self._flush_buffer()

    def _setup_main_logger(self):
        """
        Set up the main program logger.
        """
        log_file = os.path.join(self.execution_dir, "runner.log")
        self._main_log_file = log_file

        # Create main logger
        self._main_logger = logging.getLogger("taskrunner_main")
        self._main_logger.setLevel(self.log_level)  # Use configured level
        self._main_logger.handlers.clear()

        # Create file handler with rotation (10MB max, keep 10)

        file_handler = RotatingFileHandler(
            log_file,
            maxBytes=self.max_bytes_main,
            backupCount=self.max_log_file_count_main,
            encoding="utf-8",  # 10MB
        )
        file_handler.setLevel(self.log_level)  # Use configured level

        # Previous exclusion filter removed after introducing explicit LogScope routing.

        # JSONL format - just the raw JSON object per line, no timestamp prefix
        formatter = logging.Formatter("%(message)s")
        file_handler.setFormatter(formatter)
        self._main_logger.addHandler(file_handler)

        # Optional console handler for user-facing output
        if self.enable_console:
            # Check if console handler already exists (not file handler)
            has_console = any(
                isinstance(h, logging.StreamHandler)
                and not isinstance(h, (RotatingFileHandler, logging.FileHandler))
                for h in self._main_logger.handlers
            )
            if not has_console:

                class ConsoleHandler(logging.StreamHandler):
                    """StreamHandler that suppresses empty formatted messages."""

                    def emit(self, record: logging.LogRecord) -> None:  # type: ignore[override]
                        try:
                            msg = self.format(record)
                            if not msg or not str(msg).strip():
                                return  # Skip emitting blank lines
                            stream = self.stream
                            try:
                                stream.write(msg + self.terminator)
                            except UnicodeEncodeError:
                                enc = getattr(stream, "encoding", None) or "utf-8"
                                safe = msg.encode(enc, errors="ignore").decode(
                                    enc, errors="ignore"
                                )
                                stream.write(safe + self.terminator)
                            self.flush()
                        except Exception:  # pylint: disable=broad-except
                            self.handleError(record)

                console_handler = ConsoleHandler(sys.stdout)
                console_handler.setLevel(logging.INFO)

                # Custom formatter that extracts message from JSON for readability
                class ConsoleFormatter(logging.Formatter):
                    def format(self, record):
                        try:
                            # Parse JSON and extract human-readable parts
                            data = json.loads(record.msg)
                            event = data.get("event", "")
                            scope = data.get("scope")
                            msg_data = data.get("data", {})
                            msg = msg_data.get("message", "")
                            task = data.get("task")

                            # Build console-friendly message with prefix
                            prefix = f"[{task}] " if task else ""

                            # Show specific framework events on console
                            # Unified set of lifecycle events we render directly
                            lifecycle_events = {
                                "task_started",
                                "task_completed",
                                "command_started",
                                "command_succeeded",
                                "command_failed",
                                "command_result",  # aggregate summary per command
                            }
                            if (
                                scope in ("task", "command")
                                and event in lifecycle_events
                            ):
                                # Icons:
                                # - Start: green play (fallback to ▶)
                                # - Success: green check
                                # - Failure: red X
                                # "✅🚨🚥🚦🚧🚩🏴‍☠️🏳️🏴🏗️🏭
                                # 🛎️🪠⭐⛔⭕❓‼️⁉️
                                # 💯☢️☣️⚠️💹❇️✳️❎✅
                                # 🔴🔴🟠🟡🟢🔵✔️☑️💻🖥️🔗⛓️‍💥🛡️📡💡🔎📜📚📒📦📝📊📉📈📌
                                # 🟣🟤⚫⚪🟥🟧🟨🟩🟦🟪🟫⬛⬜🔶🔷🔺🔻🕒"
                                task_start_icon = "✔️ "  # may be stripped by fallback handler if unsupported
                                task_ok_icon = "✅"
                                task_fail_icon = "❌"
                                cmd_ok_icon = "🟢"
                                cmd_fail_icon = "🔴"

                                # TASK lifecycle
                                if event == "task_started" and msg:
                                    return f"{task_start_icon}{msg}"
                                if event == "task_completed":
                                    success = bool(msg_data.get("success", False))
                                    icon = task_ok_icon if success else task_fail_icon
                                    return f"{icon} {msg}" if msg else icon

                                # COMMAND lifecycle (new)
                                if event == "command_started":
                                    command_name = msg_data.get("command")
                                    if command_name:
                                        return f"▶ {command_name}"
                                    return msg or "▶ command started"
                                if event == "command_succeeded":
                                    command_name = msg_data.get("command")
                                    dur = msg_data.get("duration_seconds")
                                    icon = cmd_ok_icon
                                    if command_name:
                                        if dur:
                                            return f"{icon} {command_name} ({dur}s)"
                                        return f"{icon} {command_name}"
                                    return f"{icon} {msg}" if msg else icon
                                if event == "command_failed":
                                    command_name = msg_data.get("command")
                                    err = msg_data.get("error")
                                    icon = cmd_fail_icon
                                    base = command_name or msg or "command failed"
                                    if err:
                                        return f"{icon} {base}: {err}"
                                    return f"{icon} {base}"
                                if event == "command_result":
                                    success = bool(msg_data.get("success", False))
                                    icon = cmd_ok_icon if success else cmd_fail_icon
                                    return f"{icon} {msg}" if msg else icon

                                return msg
                            # Show warnings/errors with messages
                            elif (
                                event
                                in (
                                    "warning_message",
                                    "error_message",
                                    "interrupt_received",
                                    "schema_validation_warning",
                                )
                                and msg
                            ):
                                warn_icon = "⚠" if event == "interrupt_received" else ""
                                icon_prefix = f"{warn_icon} " if warn_icon else ""
                                return f"{icon_prefix}{prefix}{msg}"
                            # All other events - log file only
                            else:
                                return ""
                        except Exception:  # pylint: disable=broad-except
                            # Not JSON - return as-is
                            return str(record.msg)

                console_formatter = ConsoleFormatter()
                console_handler.setFormatter(console_formatter)
                self._main_logger.addHandler(console_handler)

    # Convenience logging helpers
    # Legacy convenience methods now delegate to structured logging
    def info(self, message: str, task: str | None = None):
        """Log an info-level message."""
        self.log_struct("info_message", LogLevel.INFO, task=task, message=message)

    def debug(self, message: str, task: str | None = None):
        """Log a debug-level message."""
        self.log_struct("debug_message", LogLevel.DEBUG, task=task, message=message)

    def warning(self, message: str, task: str | None = None):
        """Log a warning-level message."""
        self.log_struct("warning_message", LogLevel.WARNING, task=task, message=message)

    def error(self, message: str, task: str | None = None):
        """Log an error-level message."""
        self.log_struct("error_message", LogLevel.ERROR, task=task, message=message)

    def exception(self, message: str, task: str | None = None):
        """Log an exception-level message."""
        self.log_struct(
            "exception_message",
            LogLevel.ERROR,
            task=task,
            message=message,
            exception=True,
        )

    def _flush_buffer(self):
        """
        Flush pre-setup log buffer.
        """
        if not self._pre_setup_buffer:
            return
        for level, msg, task, exc in self._pre_setup_buffer:
            self._emit(level, msg, task, exc)
        self._pre_setup_buffer.clear()

    def _log_with_level(
        self,
        level: str,
        message: str,
        task: str | None = None,
        exc_info: bool | None = None,
    ):
        """
        Log a message with the specified level.
        """
        if not getattr(self, "_initialized", False):
            # Buffer until initialized
            self._pre_setup_buffer.append((level, message, task, exc_info))
            return
        self._emit(level, message, task, exc_info)

    def _augment(self, message: str) -> str:
        """Prefix message with correlation id if not already prefixed."""
        if message.startswith("[CID "):
            return message
        return f"[CID {self.correlation_id}] {message}"

    def _emit(self, level: str, message: str, task: str | None, exc_info: bool | None):
        logger = self._get_logger(task) if task else self._main_logger
        log_method = getattr(logger, level, logger.info)
        log_method(self._augment(message), exc_info=exc_info)

    def log_main(self, message: str, level: int | LogLevel = LogLevel.INFO):
        """Log a framework-level event to the main program log.

        Legacy method - delegates to structured logging.
        """
        self.log_struct("main_event", level, task=None, message=message)

    def _get_logger(self, task_name: str) -> logging.Logger:
        """Get or create a logger for a specific task within the execution folder.

        If the same task runs multiple times, files will be suffixed with
        an incrementing counter: task.log, task_2.log, task_3.log, ...
        """
        with self._lock:
            if task_name not in self._loggers:
                # Determine unique filename for this task in this execution
                count = self._task_counts.get(task_name, 0) + 1
                self._task_counts[task_name] = count
                base_name = (
                    f"{task_name}.log" if count == 1 else f"{task_name}_{count}.log"
                )
                log_file = os.path.join(self.execution_dir, base_name)
                self._files[task_name] = log_file

                # Create logger
                logger = logging.getLogger(f"task_{task_name}")
                logger.setLevel(logging.DEBUG)

                # Remove any existing handlers
                logger.handlers.clear()

                # Create file handler with rotation (5MB max, keep 10 rotated files per task)
                file_handler = RotatingFileHandler(
                    log_file,
                    maxBytes=self.max_bytes_task,
                    backupCount=self.max_log_file_count_task,
                    encoding="utf-8",
                )
                file_handler.setLevel(logging.DEBUG)

                # JSONL format - just the raw JSON object per line, no timestamp prefix
                formatter = logging.Formatter("%(message)s")
                file_handler.setFormatter(formatter)

                # Add handler to logger
                logger.addHandler(file_handler)

                self._loggers[task_name] = logger

            return self._loggers[task_name]

    def log_output(self, task_name: str, line: str, is_error: bool = False):
        """Structured log for raw output line.

        Disabled: Raw output logging creates noise and makes JSON output look like garbage.
        Parsers extract meaningful events; raw text is preserved in stdout/stderr buffers.
        """

    def log_event(
        self, task_name: str, message: str, level: int | LogLevel = LogLevel.INFO
    ):
        """Backward-compatible wrapper -> structured event."""
        self.log_struct("task_event", level, task=task_name, message=message)

    def get_log_file(self, task_name: str) -> str | None:
        """Get the log file path for a task."""
        with self._lock:
            return self._files.get(task_name)

    def close_task_logger(self, task_name: str):
        """Close and cleanup logger for a specific task."""
        with self._lock:
            self._close_task_logger_unlocked(task_name)

    def _close_task_logger_unlocked(self, task_name: str):
        """Internal: close task logger without acquiring lock (caller must hold lock)."""
        if task_name in self._loggers:
            logger = self._loggers[task_name]
            # Close all handlers
            for handler in logger.handlers:
                handler.close()
            # Keep logger in dict to prevent creating duplicate files
            # when subsequent logs (like command_result) are written after command completes

    def close_all(self):
        """Close all loggers."""
        with self._lock:
            for task_name in list(self._loggers.keys()):
                self._close_task_logger_unlocked(
                    task_name
                )  # Use unlocked version - we already hold the lock

            # Close main logger
            if hasattr(self, "_main_logger"):
                for handler in self._main_logger.handlers:
                    handler.close()

    def get_main_log_file(self) -> str | None:
        """Get the main program log file path."""
        return getattr(self, "_main_log_file", None)

    def get_execution_dir(self) -> str:
        """Get the per-execution log directory path."""
        return self.execution_dir

    def get_correlation_id(self) -> str:
        """Return correlation id for this execution."""
        return self.correlation_id

    def set_display_backend(self, backend) -> None:
        """Set display backend for routing console output.

        Args:
            backend: DisplayBackend instance or None to disable
        """
        self._display_backend = backend

    def console_print(self, message: str, level: str = "INFO") -> None:
        """Print message to console, routing through display backend if available.

        Args:
            message: Message to display
            level: Log level (DEBUG/INFO/WARNING/ERROR)
        """
        if self._display_backend:
            try:
                self._display_backend.add_log_line(message, level)
            except Exception as e:  # pylint: disable=broad-except
                # Fallback to direct print if backend fails
                print(message, file=sys.stderr)  # allow-print
                self.log_error(
                    "display_backend_error",
                    error=str(e),
                    error_type=type(e).__name__,
                    message="Failed to route console output through display backend",
                )
        else:
            # No backend - direct print
            print(message)  # allow-print

    # Structured logging API
    def log_struct(
        self,
        event: str,
        level: int | LogLevel = LogLevel.INFO,
        scope: LogScope | None = None,
        task: str | None = None,
        exception: bool = False,
        **data,
    ) -> None:
        """Emit a structured JSON log line with standard fields.

        Fields:
            ts: UTC timestamp ISO8601 ms
            cid: correlation id
            level: level name
            event: semantic event name
            task: optional task name
            data: event-specific key-values
        """
        # Infer scope when not explicitly provided for backward compatibility
        if scope is None:
            if task is None:
                # No task field provided: treat as framework/task lifecycle depending on event name
                if event.startswith("task_"):
                    scope = LogScope.TASK
                else:
                    scope = LogScope.FRAMEWORK
            else:
                # Has task field -> command-scoped unless event explicitly task lifecycle
                if event.startswith("task_"):
                    scope = LogScope.TASK
                else:
                    scope = LogScope.COMMAND

        payload: dict[str, object] = {
            "ts": datetime.utcnow().isoformat(timespec="milliseconds") + "Z",
            "cid": self.correlation_id,
            "level": logging.getLevelName(level),
            "event": event,
            "scope": scope.value,
        }
        if task:
            payload["task"] = task
        if data:
            # Normalize and ensure JSON-serializable values
            safe_data: dict[str, object] = {}
            for k, v in data.items():
                if isinstance(v, str):
                    # Normalize path separators for readability/cross-platform consistency
                    if "\\" in v:
                        v = v.replace("\\", "/")
                    safe_data[k] = v
                elif isinstance(v, (int, float, bool)) or v is None:
                    safe_data[k] = v
                else:
                    safe_data[k] = repr(v)
            payload["data"] = safe_data
        message = json.dumps(payload, ensure_ascii=False)
        # Routing based on semantic scope:
        #  - FRAMEWORK/TASK -> main logger
        #  - COMMAND -> per-command logger (requires task id)
        if scope == LogScope.COMMAND and task:
            logger = self._get_logger(task)
        else:
            logger = self._main_logger

        # Normalize to a safe standard level for the logging API to avoid cache KeyError
        # while preserving the intended textual level in the JSON payload above.
        if isinstance(level, int):
            if level <= logging.DEBUG:
                safe_level = logging.DEBUG
            elif level <= logging.INFO:
                safe_level = logging.INFO
            elif level <= logging.WARNING:
                safe_level = logging.WARNING
            elif level <= logging.ERROR:
                safe_level = logging.ERROR
            else:
                safe_level = logging.CRITICAL
        else:
            safe_level = logging.INFO

        try:
            logger.log(safe_level, message, exc_info=exception)
        except Exception:  # pylint: disable=broad-except
            # Workaround for rare logging cache KeyError on certain Python builds.
            # Fallback to emitting a record at INFO level; the JSON contains the intended level.
            try:
                record = logging.LogRecord(
                    name=logger.name,
                    level=logging.INFO,
                    pathname="",
                    lineno=0,
                    msg=message,
                    args=(),
                    exc_info=None,
                )
                logger.handle(record)
            except Exception:  # pylint: disable=broad-except
                # As a last resort, write to stderr to avoid crashing the runner
                try:
                    print(
                        message, file=sys.stderr
                    )  # allow-print (last resort fallback)
                except Exception:  # pylint: disable=broad-except
                    pass

        # Route key lifecycle events to display backend log panel for visibility
        if self._display_backend:
            try:
                if (
                    scope in (LogScope.TASK, LogScope.COMMAND)
                    and logging.getLevelName(level) == "INFO"
                ):
                    lifecycle_events = {
                        "task_started",
                        "task_completed",
                        "command_started",
                        "command_succeeded",
                        "command_failed",
                    }
                    if event in lifecycle_events:
                        human_msg = None
                        if event == "task_started":
                            human_msg = (
                                data.get("message")
                                or f"Starting {data.get('task_name') or task}"
                                if (data.get("task_name") or task)
                                else "Task starting"
                            )
                        elif event == "task_completed":
                            human_msg = (
                                data.get("message")
                                or f"Task {data.get('task_name') or task} completed"
                            )
                        elif event == "command_started":
                            human_msg = f"Started {data.get('command')}"
                        elif event == "command_succeeded":
                            dur = data.get("duration_seconds")
                            human_msg = f"{data.get('command')} succeeded" + (
                                f" ({dur}s)" if dur else ""
                            )
                        elif event == "command_failed":
                            err = data.get("error")
                            human_msg = f"{data.get('command')} failed" + (
                                f": {err}" if err else ""
                            )
                        if human_msg:
                            self._display_backend.add_log_line(
                                human_msg, logging.getLevelName(level)
                            )
            except Exception:  # pylint: disable=broad-except
                pass

    def log_struct_level_str(
        self,
        event: str,
        level_str: str = "INFO",
        scope: LogScope | None = None,
        task: str | None = None,
        exception: bool = False,
        **data,
    ) -> None:
        """Emit a structured JSON log line with level specified as string.

        Args:
            event: semantic event name
            level_str: log level as string (e.g., "INFO", "WARNING", "ERROR")
            task: optional task name
            exception: whether to include exception info
            **data: event-specific key-values
        """
        level = getattr(logging, level_str.upper(), logging.INFO)
        self.log_struct(event, level, scope, task, exception, **data)

    # Convenience methods for common log levels
    def log_debug(self, event: str, task: str | None = None, **data) -> None:
        """Log a debug-level structured event."""
        self.log_struct_level_str(event, "DEBUG", task=task, **data)

    def log_info(self, event: str, task: str | None = None, **data) -> None:
        """Log an info-level structured event."""
        self.log_struct_level_str(event, "INFO", task=task, **data)

    def log_warning(self, event: str, task: str | None = None, **data) -> None:
        """Log a warning-level structured event."""
        self.log_struct_level_str(event, "WARNING", task=task, **data)

    def log_error(
        self, event: str, task: str | None = None, exception: bool = False, **data
    ) -> None:
        """Log an error-level structured event."""
        self.log_struct_level_str(
            event, "ERROR", task=task, exception=exception, **data
        )

    # Explicit scoped helper methods
    def log_framework(
        self, event: str, level: int | LogLevel = LogLevel.INFO, **data
    ) -> None:
        """Log a framework-scoped event (main log only)."""
        self.log_struct(event, level, scope=LogScope.FRAMEWORK, **data)

    def log_task(
        self, event: str, task_name: str, level: int | LogLevel = LogLevel.INFO, **data
    ) -> None:
        """Log a task lifecycle event (main log only)."""
        # Keep task name in data (not routing key) to avoid creating separate file.
        if "task_name" not in data:
            data["task_name"] = task_name
        self.log_struct(event, level, scope=LogScope.TASK, **data)

    def log_command(
        self,
        event: str,
        command_name: str,
        level: int | LogLevel = LogLevel.INFO,
        **data,
    ) -> None:
        """Log a command-scoped event (routes to per-command log)."""
        self.log_struct(event, level, scope=LogScope.COMMAND, task=command_name, **data)

    def get_all_task_log_files(self) -> dict[str, str]:
        """Return a copy of the mapping of task_name -> latest log file path."""
        with self._lock:
            return dict(self._files)

    def print_task_status_summary(self, results: list):
        """Print a user-friendly summary of all task results to the console."""
        if not self.enable_console:
            return
        self.console_print("\nTask Status Summary:")
        self.console_print("-" * 60)
        for result in results:
            icon = "✅" if result.success else "❌"
            status = "SUCCESS" if result.success else "FAILED"
            duration = f"{result.duration:.2f}s" if result.duration else "N/A"
            reason = ""
            if not result.success:
                if result.error:
                    reason = f"Reason: {str(result.error)}"
                elif result.stderr:
                    reason = f"Reason: {result.stderr.strip()}"
            self.console_print(
                f"{icon} {result.task_name:20} {status:7} [Exit: {result.return_code}] [Duration: {duration}] {reason}"
            )
        self.console_print("-" * 60)
        # Also log a structured summary event
        self.log_struct(
            "task_status_summary",
            LogLevel.INFO,
            data={
                "summary": [
                    {
                        "task": r.task_name,
                        "success": r.success,
                        "return_code": r.return_code,
                        "duration": r.duration,
                        "error": (
                            str(r.error)
                            if r.error
                            else (r.stderr.strip() if r.stderr else None)
                        ),
                    }
                    for r in results
                ]
            },
        )
