from __future__ import annotations

import logging
import time
import warnings
from abc import ABC, abstractmethod
from collections.abc import Mapping
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any, Protocol

if TYPE_CHECKING:
    from synapse_sdk.plugins.models.logger import LogLevel

# Module-level logger for SDK output
_logger = logging.getLogger('synapse_sdk.loggers')

# LogLevel string value → Python logging level mapping
_LOG_LEVEL_MAP: dict[str, int] = {
    'debug': logging.DEBUG,
    'info': logging.INFO,
    'warning': logging.WARNING,
    'error': logging.ERROR,
    'critical': logging.CRITICAL,
}


class LoggerBackend(Protocol):
    """Protocol for logger backends that handle data synchronization."""

    def publish_progress(self, job_id: str, progress: 'ProgressData') -> None: ...
    def publish_metrics(self, job_id: str, metrics: dict[str, Any]) -> None: ...
    def publish_log(self, job_id: str, log_entry: 'LogEntry') -> None: ...


@dataclass
class ProgressData:
    """Immutable progress data snapshot."""

    percent: float
    time_remaining: float | None = None
    elapsed_time: float | None = None
    status: str = 'running'


@dataclass
class LogEntry:
    """Single log entry."""

    event: str
    data: dict[str, Any]
    timestamp: float = field(default_factory=time.time)
    file: str | None = None
    step: str | None = None  # 신규: 로그가 발생한 step
    level: Any = None  # 신규: 로그 레벨 (LogLevel enum, 순환 import 방지를 위해 Any)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for API serialization."""
        return {
            'event': self.event,
            'data': self.data,
            'timestamp': self.timestamp,
            'file': self.file,
            'step': self.step,
            'level': self.level.value if self.level else None,
        }


