import ast
import json
import logging
import os
import re
import sys
import traceback
from collections.abc import Mapping, Sequence
from typing import Any

from loguru import logger as _logger
from loguru._logger import Logger

from config.env import AppConfig, LogConfig
from middlewares.trace_middleware import TraceCtx
from utils.server_util import WorkerIdUtil


def _split_field_tokens(field_name: str) -> tuple[str, ...]:
    """
    拆分字段名为文本匹配用的 token

    :param field_name: 原始字段名
    :return: token 元组
    """
    separated_name = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', field_name)
    normalized_name = re.sub(r'[^A-Za-z0-9]+', ' ', separated_name)
    return tuple(token.lower() for token in normalized_name.split() if token)


def _build_text_key_pattern(field_name: str) -> str:
    """
    构建文本日志中的字段名匹配正则，自动兼容 snake_case / camelCase / kebab-case / dot.case

    :param field_name: 原始字段名
    :return: 正则片段
    """
    tokens = _split_field_tokens(field_name)
    if not tokens:
        return re.escape(field_name)
    return r'[^\S\r\n._-]*[._-]*[^\S\r\n._-]*'.join(re.escape(token) for token in tokens)


def _build_text_assignment_patterns(key_pattern: str) -> list[re.Pattern[str]]:
    """
    构建文本日志中的键值对匹配正则

    :param key_pattern: 字段名正则片段
    :return: 正则列表
    """
    if not key_pattern:
        return []
    return [
        re.compile(
            rf'(?P<prefix>(?P<key_quote>[\'"]?)(?P<key>(?:{key_pattern}))(?P=key_quote)\s*[:=]\s*)'
            r'(?P<quote>[\'"])(?P<value>.*?)(?P=quote)',
            re.IGNORECASE,
        ),
        re.compile(
            rf'(?P<prefix>(?P<key_quote>[\'"]?)(?P<key>(?:{key_pattern}))(?P=key_quote)\s*[:=]\s*)'
            r'(?P<quote>[\'"]?)(?P<value>[^\'"\s,;]+)(?P=quote)',
            re.IGNORECASE,
        ),
    ]


