"""
Centralised, opinionated logging helpers.

Every log record gets the fields:
    req_id   – UUID v4 per HTTP/WebSocket request
    user     – authenticated user (or "-")
    method   – HTTP verb (or "-")
    path     – request path (or "-")
"""

from __future__ import annotations

import json
import logging
import logging.config
import re
import uuid
from typing import Any

from flask import g, has_request_context, request


def _get_log_level() -> str:
    """
    Get log level from webapp config.
    Returns 'INFO' as default if not configured.
    """
    try:
        from dataiku.customwebapp import get_webapp_config

        params = get_webapp_config()
        return params.get("log_level", "INFO").upper()
    except Exception:
        return "INFO"


class _RequestContextFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:  # noqa: D401
        """Injects request-level context into `record` in-place."""

        if has_request_context():
            record.req_id = getattr(g, "request_id", "-")
            record.user = getattr(g, "authIdentifier", "-")
            record.method = request.method
            record.path = request.path
        else:
            record.req_id = record.user = record.method = record.path = "-"
        return True


# Lower case keys for case-insensitive matching
SENSITIVE_KEYS: set[str] = {
    "api_key",
    "apikey",
    "authorization",
    "credentials",
    "file_data",
    "password",
    "secret",
    "token",
    "dkucallerticket",
}
REDACTED_VALUE = "REDACTED"


class _RedactSensitiveDataFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:
        """Recursively apply `redact_sensitive_data` to log records."""
        if isinstance(record.msg, (dict, list)):
            record.msg = redact_sensitive_data(record.msg)

        # record.args is either a tuple (for positional args) or a dict (for keyword args)
        if record.args:
            if isinstance(record.args, tuple):
                # Handle positional arguments
                record.args = tuple(redact_sensitive_data(arg) for arg in record.args)
            elif isinstance(record.args, dict):
                # Handle keyword arguments
                record.args = redact_sensitive_data(record.args)
        return True


def redact_sensitive_data(data: Any) -> Any:
    """Recursively traverses a data structure to redact values for sensitive keys.

    - Operates on dicts, lists, and JSON strings.
    - Returns a rebuilt copy of container values, leaving original data untouched.
    - Matches string dict keys case-insensitively against `SENSITIVE_KEYS`
      while preserving the original keys in the returned payload.
    """
    if isinstance(data, str):
        try:
            parsed_data = json.loads(data)
            redacted_data = redact_sensitive_data(parsed_data)
            return json.dumps(redacted_data, indent=2, sort_keys=True)
        except json.JSONDecodeError:
            return data  # Not a valid JSON string

    if isinstance(data, dict):
        clean_data = {}
        for key, value in data.items():
            if isinstance(key, str) and key.lower() in SENSITIVE_KEYS:
                clean_data[key] = REDACTED_VALUE
            else:
                clean_data[key] = redact_sensitive_data(value)
        return clean_data

    if isinstance(data, list):
        return [redact_sensitive_data(item) for item in data]

    return data


def _copy_container_structure(data: Any) -> Any:
    """Copy dict/list containers recursively while leaving leaf objects as-is."""
    if isinstance(data, dict):
        return {key: _copy_container_structure(value) for key, value in data.items()}
    if isinstance(data, list):
        return [_copy_container_structure(item) for item in data]
    return data