class BaseLogger(ABC):
    """Base class for logging progress, metrics, and events.

    All state is instance-level to prevent cross-instance contamination.
    Uses composition over inheritance for backend communication.
    """

    _start_time: float
    _progress: dict[str, ProgressData]
    _metrics: dict[str, dict[str, Any]]
    _category_start_times: dict[str, float]
    _current_step: str | None
    _is_finished: bool

    def __init__(self) -> None:
        self._start_time = time.monotonic()
        self._progress = {}
        self._metrics = {}
        self._category_start_times = {}
        self._current_step = None
        self._is_finished = False

    def set_step(self, step: str | None) -> None:
        """Set the current step for logging context.

        Args:
            step: The step name, or None to clear.
        """
        self._current_step = step

    def get_step(self) -> str | None:
        """Get the current step.

        Returns:
            The current step name, or None if not set.
        """
        return self._current_step

    def _raise_if_finished(self) -> None:
        if self._is_finished:
            raise RuntimeError('Cannot log to a finished logger')

    def log(
        self,
        level: LogLevel,
        event: str,
        data: dict[str, Any],
        file: str | None = None,
        step: str | None = None,
    ) -> None:
        """Log an event with data.

        Args:
            level: Log level (LogLevel enum).
            event: Event name/type.
            data: Dictionary of event data.
            file: Optional file path associated with the event.
            step: Optional step name. Uses current step if not specified.

        Raises:
            TypeError: If level is not a LogLevel enum or data is not a dictionary.
            RuntimeError: If logger is already finished.
        """
        self._raise_if_finished()

        # Validate level is a LogLevel enum (duck typing check to avoid circular import)
        if not (isinstance(level, Enum) and hasattr(level, 'value') and level.value in _LOG_LEVEL_MAP):
            raise TypeError(f'level must be a LogLevel enum, got {type(level).__name__}')

        if not isinstance(data, Mapping):
            raise TypeError(f'data must be a dict, got {type(data).__name__}')

        data = dict(data)  # Copy to avoid mutating input
        # Use explicit step or fall back to current step
        effective_step = step if step is not None else self._current_step
        self._log_impl(event, data, file, effective_step, level)

    def info(self, message: str) -> None:
        """Log an info message."""
        from synapse_sdk.plugins.models.logger import LogLevel

        self.log(LogLevel.INFO, 'info', {'message': message})

    def debug(self, message: str) -> None:
        """Log a debug message."""
        from synapse_sdk.plugins.models.logger import LogLevel

        self.log(LogLevel.DEBUG, 'debug', {'message': message})

    def warning(self, message: str) -> None:
        """Log a warning message."""
        from synapse_sdk.plugins.models.logger import LogLevel

        self.log(LogLevel.WARNING, 'warning', {'message': message})

    def error(self, message: str) -> None:
        """Log an error message."""
        from synapse_sdk.plugins.models.logger import LogLevel

        self.log(LogLevel.ERROR, 'error', {'message': message})

    def critical(self, message: str) -> None:
        """Log a critical message."""
        from synapse_sdk.plugins.models.logger import LogLevel

        self.log(LogLevel.CRITICAL, 'critical', {'message': message})

    def set_progress(
        self,
        current: int,
        total: int,
        step: str | None = None,
        category: str | None = None,
    ) -> None:
        """Set progress for the current operation.

        Args:
            current: Current progress value (0 to total).
            total: Total progress value.
            step: Optional step name. Uses current step if not specified.
            category: Deprecated. Use step instead.

        Raises:
            ValueError: If current/total values are invalid.
            RuntimeError: If logger is already finished.
        """
        self._raise_if_finished()

        if total <= 0:
            raise ValueError(f'total must be > 0, got {total}')
        if not 0 <= current <= total:
            raise ValueError(f'current must be between 0 and {total}, got {current}')

        # Priority: explicit step > current step > category (deprecated)
        effective_key: str | None = None
        if step is not None:
            effective_key = step
            if category is not None:
                warnings.warn(
                    "The 'category' parameter is deprecated. Use 'step' instead. "
                    "'step' takes precedence when both are provided.",
                    DeprecationWarning,
                    stacklevel=2,
                )
        elif self._current_step is not None:
            # Use current step if available
            effective_key = self._current_step
            if category is not None:
                warnings.warn(
                    "The 'category' parameter is deprecated. Use 'step' instead. "
                    'Current step takes precedence over category.',
                    DeprecationWarning,
                    stacklevel=2,
                )
        elif category is not None:
            warnings.warn(
                "The 'category' parameter is deprecated. Use 'step' instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            effective_key = category

        key = effective_key or '__default__'
        now = time.monotonic()

        # Initialize start time on first call for this category
        if key not in self._category_start_times or current == 0:
            self._category_start_times[key] = now

        elapsed = now - self._category_start_times[key]
        percent = round((current / total) * 100, 2)

        # Calculate time remaining
        time_remaining = None
        if current > 0:
            rate = elapsed / current
            time_remaining = round(rate * (total - current), 2)

        progress = ProgressData(
            percent=percent,
            time_remaining=time_remaining,
            elapsed_time=round(elapsed, 2),
        )

        self._progress[key] = progress
        self._on_progress(progress, effective_key)

    def set_progress_failed(self, category: str | None = None) -> None:
        """Mark progress as failed.

        Args:
            category: Optional category name.

        Raises:
            RuntimeError: If logger is already finished.
        """
        self._raise_if_finished()

        key = category or '__default__'
        elapsed = None

        if key in self._category_start_times:
            elapsed = round(time.monotonic() - self._category_start_times[key], 2)

        progress = ProgressData(
            percent=0.0,
            time_remaining=None,
            elapsed_time=elapsed,
            status='failed',
        )

        self._progress[key] = progress
        self._on_progress(progress, category)

    def set_metrics(
        self,
        value: dict[str, Any],
        step: str | None = None,
        category: str | None = None,
    ) -> None:
        """Set metrics for a step.

        Args:
            value: Dictionary of metric values.
            step: Optional step name. Uses current step if not specified.
            category: Deprecated. Use step instead.

        Raises:
            ValueError: If no step/category is available.
            TypeError: If value is not a dictionary.
            RuntimeError: If logger is already finished.
        """
        self._raise_if_finished()

        if not isinstance(value, Mapping):
            raise TypeError(f'value must be a dict, got {type(value).__name__}')

        # Priority: explicit step > current step > category (deprecated)
        effective_key: str | None = None
        if step is not None:
            effective_key = step
            if category is not None:
                warnings.warn(
                    "The 'category' parameter is deprecated. Use 'step' instead. "
                    "'step' takes precedence when both are provided.",
                    DeprecationWarning,
                    stacklevel=2,
                )
        elif self._current_step is not None:
            # Use current step if available
            effective_key = self._current_step
            if category is not None:
                warnings.warn(
                    "The 'category' parameter is deprecated. Use 'step' instead. "
                    'Current step takes precedence over category.',
                    DeprecationWarning,
                    stacklevel=2,
                )
        elif category is not None:
            warnings.warn(
                "The 'category' parameter is deprecated. Use 'step' instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            effective_key = category

        if not effective_key:
            raise ValueError('step must be specified or set via set_step()')

        data = dict(value)  # Copy

        if effective_key not in self._metrics:
            self._metrics[effective_key] = {}
        self._metrics[effective_key].update(data)

        self._on_metrics(effective_key, self._metrics[effective_key])

    def get_progress(self, category: str | None = None) -> ProgressData | None:
        """Get progress for a category."""
        key = category or '__default__'
        return self._progress.get(key)

    def get_metrics(self, category: str | None = None) -> dict[str, Any]:
        """Get metrics, optionally filtered by category."""
        if category:
            return dict(self._metrics.get(category, {}))
        return {k: dict(v) for k, v in self._metrics.items()}

    def finish(self) -> None:
        """Mark the logger as finished. No further logging is allowed."""
        self._is_finished = True
        self._on_finish()

    @abstractmethod
    def _log_impl(
        self,
        event: str,
        data: dict[str, Any],
        file: str | None,
        step: str | None,
        level: LogLevel | None = None,
    ) -> None:
        """Implementation-specific log handling."""
        ...

    def _on_progress(self, progress: ProgressData, category: str | None) -> None:
        """Hook called when progress is updated. Override in subclasses."""
        pass

    def _on_metrics(self, category: str, metrics: dict[str, Any]) -> None:
        """Hook called when metrics are updated. Override in subclasses."""
        pass

    def _on_finish(self) -> None:
        """Hook called when logger is finished. Override in subclasses."""
        pass


class ConsoleLogger(BaseLogger):
    """Logger that prints to console using Python logging module."""

    def _log_impl(
        self,
        event: str,
        data: dict[str, Any],
        file: str | None,
        step: str | None,
        level: LogLevel | None = None,
    ) -> None:
        prefix = f'[{step}] ' if step else ''
        level_str = level.value.upper() if level else 'INFO'
        message = f'{level_str}: {prefix}{event} {data}'

        # Use print with flush=True for immediate output
        # This ensures logs are visible in Ray remote workers
        print(message, flush=True)

    def _on_progress(self, progress: ProgressData, category: str | None) -> None:
        prefix = f'[{category}] ' if category else ''
        print(f'INFO: {prefix}Progress: {progress.percent}% | ETA: {progress.time_remaining}s', flush=True)

    def _on_metrics(self, category: str, metrics: dict[str, Any]) -> None:
        print(f'INFO: [{category}] Metrics: {metrics}', flush=True)


class BackendLogger(BaseLogger):
    """Logger that syncs with a remote backend.

    Uses a backend interface for decoupled communication.
    """

    _backend: LoggerBackend | None
    _job_id: str
    _log_queue: list[LogEntry]

    def __init__(self, backend: LoggerBackend | None, job_id: str) -> None:
        super().__init__()
        self._backend = backend
        self._job_id = job_id
        self._log_queue = []

    def _log_impl(
        self,
        event: str,
        data: dict[str, Any],
        file: str | None,
        step: str | None,
        level: LogLevel | None = None,
    ) -> None:
        entry = LogEntry(event=event, data=data, file=file, step=step, level=level)
        self._log_queue.append(entry)
        self._flush_logs()

    def _on_progress(self, progress: ProgressData, category: str | None) -> None:
        if self._backend is None:
            return

        try:
            self._backend.publish_progress(self._job_id, progress)
        except Exception as e:
            _logger.error(f'Failed to publish progress: {e}')

    def _on_metrics(self, category: str, metrics: dict[str, Any]) -> None:
        if self._backend is None:
            return

        try:
            self._backend.publish_metrics(self._job_id, {category: metrics})
        except Exception as e:
            _logger.error(f'Failed to publish metrics: {e}')

    def _flush_logs(self) -> None:
        if self._backend is None or not self._log_queue:
            return

        try:
            for entry in self._log_queue:
                self._backend.publish_log(self._job_id, entry)
            self._log_queue.clear()
        except Exception as e:
            _logger.error(f'Failed to flush logs: {e}')

    def _on_finish(self) -> None:
        self._flush_logs()


class NoOpLogger(BaseLogger):
    """Logger that does nothing. Useful for testing or disabled logging."""

    def _log_impl(
        self,
        event: str,
        data: dict[str, Any],
        file: str | None,
        step: str | None,
        level: LogLevel | None = None,
    ) -> None:
        pass


__all__ = [
    'BaseLogger',
    'BackendLogger',
    'ConsoleLogger',
    'LogEntry',
    'LoggerBackend',
    'NoOpLogger',
    'ProgressData',
]