class LogSanitizer:
    """
    日志脱敏工具
    """

    _MASK = LogConfig.log_mask_placeholder
    _PHONE_MIN_LENGTH = 7
    _PHONE_PREFIX_LENGTH = 3
    _PHONE_SUFFIX_LENGTH = 4
    _EMAIL_SHORT_LOCAL_LENGTH = 2
    _IPV4_PARTS = 4
    _IPV4_VISIBLE_PARTS = 3
    _IPV6_MASK_THRESHOLD = 2
    _IPV6_VISIBLE_PARTS = 2
    _SENSITIVE_FIELDS = {
        re.sub(r'[^a-z0-9]', '', item.lower())
        for item in (field.strip() for field in LogConfig.log_mask_fields.split(','))
        if item
    }
    _TEXT_SENSITIVE_FIELDS = tuple(field.strip() for field in LogConfig.log_mask_fields.split(',') if field.strip())
    _TEXT_SENSITIVE_KEY_PATTERN = '|'.join(
        sorted({_build_text_key_pattern(field_name) for field_name in _TEXT_SENSITIVE_FIELDS}, key=len, reverse=True)
    )
    _PARTIAL_MASK_FIELDS = {
        re.sub(r'[^a-z0-9]', '', item.lower())
        for item in (field.strip() for field in LogConfig.log_partial_mask_fields.split(','))
        if item
    }
    _TEXT_PARTIAL_FIELDS = tuple(
        field.strip() for field in LogConfig.log_partial_mask_fields.split(',') if field.strip()
    )
    _TEXT_PARTIAL_KEY_PATTERN = '|'.join(
        sorted({_build_text_key_pattern(field_name) for field_name in _TEXT_PARTIAL_FIELDS}, key=len, reverse=True)
    )
    _CONFIG_SECRET_PATTERNS = [
        re.compile(pattern.strip(), re.IGNORECASE)
        for pattern in LogConfig.log_config_secret_patterns.split(',')
        if pattern.strip()
    ]
    _KV_PATTERNS = [
        re.compile(
            r'(?P<prefix>authorization\s*[:=]\s*)(?P<quote>[\'"]?)(?P<value>bearer\s+[^\s\'",;]+|[^\s\'",;]+)(?P=quote)',
            re.IGNORECASE,
        ),
        *_build_text_assignment_patterns(_TEXT_SENSITIVE_KEY_PATTERN),
        re.compile(r'(?P<prefix>Bearer\s+)(?P<value>[A-Za-z0-9\-._~+/]+=*)', re.IGNORECASE),
    ]
    _PARTIAL_KV_PATTERNS = _build_text_assignment_patterns(_TEXT_PARTIAL_KEY_PATTERN)
    _LOGIN_CODE_PATTERN = re.compile(r'^[A-Za-z0-9]{4,8}$')

    @classmethod
    def sanitize_data(cls, data: Any, field_name: str | None = None) -> Any:
        """
        对日志数据进行脱敏

        :param data: 原始日志数据
        :param field_name: 当前字段名
        :return: 脱敏后的数据
        """
        if not LogConfig.log_mask_enabled:
            return data
        if data is None:
            return None
        if hasattr(data, 'model_dump'):
            return cls.sanitize_data(data.model_dump(by_alias=True, exclude_none=True), field_name)
        if isinstance(data, Mapping):
            return cls._sanitize_mapping(data)
        if isinstance(data, str):
            return cls._sanitize_string(data, field_name)
        if isinstance(data, bytes):
            return f'<bytes:{len(data)}>'
        if isinstance(data, Sequence) and not isinstance(data, (str, bytes, bytearray)):
            return [cls.sanitize_data(item, field_name) for item in data]
        return data

    @classmethod
    def sanitize_text(cls, text: str) -> str:
        """
        对普通文本日志进行脱敏

        :param text: 原始文本
        :return: 脱敏后的文本
        """
        if not LogConfig.log_mask_enabled:
            return text
        if not isinstance(text, str):
            return text
        sanitized_text = cls._sanitize_string(text)
        return sanitized_text if isinstance(sanitized_text, str) else json.dumps(sanitized_text, ensure_ascii=False)

    @classmethod
    def _sanitize_mapping(cls, data: Mapping[Any, Any]) -> dict[Any, Any]:
        """
        对字典类型数据进行脱敏

        :param data: 字典数据
        :return: 脱敏后的字典
        """
        sanitized: dict[Any, Any] = {}
        normalized_map = {cls._normalize_key(str(key)): value for key, value in data.items()}

        for key, value in data.items():
            key_str = str(key)
            normalized_key = cls._normalize_key(key_str)
            if cls._should_fully_mask_field(normalized_key, value, normalized_map):
                sanitized[key] = cls._MASK
            elif normalized_key in cls._PARTIAL_MASK_FIELDS and isinstance(value, str):
                sanitized[key] = cls._mask_partial_value(value, normalized_key)
            else:
                sanitized[key] = cls.sanitize_data(value, key_str)

        config_key = normalized_map.get('configkey')
        if isinstance(config_key, str) and cls._is_secret_config_key(config_key):
            for key in data:
                if cls._normalize_key(str(key)) == 'configvalue':
                    sanitized[key] = cls._MASK

        return sanitized

    @classmethod
    def _sanitize_string(cls, value: str, field_name: str | None = None) -> Any:
        """
        对字符串数据进行脱敏

        :param value: 字符串值
        :param field_name: 当前字段名
        :return: 脱敏后的字符串或结构化数据
        """
        normalized_field = cls._normalize_key(field_name or '')
        if normalized_field in cls._SENSITIVE_FIELDS:
            return cls._MASK
        if normalized_field in cls._PARTIAL_MASK_FIELDS:
            return cls._mask_partial_value(value, normalized_field)

        stripped_value = value.strip()
        if stripped_value and stripped_value[0] in '{[':
            try:
                parsed_value = json.loads(value)
            except (TypeError, ValueError, json.JSONDecodeError):
                try:
                    parsed_value = ast.literal_eval(value)
                except (SyntaxError, ValueError):
                    pass
                else:
                    return cls._dump_sanitized_structured_text(
                        cls.sanitize_data(parsed_value, field_name),
                        original_text=value,
                    )
            else:
                return cls._dump_sanitized_structured_text(
                    cls.sanitize_data(parsed_value, field_name),
                    original_text=value,
                )

        sanitized_text = value
        for pattern in cls._KV_PATTERNS:
            sanitized_text = pattern.sub(cls._replace_text_secret, sanitized_text)
        for pattern in cls._PARTIAL_KV_PATTERNS:
            sanitized_text = pattern.sub(cls._replace_text_partial_secret, sanitized_text)

        if '验证码' in sanitized_text:
            sanitized_text = re.sub(r'(验证码(?:为|是)?\s*)([A-Za-z0-9]{4,8})', rf'\1{cls._MASK}', sanitized_text)

        return sanitized_text

    @classmethod
    def _replace_text_secret(cls, match: re.Match[str]) -> str:
        """
        替换文本中的敏感值

        :param match: 正则匹配对象
        :return: 脱敏后的文本
        """
        prefix = match.group('prefix')
        quote = match.groupdict().get('quote', '')
        if quote is None:
            quote = ''
        return f'{prefix}{quote}{cls._MASK}{quote}'

    @classmethod
    def _replace_text_partial_secret(cls, match: re.Match[str]) -> str:
        """
        替换文本中的部分脱敏字段值

        :param match: 正则匹配对象
        :return: 脱敏后的文本
        """
        prefix = match.group('prefix')
        quote = match.groupdict().get('quote', '')
        if quote is None:
            quote = ''
        normalized_key = cls._normalize_key(match.groupdict().get('key', ''))
        masked_value = cls._mask_partial_value(match.group('value'), normalized_key)
        return f'{prefix}{quote}{masked_value}{quote}'

    @staticmethod
    def _dump_sanitized_structured_text(sanitized_value: Any, original_text: str) -> Any:
        """
        将脱敏后的结构化数据恢复为文本，并尽量保持原始换行风格

        :param sanitized_value: 脱敏后的结构化数据
        :param original_text: 原始文本
        :return: 文本或原始值
        """
        if isinstance(sanitized_value, (dict, list)):
            indent = 2 if '\n' in original_text or '\r' in original_text else None
            return json.dumps(sanitized_value, ensure_ascii=False, indent=indent)
        return sanitized_value

    @classmethod
    def _should_fully_mask_field(
        cls, normalized_key: str, value: Any, full_mapping: Mapping[str, Any] | None = None
    ) -> bool:
        """
        判断字段是否需要全量脱敏

        :param normalized_key: 标准化后的字段名
        :param value: 字段值
        :param full_mapping: 当前层级的完整字段映射
        :return: 是否需要全量脱敏
        """
        if normalized_key in cls._SENSITIVE_FIELDS:
            return True
        if normalized_key in {'captchacode', 'smscode'}:
            return True
        if normalized_key == 'code' and isinstance(value, str):
            sibling_keys = set((full_mapping or {}).keys())
            if 'uuid' in sibling_keys and cls._LOGIN_CODE_PATTERN.fullmatch(value):
                return True
        return False

    @classmethod
    def _is_secret_config_key(cls, config_key: str) -> bool:
        """
        判断参数键是否属于敏感配置

        :param config_key: 参数键
        :return: 是否敏感
        """
        return any(pattern.search(config_key) for pattern in cls._CONFIG_SECRET_PATTERNS)

    @classmethod
    def _mask_partial_value(cls, value: str, normalized_field: str) -> str:
        """
        对部分字段进行部分脱敏

        :param value: 原始值
        :param normalized_field: 标准化后的字段名
        :return: 脱敏后的值
        """
        if normalized_field in {'phonenumber', 'phone', 'mobile'}:
            return cls._mask_phone(value)
        if normalized_field == 'email':
            return cls._mask_email(value)
        if normalized_field in {'ip', 'ipaddr', 'operip', 'loginip'}:
            return cls._mask_ip(value)
        return cls._MASK

    @staticmethod
    def _normalize_key(field_name: str) -> str:
        """
        标准化字段名

        :param field_name: 原始字段名
        :return: 标准化字段名
        """
        return re.sub(r'[^a-z0-9]', '', field_name.lower())

    @classmethod
    def _mask_phone(cls, value: str) -> str:
        """
        手机号脱敏

        :param value: 原始手机号
        :return: 脱敏后的手机号
        """
        digits = re.sub(r'\D', '', value)
        if len(digits) < cls._PHONE_MIN_LENGTH:
            return cls._MASK
        return f'{digits[: cls._PHONE_PREFIX_LENGTH]}****{digits[-cls._PHONE_SUFFIX_LENGTH :]}'

    @classmethod
    def _mask_email(cls, value: str) -> str:
        """
        邮箱脱敏

        :param value: 原始邮箱
        :return: 脱敏后的邮箱
        """
        if '@' not in value:
            return cls._MASK
        local_part, domain = value.split('@', 1)
        masked_local = (
            f'{local_part[:1]}***'
            if len(local_part) <= cls._EMAIL_SHORT_LOCAL_LENGTH
            else f'{local_part[:1]}***{local_part[-1:]}'
        )
        return f'{masked_local}@{domain}'

    @classmethod
    def _mask_ip(cls, value: str) -> str:
        """
        IP地址脱敏

        :param value: 原始IP
        :return: 脱敏后的IP
        """
        if '.' in value:
            parts = value.split('.')
            if len(parts) == cls._IPV4_PARTS:
                return '.'.join([*parts[: cls._IPV4_VISIBLE_PARTS], '*'])
        if ':' in value:
            parts = value.split(':')
            if len(parts) > cls._IPV6_MASK_THRESHOLD:
                return ':'.join([*parts[: cls._IPV6_VISIBLE_PARTS], '*', '*'])
        return cls._MASK


