"""Logging helpers."""

import json
import logging
import os
import re
import sys
from dataclasses import dataclass, field
from typing import Any, TextIO
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse

from ..runtime.context import correlation_id

_STANDARD_KEYS = set(
    logging.LogRecord(name="x", level=0, pathname="", lineno=0, msg="", args=(), exc_info=None).__dict__.keys()
) | {"message"}
_SENSITIVE_KEYWORDS = ("authorization", "password", "token", "secret", "api_key", "apikey")


@dataclass(slots=True, frozen=True)
class LoggingConfig:
    """Logging setup."""

    level: int | str = logging.INFO
    format: str | None = None
    logger_names: tuple[str, ...] = ("iisi_app_core",)
    stream: TextIO = field(default=sys.stdout, repr=False)


class CorrelationIdFilter(logging.Filter):
    """Attach the correlation id to a log record."""

    def filter(self, record: logging.LogRecord) -> bool:
        record.correlation_id = correlation_id() or "-"
        return True


def _shorten_logger_name(full_name: str, segments_to_shorten: int = 2) -> str:
    parts = full_name.split(".")
    limit = min(segments_to_shorten, len(parts))
    for index in range(limit):
        if parts[index]:
            parts[index] = parts[index][0]
    return ".".join(parts)


def _short_logger_name(record: logging.LogRecord) -> str:
    short_name = getattr(record, "short_logger_name", None)
    if short_name:
        return short_name
    short_name = _shorten_logger_name(record.name)
    record.short_logger_name = short_name
    return short_name


class ShortLoggerNameFilter(logging.Filter):
    """Attach a shortened logger name."""

    def filter(self, record: logging.LogRecord) -> bool:
        record.short_logger_name = _shorten_logger_name(record.name)
        return True


class JsonFormatter(logging.Formatter):
    """Format logs as JSON."""

    def format(self, record: logging.LogRecord) -> str:
        payload: dict[str, Any] = {
            "level": record.levelname,
            "logger": _short_logger_name(record),
            "message": record.getMessage(),
            "time": self.formatTime(record, self.datefmt),
            "correlation_id": getattr(record, "correlation_id", "-"),
        }
        for key, value in record.__dict__.items():
            if key not in _STANDARD_KEYS and key not in {"args"}:
                payload[key] = value
        if record.exc_info:
            payload["exc_info"] = self.formatException(record.exc_info)
        return json.dumps(payload, default=str, ensure_ascii=False, separators=(", ", ": "))


class PlainFormatter(logging.Formatter):
    """Format logs as plain text."""

    def __init__(self) -> None:
        super().__init__("%(levelname)s [%(short_logger_name)s] %(message)s")

    def format(self, record: logging.LogRecord) -> str:
        _short_logger_name(record)
        base_entry = super().format(record)
        extra_payload = {}
        for key, value in record.__dict__.items():
            if key in _STANDARD_KEYS or key in {"correlation_id", "short_logger_name"}:
                continue
            extra_payload[key] = value
        if not extra_payload:
            return base_entry
        return f"{base_entry} {json.dumps(extra_payload, default=str, ensure_ascii=False, indent=2)}"


def _use_plain_formatter(fmt: str | None) -> bool:
    override = (fmt or os.environ.get("APP_LOG_FORMAT") or "").strip().lower()
    if override in {"text", "txt", "plain"}:
        return True
    if override in {"json", "structured"}:
        return False
    environment = os.environ.get("APP_ENV") or os.environ.get("ENVIRONMENT")
    if environment and environment.strip().lower() in {"dev", "development", "local"}:
        return True
    return "PYTEST_CURRENT_TEST" in os.environ or "PYTEST_VERSION" in os.environ


def configure_logging(config: LoggingConfig | None = None) -> None:
    """Configure library loggers."""

    resolved = config or LoggingConfig()
    formatter: logging.Formatter = PlainFormatter() if _use_plain_formatter(resolved.format) else JsonFormatter()
    for name in resolved.logger_names:
        logger = logging.getLogger(name)
        logger.handlers.clear()
        handler = logging.StreamHandler(resolved.stream)
        handler.addFilter(CorrelationIdFilter())
        handler.addFilter(ShortLoggerNameFilter())
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        logger.setLevel(resolved.level)
        logger.propagate = False


def get_logger(name: str = __name__) -> logging.Logger:
    """Return a logger."""

    return logging.getLogger(name)


@dataclass(slots=True, frozen=True)
class RequestLogEntry:
    type: str
    url: str
    method: str
    headers: dict[str, Any]
    body: Any | None = None

    def as_dict(self) -> dict[str, Any]:
        data = {
            "type": self.type,
            "url": self.url,
            "method": self.method,
            "headers": self.headers,
        }
        if self.body is not None:
            data["body"] = self.body
        return data


