"""
Centralised logging configuration.

Adds request context and timing for both webapp and agent-tool logs.
"""

from __future__ import annotations

import contextvars
import logging
import logging.config
import sys
import time
import uuid
from contextlib import contextmanager
from typing import Any, Iterator

try:
    from flask import g, has_request_context, request
except Exception:  # pragma: no cover - flask may be unavailable in some contexts
    g = None  # type: ignore[assignment]
    request = None  # type: ignore[assignment]

    def has_request_context() -> bool:  # type: ignore[no-redef]
        return False


DEFAULT_LOG_LEVEL = "DEBUG"
DEFAULT_MAX_LOG_TEXT_CHARS = 1000
DEFAULT_MAX_LOG_ITEMS = 25
MAX_RESPONSE_JSON_LOG_BYTES = 50000

_LOG_CONTEXT: contextvars.ContextVar[dict[str, str] | None] = contextvars.ContextVar(
    "semantic_models_log_context", default=None
)

SENSITIVE_LOG_KEYS = {
    "authorization",
    "password",
    "token",
    "access_token",
    "api_key",
    "secret",
}


def _get_log_level() -> str:
    """Get default log level for this plugin."""
    return DEFAULT_LOG_LEVEL


def truncate_text(value: Any, max_chars: int = DEFAULT_MAX_LOG_TEXT_CHARS) -> str:
    """Truncate text values for safe logging."""
    text = value if isinstance(value, str) else str(value)
    if len(text) <= max_chars:
        return text
    return f"{text[:max_chars]}... [{len(text) - max_chars} chars truncated]"


def _is_sensitive_key(key: str) -> bool:
    return key.lower() in SENSITIVE_LOG_KEYS


def summarize_for_logging(
    value: Any,
    *,
    max_chars: int = DEFAULT_MAX_LOG_TEXT_CHARS,
    max_items: int = DEFAULT_MAX_LOG_ITEMS,
    max_depth: int = 4,
) -> Any:
    """
    Return a size-limited structure suitable for logs.
    """
    if max_depth <= 0:
        return "<max-depth>"

    if value is None or isinstance(value, (bool, int, float)):
        return value

    if isinstance(value, str):
        return truncate_text(value, max_chars=max_chars)

    if isinstance(value, bytes):
        return f"<bytes:{len(value)}>"

    if isinstance(value, dict):
        items = list(value.items())
        out: dict[str, Any] = {}
        for i, (k, v) in enumerate(items):
            key = str(k)
            if i >= max_items:
                out["__truncated_items__"] = len(items) - max_items
                break
            if _is_sensitive_key(key):
                out[key] = "[redacted]"
                continue
            out[key] = summarize_for_logging(v, max_chars=max_chars, max_items=max_items, max_depth=max_depth - 1)
        return out

    if isinstance(value, (list, tuple, set)):
        seq = list(value)
        out = [
            summarize_for_logging(v, max_chars=max_chars, max_items=max_items, max_depth=max_depth - 1)
            for v in seq[:max_items]
        ]
        if len(seq) > max_items:
            out.append(f"... [{len(seq) - max_items} items truncated]")
        return out

    return truncate_text(repr(value), max_chars=max_chars)


def _summarize_request_payload() -> dict[str, Any]:
    payload: dict[str, Any] = {}
    if request is None:
        return payload

    args = request.args.to_dict(flat=False)
    if args:
        payload["query"] = summarize_for_logging(args, max_chars=400, max_items=20, max_depth=3)

    json_payload = request.get_json(silent=True)
    if json_payload is not None:
        payload["json"] = summarize_for_logging(json_payload, max_chars=400, max_items=30, max_depth=4)

    form_payload = request.form.to_dict(flat=False)
    if form_payload:
        payload["form"] = summarize_for_logging(form_payload, max_chars=400, max_items=20, max_depth=3)

    if request.files:
        files = []
        for field_name, uploaded in request.files.items(multi=True):
            files.append(
                {
                    "field": field_name,
                    "filename": uploaded.filename,
                    "content_type": uploaded.content_type,
                }
            )
        payload["files"] = files

    if request.content_length is not None:
        payload["content_length"] = request.content_length

    return payload


def _summarize_response(response: Any) -> dict[str, Any]:
    summary = {
        "status": getattr(response, "status_code", "-"),
        "content_type": getattr(response, "mimetype", None) or getattr(response, "content_type", "-"),
    }
    try:
        content_length = response.calculate_content_length()
    except Exception:
        content_length = None
    if content_length is not None:
        summary["content_length"] = content_length

    if getattr(response, "mimetype", "") == "application/json":
        if content_length is not None and content_length > MAX_RESPONSE_JSON_LOG_BYTES:
            summary["json"] = f"[omitted: {content_length} bytes]"
        else:
            try:
                json_payload = response.get_json(silent=True)
            except Exception:
                json_payload = None
            if json_payload is not None:
                summary["json"] = summarize_for_logging(json_payload, max_chars=400, max_items=30, max_depth=3)
    return summary