class InterceptHandler(logging.Handler):
    target_logger = _logger

    def emit(self, record: logging.LogRecord) -> None:
        """
        拦截标准 logging 记录并转发到 Loguru

        :param record: 原生 logging 日志记录
        :return: None
        """
        try:
            level = self.target_logger.level(record.levelname).name
        except ValueError:
            level = record.levelno
        frame, depth = logging.currentframe(), 2
        while frame and frame.f_code.co_filename == logging.__file__:
            frame = frame.f_back
            depth += 1
        self.target_logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())


class LoggerInitializer:
    def __init__(self) -> None:
        """
        初始化日志基础配置与运行时标识

        :return: None
        """
        self.worker_id = WorkerIdUtil.get_worker_id(LogConfig.log_worker_id)
        self.instance_id = LogConfig.log_instance_id
        self.service_name = LogConfig.log_service_name or AppConfig.app_name
        self._log_file_enabled = LogConfig.log_file_enabled
        self._log_base_dir = LogConfig.log_file_base_dir
        self._ensure_log_directory_exists()

    def _ensure_log_directory_exists(self) -> None:
        """
        确保日志目录存在

        :return: None
        """
        if not self._log_file_enabled:
            return
        if self._log_base_dir and not os.path.exists(self._log_base_dir):
            os.makedirs(self._log_base_dir, exist_ok=True)

    def _filter(self, record: dict) -> bool:
        """
        注入 Trace 上下文并控制启动阶段日志输出

        :param record: Loguru 日志记录字典
        :return: 是否允许输出日志
        """
        record['extra']['trace_id'] = TraceCtx.get_trace_id()
        record['extra']['request_id'] = TraceCtx.get_request_id()
        record['extra']['span_id'] = TraceCtx.get_span_id()
        record['extra']['path'] = TraceCtx.get_request_path()
        record['extra']['method'] = TraceCtx.get_request_method()
        record['extra']['worker_id'] = self.worker_id
        record['extra']['instance_id'] = self.instance_id
        record['extra']['service'] = self.service_name
        startup_phase = record['extra'].get('startup_phase')
        startup_log_enabled = record['extra'].get('startup_log_enabled')
        record['extra'] = LogSanitizer.sanitize_data(record['extra'])
        if startup_phase:
            return bool(startup_log_enabled)
        return True

    @staticmethod
    def _get_exception_value_text(exception: Any) -> str | None:
        """
        获取脱敏后的异常值文本

        :param exception: Loguru 异常对象
        :return: 脱敏后的异常值文本
        """
        if not exception or not exception.value:
            return None
        return LogSanitizer.sanitize_text(str(exception.value))

    def _get_exception_traceback_text(self, exception: Any) -> str | None:
        """
        获取脱敏后的异常堆栈文本

        :param exception: Loguru 异常对象
        :return: 脱敏后的异常堆栈文本
        """
        if not exception or not exception.traceback:
            return None
        if isinstance(exception.traceback, str):
            traceback_text = exception.traceback
        elif exception.type and exception.value:
            traceback_text = ''.join(traceback.format_exception(exception.type, exception.value, exception.traceback))
        else:
            traceback_text = str(exception.traceback)
        return LogSanitizer.sanitize_text(traceback_text.rstrip())

    def _build_plain_exception_suffix(self, record: dict) -> str:
        """
        构建普通文本日志使用的异常后缀

        :param record: Loguru 日志记录字典
        :return: 异常后缀文本
        """
        exception_text = self._get_exception_traceback_text(record.get('exception'))
        return f'\n{exception_text}' if exception_text else ''

    def _build_json_payload(self, record: dict) -> dict:
        """
        构建统一的 JSON 日志结构

        :param record: Loguru 日志记录字典
        :return: JSON 日志结构
        """
        exception = None
        if record['exception']:
            exception = {
                'type': record['exception'].type.__name__ if record['exception'].type else None,
                'value': self._get_exception_value_text(record['exception']),
                'traceback': self._get_exception_traceback_text(record['exception']),
            }
        return {
            'timestamp': record['time'].isoformat(),
            'level': record['level'].name,
            'message': record['message'],
            'logger': record['name'],
            'trace_id': record['extra'].get('trace_id'),
            'request_id': record['extra'].get('request_id'),
            'span_id': record['extra'].get('span_id'),
            'worker_id': record['extra'].get('worker_id'),
            'instance_id': record['extra'].get('instance_id'),
            'service': record['extra'].get('service'),
            'method': record['extra'].get('method'),
            'path': record['extra'].get('path'),
            'module': record['module'],
            'function': record['function'],
            'line': record['line'],
            'exception': exception,
            'extra': {key: value for key, value in record['extra'].items() if key != 'json_payload'},
        }

    def _json_log_formatter(self, record: dict) -> str:
        """
        将 Loguru 日志记录格式化为 JSON 文本

        :param record: Loguru 日志记录字典
        :return: JSON 格式文本
        """
        record['extra']['json_payload'] = json.dumps(self._build_json_payload(record), ensure_ascii=False, default=str)
        return '{extra[json_payload]}\n'

    def _plain_log_formatter(self, record: dict) -> str:
        """
        将 Loguru 日志记录格式化为普通文本，并追加脱敏后的异常堆栈

        :param record: Loguru 日志记录字典
        :return: 普通文本格式模板
        """
        record['extra']['sanitized_exception'] = self._build_plain_exception_suffix(record)
        return (
            '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | '
            '<cyan>{extra[trace_id]}</cyan> | '
            '<magenta>{extra[span_id]}</magenta> | '
            '<yellow>{extra[request_id]}</yellow> | '
            '<blue>{extra[worker_id]}</blue> | '
            '<level>{level: <8}</level> | '
            '<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - '
            '<level>{message}</level>{extra[sanitized_exception]}\n'
        )

    def _patch_record(self, record: dict) -> dict:
        """
        在日志落地前统一脱敏

        :param record: Loguru 日志记录字典
        :return: 脱敏后的日志记录字典
        """
        record['message'] = LogSanitizer.sanitize_text(record['message'])
        return record

    def _info_file_filter(self, record: dict) -> bool:
        """
        仅输出 INFO 级别日志到 info 文件

        :param record: Loguru 日志记录字典
        :return: 是否允许输出日志
        """
        return self._filter(record) and record['level'].name == 'INFO'

    def _error_file_filter(self, record: dict) -> bool:
        """
        输出 WARNING 及以上日志到 error 文件

        :param record: Loguru 日志记录字典
        :return: 是否允许输出日志
        """
        return self._filter(record) and record['level'].no >= logging.WARNING

    def _configure_logging(self) -> None:
        """
        统一接管标准 logging 与第三方日志输出

        :return: None
        """
        logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
        for logger_name in ('uvicorn', 'uvicorn.error', 'uvicorn.access', 'fastapi'):
            logging.getLogger(logger_name).handlers = [InterceptHandler()]
            logging.getLogger(logger_name).propagate = False
        for logger_name in ('LiteLLM', 'litellm'):
            logging.getLogger(logger_name).setLevel(logging.WARNING)

    def init_log(self) -> Logger:
        """
        初始化 Loguru 输出与标准 logging 配置

        :return: 已配置的 Loguru Logger 实例
        """
        configured_logger = _logger.patch(self._patch_record)
        InterceptHandler.target_logger = configured_logger
        configured_logger.remove()
        info_log_path = os.path.join(self._log_base_dir, '{time:YYYY}', '{time:MM}', '{time:DD}', 'info.log')
        error_log_path = os.path.join(self._log_base_dir, '{time:YYYY}', '{time:MM}', '{time:DD}', 'error.log')
        if LogConfig.loguru_stdout:
            if LogConfig.loguru_json:
                configured_logger.add(
                    sys.stdout,
                    level=LogConfig.loguru_level,
                    enqueue=True,
                    filter=self._filter,
                    format=self._json_log_formatter,
                )
            else:
                configured_logger.add(
                    sys.stdout,
                    level=LogConfig.loguru_level,
                    enqueue=True,
                    filter=self._filter,
                    format=self._plain_log_formatter,
                )
        if self._log_file_enabled:
            if LogConfig.loguru_json:
                configured_logger.add(
                    info_log_path,
                    level='INFO',
                    rotation=LogConfig.loguru_rotation,
                    retention=LogConfig.loguru_retention,
                    compression=LogConfig.loguru_compression,
                    enqueue=True,
                    filter=self._info_file_filter,
                    serialize=False,
                    format=self._json_log_formatter,
                )
                configured_logger.add(
                    error_log_path,
                    level='WARNING',
                    rotation=LogConfig.loguru_rotation,
                    retention=LogConfig.loguru_retention,
                    compression=LogConfig.loguru_compression,
                    enqueue=True,
                    filter=self._error_file_filter,
                    serialize=False,
                    format=self._json_log_formatter,
                )
            else:
                configured_logger.add(
                    info_log_path,
                    level='INFO',
                    rotation=LogConfig.loguru_rotation,
                    retention=LogConfig.loguru_retention,
                    compression=LogConfig.loguru_compression,
                    enqueue=True,
                    filter=self._info_file_filter,
                    format=self._plain_log_formatter,
                )
                configured_logger.add(
                    error_log_path,
                    level='WARNING',
                    rotation=LogConfig.loguru_rotation,
                    retention=LogConfig.loguru_retention,
                    compression=LogConfig.loguru_compression,
                    enqueue=True,
                    filter=self._error_file_filter,
                    format=self._plain_log_formatter,
                )
        self._configure_logging()
        return configured_logger


# 初始化日志处理器
log_initializer = LoggerInitializer()
logger = log_initializer.init_log()