@dataclass(slots=True, frozen=True)
class ResponseLogEntry(RequestLogEntry):
    status: int = 200

    def as_dict(self) -> dict[str, Any]:
        data = RequestLogEntry.as_dict(self)
        data["status"] = self.status
        return data


class LogSanitizer:
    """Mask sensitive values in logs."""

    def __init__(self, *, params: set[str] | None = None, headers: set[str] | None = None) -> None:
        self._explicit_sensitive_names = (
            {str(value).lower() for value in (params or set())}
            | {str(value).lower() for value in (headers or set())}
            | {"authorization"}
        )
        self._secrets = {str(value) for value in (params or set())} | {str(value) for value in (headers or set())}

    def _is_sensitive_name(self, name: str | None) -> bool:
        if not name:
            return False
        normalized = name.strip().lower()
        if normalized in self._explicit_sensitive_names:
            return True
        return any(keyword in normalized for keyword in _SENSITIVE_KEYWORDS)

    def _mask_str(self, value: str, *, key_hint: str | None = None) -> str:
        if self._is_sensitive_name(key_hint):
            return "*****"
        masked = re.sub(r"(?i)\bBearer\s+\S+", "Bearer *****", value)
        masked = re.sub(
            r"(?i)([?&][^=&]*(?:token|password|secret|api[_-]?key)[^=&]*=)[^&]*",
            r"\1*****",
            masked,
        )
        for secret in self._secrets:
            if secret:
                masked = masked.replace(secret, "*****")
        return masked

    def _mask_url(self, value: str) -> str:
        parsed = urlparse(value)
        if not parsed.query:
            return self._mask_str(value)
        query = parse_qsl(parsed.query, keep_blank_values=True)
        masked_query = [
            (key, "*****" if self._is_sensitive_name(key) else self._mask_str(item, key_hint=key))
            for key, item in query
        ]
        return urlunparse(parsed._replace(query=urlencode(masked_query, doseq=True)))

    def mask(self, value: Any, *, key_hint: str | None = None) -> Any:
        if self._is_sensitive_name(key_hint):
            return "*****"
        if isinstance(value, str):
            try:
                parsed = json.loads(value)
            except json.JSONDecodeError, TypeError:
                return self._mask_str(value, key_hint=key_hint)
            if isinstance(parsed, dict | list):
                return json.dumps(self.mask(parsed, key_hint=key_hint), ensure_ascii=False)
            return self._mask_str(value, key_hint=key_hint)
        if isinstance(value, bytes):
            return self._mask_str(value.decode("utf-8", errors="replace"), key_hint=key_hint)
        if isinstance(value, dict):
            return {key: self.mask(item, key_hint=str(key)) for key, item in value.items()}
        if isinstance(value, list):
            return [self.mask(item, key_hint=key_hint) for item in value]
        if isinstance(value, tuple):
            return tuple(self.mask(item, key_hint=key_hint) for item in value)
        return value

    def mask_headers(self, values: dict[str, Any]) -> dict[str, Any]:
        return {key: self.mask(item, key_hint=key) for key, item in values.items()}

    def mask_url(self, value: str) -> str:
        return self._mask_url(value)


def make_log_hook(
    logger: logging.Logger | None = None,
    params: set[str] | None = None,
    headers: set[str] | None = None,
):
    """Build a requests-compatible log hook."""

    resolved_logger = logger or get_logger(__name__)
    sanitizer = LogSanitizer(params=params, headers=headers)

    def log_request_and_response(response, *args, **kwargs):
        del args, kwargs
        request = response.request
        method = (request.method or "").upper()
        body_to_log = None
        if method in {"POST", "PUT", "PATCH", "DELETE"}:
            body_to_log = sanitizer.mask(getattr(request, "body", None), key_hint="body")
        resolved_logger.info(
            "",
            extra=RequestLogEntry(
                type="request",
                url=sanitizer.mask_url(request.url),
                method=method,
                headers=sanitizer.mask_headers(dict(request.headers)),
                body=body_to_log,
            ).as_dict(),
        )
        resolved_logger.info(
            "",
            extra=ResponseLogEntry(
                type="response",
                url=sanitizer.mask_url(request.url),
                method=method,
                headers=sanitizer.mask_headers(dict(response.headers)),
                body=sanitizer.mask(getattr(response, "text", ""), key_hint="body")
                if getattr(response, "text", "")
                else None,
                status=response.status_code,
            ).as_dict(),
        )
        return response

    return log_request_and_response


__all__ = [
    "CorrelationIdFilter",
    "JsonFormatter",
    "LogSanitizer",
    "LoggingConfig",
    "PlainFormatter",
    "RequestLogEntry",
    "ResponseLogEntry",
    "ShortLoggerNameFilter",
    "configure_logging",
    "get_logger",
    "make_log_hook",
]
