"""
Structured logging with timeline event collection and correlation IDs.

Usage:
    from sondeo import log

    @log
    def my_function(x):
        log(f"inside call with x={x}")
        return x * 2

    my_function(21)
"""

from __future__ import annotations

import functools
import inspect
import time
import uuid
from collections.abc import Callable, Generator, Iterable, Mapping
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import Any, Literal, TypeVar, overload

F = TypeVar("F", bound=Callable[..., Any])
LogLevel = Literal["debug", "info", "warn", "error"]

_LEVEL_ALIASES: dict[str, LogLevel] = {
    "debug": "debug",
    "info": "info",
    "warn": "warn",
    "warning": "warn",
    "error": "error",
}

_LEVEL_COLOR: dict[LogLevel, str] = {
    "debug": "\033[36m",  # cyan
    "info": "\033[32m",  # green
    "warn": "\033[33m",  # yellow
    "error": "\033[31m",  # red
}

_RESET = "\033[0m"


@dataclass(slots=True)
class TimelineEvent:
    """A single timeline event interleaving profiling and logs."""

    timestamp: float
    event_type: str
    level: LogLevel
    module: str
    message: str
    correlation_id: str | None = None
    data: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        payload: dict[str, Any] = {
            "timestamp": round(self.timestamp, 6),
            "event_type": self.event_type,
            "level": self.level,
            "module": self.module,
            "message": self.message,
            "correlation_id": self.correlation_id,
        }
        if self.data:
            payload["data"] = dict(self.data)
        return payload


_events: list[TimelineEvent] = []
_correlation_stack: ContextVar[tuple[str, ...]] = ContextVar("sondeo_correlation_stack", default=())


def normalize_level(level: str) -> LogLevel:
    """Normalize a log level to Sondeo's canonical level names."""
    normalized = _LEVEL_ALIASES.get(level.strip().lower())
    if normalized is None:
        raise ValueError(f"Invalid log level: {level!r}. Use debug, info, warn, or error.")
    return normalized


def colorize_level(level: str, *, enabled: bool = True) -> str:
    """Return an uppercased level label with optional ANSI color."""
    normalized = normalize_level(level)
    label = normalized.upper()
    if not enabled:
        return label
    return f"{_LEVEL_COLOR[normalized]}{label}{_RESET}"


def _safe_repr(value: Any, *, max_len: int = 160) -> str:
    text = repr(value)
    if len(text) <= max_len:
        return text
    return text[: max_len - 3] + "..."


def _infer_module_name(stack_depth: int = 2) -> str:
    frame = inspect.currentframe()
    try:
        for _ in range(stack_depth):
            if frame is None:
                break
            frame = frame.f_back
        if frame is None:
            return "__main__"
        module_name = frame.f_globals.get("__name__")
        if isinstance(module_name, str):
            return module_name
        return "__main__"
    finally:
        del frame


def current_correlation_id() -> str | None:
    """Return the current correlation id from context, if one is set."""
    stack = _correlation_stack.get()
    if not stack:
        return None
    return stack[-1]


@contextmanager
def correlation_scope(correlation_id: str | None = None) -> Generator[str, None, None]:
    """Push a correlation id for the current execution scope."""
    cid = correlation_id or uuid.uuid4().hex[:12]
    stack = _correlation_stack.get()
    token = _correlation_stack.set(stack + (cid,))
    try:
        yield cid
    finally:
        _correlation_stack.reset(token)


def emit_event(
    *,
    event_type: str,
    message: str,
    level: str = "info",
    module: str | None = None,
    correlation_id: str | None = None,
    data: Mapping[str, Any] | None = None,
) -> TimelineEvent:
    """Emit a timeline event and store it globally."""
    event = TimelineEvent(
        timestamp=time.time(),
        event_type=event_type,
        level=normalize_level(level),
        module=module or _infer_module_name(stack_depth=2),
        message=message,
        correlation_id=correlation_id or current_correlation_id(),
        data=dict(data) if data is not None else {},
    )
    _events.append(event)
    return event


def get_events(
    *,
    include_modules: Iterable[str] | None = None,
    exclude_modules: Iterable[str] | None = None,
    levels: Iterable[str] | None = None,
    pattern: str | None = None,
) -> list[TimelineEvent]:
    """Get timeline events with optional filters."""
    filtered = list(_events)

    include_prefixes = tuple(include_modules or ())
    if include_prefixes:
        filtered = [
            event
            for event in filtered
            if any(event.module.startswith(prefix) for prefix in include_prefixes)
        ]

    exclude_prefixes = tuple(exclude_modules or ())
    if exclude_prefixes:
        filtered = [
            event
            for event in filtered
            if not any(event.module.startswith(prefix) for prefix in exclude_prefixes)
        ]

    if levels is not None:
        allowed = {normalize_level(level) for level in levels}
        filtered = [event for event in filtered if event.level in allowed]

    if pattern is not None:
        needle = pattern.lower()
        filtered = [
            event
            for event in filtered
            if needle in event.message.lower() or needle in event.module.lower()
        ]

    filtered.sort(key=lambda event: event.timestamp)
    return filtered