class RequestContextFilter(logging.Filter):
    """Inject request and caller context into every log record."""

    def filter(self, record: logging.LogRecord) -> bool:
        # Default values for non-request contexts.
        request_id = "-"
        user = "-"
        method = "-"
        path = "-"

        # Webapp request context.
        if has_request_context():
            request_id = getattr(g, "request_id", "-")
            user = getattr(g, "authIdentifier", "-")
            method = request.method if request is not None else "-"
            path = request.path if request is not None else "-"
        else:
            # Agent tool / background context.
            ctx = _LOG_CONTEXT.get()
            if ctx:
                request_id = ctx.get("request_id", "-")
                user = ctx.get("user", "-")
                method = ctx.get("method", "-")
                path = ctx.get("path", "-")

        record.request_id = request_id
        record.user = user
        record.method = method
        record.path = path
        return True


@contextmanager
def log_context(
    *,
    request_id: str | None = None,
    user: str | None = None,
    method: str | None = None,
    path: str | None = None,
) -> Iterator[None]:
    """
    Attach context fields to logs outside Flask request scope.
    Useful for agent-tool invocations.
    """
    current = _LOG_CONTEXT.get() or {}
    merged = dict(current)
    if request_id is not None:
        merged["request_id"] = request_id
    if user is not None:
        merged["user"] = user
    if method is not None:
        merged["method"] = method
    if path is not None:
        merged["path"] = path

    token = _LOG_CONTEXT.set(merged)
    try:
        yield
    finally:
        _LOG_CONTEXT.reset(token)


def _resolve_request_user() -> str:
    """
    Resolve user exactly like agent-hub (from browser headers).
    """
    if not has_request_context():
        return "-"

    try:
        import dataiku

        headers = dict(request.headers) if request is not None else {}
        auth = dataiku.api_client().get_auth_info_from_browser_headers(headers)
        return auth["authIdentifier"]
    except Exception:
        return "-"


def install_request_logging_hooks(app: Any) -> None:
    """
    Register before/after request hooks:
    - create request_id/user context for all logs
    - emit request start/end logs with execution timing
    """
    if getattr(app, "_semantic_models_request_logging_installed", False):
        return

    request_logger = logging.getLogger("semantic_models.request")

    @app.before_request
    def _before_request_logging() -> None:
        setattr(g, "request_id", str(uuid.uuid4()))
        setattr(g, "request_started_at", time.perf_counter())
        setattr(g, "authIdentifier", _resolve_request_user())
        payload = _summarize_request_payload()
        request_logger.info("Request started: payload=%s", payload)

    @app.after_request
    def _after_request_logging(response: Any) -> Any:
        started_at = getattr(g, "request_started_at", None)
        duration_ms = 0.0
        if isinstance(started_at, (int, float)):
            duration_ms = (time.perf_counter() - started_at) * 1000

        response_summary = _summarize_response(response)
        request_logger.info("Request completed: duration_ms=%.2f response=%s", duration_ms, response_summary)

        try:
            response.headers["X-Request-ID"] = getattr(g, "request_id", "-")
        except Exception:
            pass
        return response

    setattr(app, "_semantic_models_request_logging_installed", True)


def configure_logging(log_level: str | None = None) -> None:
    """
    Configure logging with request context format.

    Args:
        log_level: Optional override for log level. If not provided,
                   defaults to DEBUG.
    """
    if log_level is None:
        log_level = _get_log_level()
    log_level = log_level.upper()

    fmt = (
        "[%(asctime)s.%(msecs)03d] [%(threadName)s] [%(levelname)s] [%(name)s] "
        "req_id=%(request_id)s user=%(user)s method=%(method)s path=%(path)s - %(message)s"
    )
    datefmt = "%Y/%m/%d-%H:%M:%S"
    formatter = {"format": fmt, "datefmt": datefmt}

    logging.config.dictConfig(
        {
            "version": 1,
            "disable_existing_loggers": False,
            "filters": {"ctx": {"()": RequestContextFilter}},
            "formatters": {"default": formatter},
            "handlers": {
                "console": {
                    "class": "logging.StreamHandler",
                    "formatter": "default",
                    "filters": ["ctx"],
                    "level": log_level,
                    "stream": "ext://sys.stdout",
                }
            },
            "root": {"handlers": ["console"], "level": log_level},
        }
    )


class NoParsingFilter(logging.Filter):
    """Filter out werkzeug assertion errors."""

    def filter(self, record: logging.LogRecord) -> bool:
        message = record.getMessage()
        return not message.endswith("AssertionError: write() before start_response")