class _CredentialSanitizingFilter(logging.Filter):
    """Filter that sanitizes credentials from log messages."""

    # Pattern to match database URLs with credentials
    # Matches: dialect[+driver]://user:password@host... and hides both user and password
    _db_url_pattern = re.compile(
        r"((?:postgresql|snowflake|mysql|oracle|mssql)(?:\+[a-z0-9_]+)?://)([^@]+)(@)", re.IGNORECASE
    )

    # Pattern to match OAuth tokens in query parameters
    # Matches: token=... (until & or end of string)
    _token_pattern = re.compile(r"(token=)([^&\s]+)")

    def filter(self, record: logging.LogRecord) -> bool:
        """Sanitize credentials from the log message."""
        if record.msg and isinstance(record.msg, str):
            record.msg = self._db_url_pattern.sub(r"\1***\3", record.msg)
            record.msg = self._token_pattern.sub(r"\1***", record.msg)
        # Also sanitize args if they contain strings
        if record.args:
            record.args = self._sanitize_args(record.args)
        return True

    def _sanitize_args(self, args):
        """Sanitize any string arguments that may contain credentials."""
        if isinstance(args, dict):
            return {k: self._sanitize_value(v) for k, v in args.items()}
        elif isinstance(args, (list, tuple)):
            sanitized = [self._sanitize_value(arg) for arg in args]
            return tuple(sanitized) if isinstance(args, tuple) else sanitized
        return args

    def _sanitize_value(self, value):
        """Sanitize a single value if it's a string containing credentials."""
        if isinstance(value, str):
            value = self._db_url_pattern.sub(r"\1***\3", value)
            value = self._token_pattern.sub(r"\1***", value)
        return value


def configure_logging() -> None:
    log_level = _get_log_level()

    fmt = "[%(asctime)s.%(msecs)03d] [%(threadName)s] [%(levelname)s] [%(name)s] user=%(user)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},
                "sanitize": {"()": _CredentialSanitizingFilter},
                "redact": {"()": _RedactSensitiveDataFilter},
            },
            "formatters": {"default": formatter},
            "handlers": {
                "console": {
                    "class": "logging.StreamHandler",
                    "formatter": "default",
                    "filters": ["ctx", "redact", "sanitize"],
                    "level": log_level,
                }
            },
            "root": {"handlers": ["console"], "level": log_level},
        }
    )


_logging_configured = False


def get_logger(name: str) -> logging.Logger:
    """Uniform helper – **always** import this instead of `logging.getLogger`."""
    global _logging_configured
    if not _logging_configured:
        configure_logging()
        _logging_configured = True
    return logging.getLogger(name)


def set_request_id() -> None:
    """
    Ensure `g.request_id` exists (UUID4).
    Call once in *every* Flask/SocketIO entry-point.
    """

    if has_request_context() and not hasattr(g, "request_id"):
        g.request_id = str(uuid.uuid4())


def _redact_document_fields(messages: list[Any]) -> None:
    for message in messages:
        if isinstance(message, dict):
            docs = message.get("__documents__")
            if isinstance(docs, list):
                for doc in docs:
                    if isinstance(doc, dict):
                        if "text" in doc:
                            doc["text"] = "[redacted]"
                        if "text_chunks" in doc:
                            doc["text_chunks"] = "[redacted]"
                        if "file_data" in doc:
                            doc["file_data"] = "[redacted]"


def _redact_inline_images(messages: list[Any]) -> None:
    image_index = 1
    for msg in messages:
        if not isinstance(msg, dict):
            continue
        parts = msg.get("parts")
        if not isinstance(parts, list):
            continue
        for part in parts:
            if not isinstance(part, dict):
                continue
            if "inlineImage" in part:
                part["inlineImage"] = f"image[{image_index}]"
                image_index += 1


def sanitize_for_log(payload: Any | None) -> Any | None:
    """Sanitizes a payload for logging.

    - Redacts large fields inside `__documents__` to avoid bloating logs.
    - Redacts inline images to avoid logging base64 payloads.
    - Redacts sensitive data like passwords, tokens, etc. via `redact_sensitive_data`.
    """
    if not payload:
        return None

    payload_copy = _copy_container_structure(payload)

    try:
        messages = None
        if isinstance(payload_copy, list):
            messages = payload_copy
        elif isinstance(payload_copy, dict):
            messages = payload_copy.get("messages")

        if isinstance(messages, list):
            _redact_document_fields(messages)
            _redact_inline_images(messages)
    except Exception:
        # Best-effort: if anything goes wrong, avoid breaking callers and proceed
        # to the next sanitization step.
        pass

    return redact_sensitive_data(payload_copy)


def extract_error_message(error_str: str) -> str:
    """Extract the meaningful message from error string, removing package names."""
    parts = [part.strip() for part in error_str.split(":") if part.strip()]
    return parts[-1] if parts else error_str