def clear_events() -> None:
    """Clear all timeline events."""
    _events.clear()


def _decorate_log_calls(
    *,
    level: str = "info",
    capture_args: bool = True,
    capture_return: bool = True,
    name: str | None = None,
) -> Callable[[F], F]:
    normalized_level = normalize_level(level)

    def decorator(func: F) -> F:
        display_name = name or func.__qualname__

        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            parent_cid = current_correlation_id()
            with correlation_scope() as cid:
                enter_payload: dict[str, Any] = {
                    "phase": "enter",
                    "function": func.__qualname__,
                }
                if parent_cid is not None:
                    enter_payload["parent_correlation_id"] = parent_cid
                if capture_args:
                    enter_payload["args"] = [_safe_repr(arg) for arg in args]
                    enter_payload["kwargs"] = {
                        key: _safe_repr(value) for key, value in kwargs.items()
                    }

                emit_event(
                    event_type="log",
                    level=normalized_level,
                    module=func.__module__,
                    message=f"{display_name} enter",
                    correlation_id=cid,
                    data=enter_payload,
                )

                try:
                    result = func(*args, **kwargs)
                except Exception as exc:
                    emit_event(
                        event_type="log",
                        level="error",
                        module=func.__module__,
                        message=f"{display_name} error",
                        correlation_id=cid,
                        data={
                            "phase": "error",
                            "function": func.__qualname__,
                            "error": f"{exc.__class__.__name__}: {exc}",
                        },
                    )
                    raise

                exit_payload: dict[str, Any] = {
                    "phase": "exit",
                    "function": func.__qualname__,
                }
                if parent_cid is not None:
                    exit_payload["parent_correlation_id"] = parent_cid
                if capture_return:
                    exit_payload["return"] = _safe_repr(result)

                emit_event(
                    event_type="log",
                    level=normalized_level,
                    module=func.__module__,
                    message=f"{display_name} exit",
                    correlation_id=cid,
                    data=exit_payload,
                )

                return result

        return wrapper  # type: ignore[return-value]

    return decorator


def _emit_log_message(
    message: str,
    *,
    level: str = "info",
    module: str | None = None,
    correlation_id: str | None = None,
) -> TimelineEvent:
    return emit_event(
        event_type="log",
        message=message,
        level=level,
        module=module,
        correlation_id=correlation_id,
    )


@overload
def log(func: F, /) -> F: ...


@overload
def log(
    func: None = None,
    /,
    *,
    level: str = "info",
    capture_args: bool = True,
    capture_return: bool = True,
    name: str | None = None,
) -> Callable[[F], F]: ...


@overload
def log(
    message: str,
    /,
    *messages: str,
    level: str = "info",
    module: str | None = None,
    correlation_id: str | None = None,
) -> TimelineEvent: ...


def log(
    target: F | object | None = None,
    /,
    *messages: object,
    level: str = "info",
    capture_args: bool = True,
    capture_return: bool = True,
    name: str | None = None,
    module: str | None = None,
    correlation_id: str | None = None,
) -> F | Callable[[F], F] | TimelineEvent:
    """Decorator + logger API.

    - `@log` / `@log(...)`: decorate function calls and capture enter/exit events.
    - `log("message")`: emit a timeline log event.
    """
    if callable(target) and not messages and module is None and correlation_id is None:
        return _decorate_log_calls(
            level=level,
            capture_args=capture_args,
            capture_return=capture_return,
            name=name,
        )(target)

    if target is None and not messages and module is None and correlation_id is None:
        return _decorate_log_calls(
            level=level,
            capture_args=capture_args,
            capture_return=capture_return,
            name=name,
        )

    parts: list[object] = []
    if target is not None:
        parts.append(target)
    parts.extend(messages)
    message = " ".join(str(part) for part in parts)

    return _emit_log_message(
        message=message,
        level=level,
        module=module,
        correlation_id=correlation_id,
    )


__all__ = [
    "TimelineEvent",
    "log",
    "emit_event",
    "get_events",
    "clear_events",
    "normalize_level",
    "colorize_level",
    "current_correlation_id",
    "correlation_scope",
]
