"""Core logger implementation."""

import functools
import inspect
import sys
import threading
import time
from datetime import datetime, timezone
from typing import Any, Callable, TextIO

from snaplog.context import get_context
from snaplog.formatter import JsonFormatter, HumanFormatter


_LEVELS = {"debug": 10, "info": 20, "warning": 30, "error": 40, "critical": 50, "trace": 15}

_loggers: dict[str, "Logger"] = {}
_loggers_lock = threading.Lock()


class Logger:
    """Structured logger with zero config.

    Args:
        name: Logger name (appears in output as "logger" field).
        min_level: Minimum log level. Default: "debug".
        formatter: Output formatter. Default: JsonFormatter().
        output: Output destination. Default: sys.stderr.
        extra: Static extra fields included in every log entry.
    """

    def __init__(
        self,
        name: str = "root",
        min_level: str = "debug",
        formatter: Any = None,
        output: TextIO | Callable[[str], None] | None = None,
        extra: dict[str, Any] | None = None,
    ):
        if min_level not in _LEVELS:
            raise ValueError(f"min_level must be one of {list(_LEVELS.keys())}")
        self.name = name
        self.min_level_num = _LEVELS[min_level]
        self.min_level = min_level
        self.formatter = formatter or JsonFormatter()
        self.output = output or sys.stderr
        self.extra = extra or {}
        self._lock = threading.Lock()

    def _should_log(self, level: str) -> bool:
        return _LEVELS.get(level, 0) >= self.min_level_num

    def _log(self, level: str, msg: str, *, _skip_frames: int = 1, **kwargs) -> None:
        if not self._should_log(level):
            return

        # Build record
        record = {
            "ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
            "level": level,
            "msg": msg,
            "logger": self.name,
        }

        # Auto-capture caller info
        frame = inspect.currentframe()
        try:
            for _ in range(_skip_frames + 1):
                if frame is None:
                    break
                frame = frame.f_back
            if frame is not None:
                record["func"] = frame.f_code.co_name
                fname = frame.f_code.co_filename
                record["file"] = fname.split("/")[-1] if "/" in fname else fname
                record["line"] = frame.f_lineno
        finally:
            del frame

        # Add context
        ctx = get_context()
        if ctx:
            record.update(ctx)

        # Add static extra
        if self.extra:
            record.update(self.extra)

        # Add user fields
        record.update(kwargs)

        # Format and output
        line = self.formatter.format(record)
        with self._lock:
            if callable(self.output) and not hasattr(self.output, "write"):
                self.output(line)
            else:
                try:
                    self.output.write(line + "\n")
                except Exception:
                    pass

    def debug(self, msg: str, **kwargs) -> None:
        self._log("debug", msg, **kwargs)

    def info(self, msg: str, **kwargs) -> None:
        self._log("info", msg, **kwargs)

    def warning(self, msg: str, **kwargs) -> None:
        self._log("warning", msg, **kwargs)

    def warn(self, msg: str, **kwargs) -> None:
        self.warning(msg, **kwargs)

    def error(self, msg: str, **kwargs) -> None:
        self._log("error", msg, **kwargs)

    def critical(self, msg: str, **kwargs) -> None:
        self._log("critical", msg, **kwargs)

    def trace(self, msg: str, **kwargs) -> None:
        self._log("trace", msg, **kwargs)

    def trace_fn(self, func: Callable | None = None, *, log_args: bool = True,
                  log_result: bool = True, log_error: bool = True) -> Callable:
        """Decorator for automatic function tracing.

        Logs entry, exit (with timing), and optional error info.

        Args:
            log_args: Include function arguments in trace log.
            log_result: Include return value in trace log.
            log_error: Include exception info if function raises.
        """
        def decorator(fn: Callable) -> Callable:
            @functools.wraps(fn)
            def wrapper(*args, **kwargs):
                fname = fn.__name__
                extra = {"func": fname}
                if log_args:
                    # Don't log 'self' for methods
                    sig = inspect.signature(fn)
                    params = list(sig.parameters.keys())
                    arg_vals = {}
                    for i, val in enumerate(args):
                        if i < len(params) and params[i] != "self":
                            arg_vals[params[i]] = _safe_repr(val)
                    for k, v in kwargs.items():
                        arg_vals[k] = _safe_repr(v)
                    if arg_vals:
                        extra["args"] = arg_vals
                self._log("trace", f"enter: {fname}", _skip_frames=2, **extra)
                start = time.perf_counter()
                try:
                    result = fn(*args, **kwargs)
                    elapsed = (time.perf_counter() - start) * 1000
                    exit_extra = {"func": fname, "elapsed_ms": round(elapsed, 2)}
                    if log_result:
                        exit_extra["result"] = _safe_repr(result)
                    self._log("trace", f"exit: {fname}", _skip_frames=2, **exit_extra)
                    return result
                except Exception as e:
                    elapsed = (time.perf_counter() - start) * 1000
                    error_extra = {"func": fname, "elapsed_ms": round(elapsed, 2)}
                    if log_error:
                        error_extra["error"] = type(e).__name__
                        error_extra["error_msg"] = str(e)
                    self._log("trace", f"error: {fname}", _skip_frames=2, **error_extra)
                    raise
            wrapper._is_traced = True
            return wrapper

        if func is not None:
            return decorator(func)
        return decorator

    def with_context(self, **kwargs) -> "ContextLogger":
        """Return a logger that includes these extra fields in every call."""
        merged_extra = {**self.extra, **kwargs}
        return ContextLogger(self, merged_extra)

    def context(self, **kwargs):
        """Context manager that adds fields to all logs within the block."""
        from snaplog.context import context
        return context(**kwargs)


class ContextLogger:
    """Logger wrapper that adds static extra fields to every log call."""

    def __init__(self, parent: Logger, extra: dict[str, Any]):
        self._parent = parent
        self.extra = extra

    def _log(self, level: str, msg: str, **kwargs) -> None:
        merged = {**self.extra, **kwargs}
        self._parent._log(level, msg, **merged)

    def debug(self, msg: str, **kwargs) -> None:
        self._log("debug", msg, **kwargs)

    def info(self, msg: str, **kwargs) -> None:
        self._log("info", msg, **kwargs)

    def warning(self, msg: str, **kwargs) -> None:
        self._log("warning", msg, **kwargs)

    def warn(self, msg: str, **kwargs) -> None:
        self.warning(msg, **kwargs)

    def error(self, msg: str, **kwargs) -> None:
        self._log("error", msg, **kwargs)

    def critical(self, msg: str, **kwargs) -> None:
        self._log("critical", msg, **kwargs)

    def trace(self, msg: str, **kwargs) -> None:
        self._log("trace", msg, **kwargs)

    def with_context(self, **kwargs) -> "ContextLogger":
        merged_extra = {**self.extra, **kwargs}
        return ContextLogger(self._parent, merged_extra)

    def context(self, **kwargs):
        from snaplog.context import context
        return context(**kwargs)


def _safe_repr(obj: Any) -> Any:
    """Safe representation of objects for logging."""
    if isinstance(obj, (str, int, float, bool, type(None))):
        return obj
    if isinstance(obj, (list, tuple)):
        if len(obj) <= 10:
            return [_safe_repr(x) for x in obj]
        return f"[{len(obj)} items]"
    if isinstance(obj, dict):
        if len(obj) <= 10:
            return {k: _safe_repr(v) for k, v in list(obj.items())[:10]}
        return f"{{{len(obj)} keys}}"
    if isinstance(obj, Exception):
        return {"error": type(obj).__name__, "message": str(obj)}
    return str(obj)


def get_logger(
    name: str = "root",
    min_level: str = "debug",
    formatter: Any = None,
    output: TextIO | Callable[[str], None] | None = None,
    extra: dict[str, Any] | None = None,
) -> Logger:
    """Get or create a named logger.

    Args:
        name: Logger name.
        min_level: Minimum log level.
        formatter: Output formatter (JsonFormatter or HumanFormatter).
        output: Output destination. Default: sys.stderr.
        extra: Static extra fields for every log entry.
    """
    with _loggers_lock:
        if name not in _loggers:
            _loggers[name] = Logger(
                name=name,
                min_level=min_level,
                formatter=formatter,
                output=output,
                extra=extra,
            )
        return _loggers[name]
