#!/usr/bin/env python
# @Project ：django_base_ai
# @File    : operation_log.py
# @Author  : cx
# @Time    : 11/2/2026
# @Desc    : 操作日志查询与筛选。
"""
操作日志模块：提供操作日志的 CRUD、导出、使用量统计；
日志文件分析（analyze_logs）、下载（download_logs）、实时 tail 流（tail_logs）。
仅管理员可访问日志文件相关接口（download_logs / tail_logs）。
"""
import logging
import re
import os
import time
from datetime import datetime, timedelta
from urllib.parse import quote

from django.conf import settings
from django.db.models import Count
from django.http import StreamingHttpResponse
from rest_framework.decorators import action

from django_base_ai.system.models import Apps, OperationLog
from django_base_ai.utils.json_response import DetailResponse, ErrorResponse
from django_base_ai.utils.permission import AdminPermission
from django_base_ai.utils.serializers import CustomModelSerializer
from django_base_ai.utils.viewset import CustomModelViewSet

logger = logging.getLogger(__name__)


class OperationLogSerializer(CustomModelSerializer):
    """
    日志-序列化器
    """

    class Meta:
        model = OperationLog
        fields = "__all__"
        read_only_fields = ["id"]


class OperationLogCreateUpdateSerializer(CustomModelSerializer):
    """
    操作日志  创建/更新时的列化器
    """

    class Meta:
        model = OperationLog
        fields = "__all__"


class ExportOperationLogSerializer(CustomModelSerializer):
    """
    操作日志  创建/更新时的列化器
    """

    class Meta:
        model = OperationLog
        fields = "__all__"


class OperationLogViewSet(CustomModelViewSet):
    """
    操作日志接口
    list:查询
    create:新增
    update:修改
    retrieve:单例
    destroy:删除
    """

    queryset = OperationLog.objects.order_by("-create_datetime")
    serializer_class = OperationLogSerializer
    search_fields = ["appid", "request_modular", "request_ip", "response_code"]
    # 导出
    export_field_label = {
        "appid": "应用id",
        "creator_name": "操作人",
        "request_modular": "请求模块",
        "request_path": "请求地址",
        "request_body": "请求参数",
        "request_method": "请求方式",
        "request_msg": "操作说明",
        "request_ip": "请求ip地址",
        "request_browser": "请求浏览器",
        "response_code": "响应状态码",
        "request_os": "操作系统",
        "json_result": "返回信息",
    }
    export_serializer_class = ExportOperationLogSerializer

    @action(methods=["get"], detail=False)
    def usage_amount(self, request, *args, **kwargs):
        """
        使用量统计：按 appid、request_method、response_code 聚合计数。
        :param request: DRF 请求
        :return: DetailResponse([])，统计结果可在此扩展返回
        """
        logger.debug("operation_log usage_amount 请求")
        operation_log_result = OperationLog.objects.values("appid").annotate(count=Count("id"))
        operation_action_result = OperationLog.objects.values("request_method").annotate(count=Count("id"))
        operation_code_result = OperationLog.objects.values("response_code").annotate(count=Count("id"))
        logger.debug("usage_amount appid=%s method=%s code=%s", list(operation_log_result), list(operation_action_result), list(operation_code_result))
        return DetailResponse([])

    def file_iterator(self, filename, chunk_size=512):
        """
        文件迭代器，按块读取文件并 yield，用于 StreamingHttpResponse 流式下载。
        :param filename: 文件绝对路径
        :param chunk_size: 每块字节数，默认 512
        """
        with open(filename, "rb") as f:
            while True:
                c = f.read(chunk_size)
                if c:
                    yield c
                else:
                    break

    @action(methods=["get"], detail=False)
    def analyze_logs(self, request, *args, **kwargs):
        """
        分析日志文件中的报错内容：按级别/模块/时间统计，返回错误模式与最近错误列表。
        查询参数：log_type(error|server)、start_date、end_date、error_level、limit。
        """
        import re
        from collections import Counter, defaultdict
        from datetime import datetime

        log_type = request.GET.get("log_type", "error")  # error 或 server
        logger.info("analyze_logs 请求 log_type=%s", log_type)
        start_date = request.GET.get("start_date")
        end_date = request.GET.get("end_date")
        error_level = request.GET.get("error_level", "ERROR")  # ERROR, WARNING, INFO
        limit = int(request.GET.get("limit", 100))  # 返回记录数限制

        # 构建日志文件路径
        logs_dir = os.path.join(settings.BASE_DIR, "logs")
        if log_type == "error":
            log_file_path = os.path.join(logs_dir, "error.log")
        else:
            log_file_path = os.path.join(logs_dir, "server.log")

        if not os.path.exists(log_file_path):
            logger.warning("analyze_logs 日志文件不存在 path=%s", log_file_path)
            return ErrorResponse(msg=f"日志文件不存在: {log_type}.log")

        error_logs = []
        error_patterns = []
        error_counts = Counter()
        module_counts = Counter()
        time_counts = defaultdict(int)

        try:
            with open(log_file_path, encoding="utf-8") as f:
                lines = f.readlines()

            current_error = None

            for line_num, line in enumerate(lines, 1):
                line = line.strip()
                if not line:
                    continue

                # 匹配日志格式: [时间][模块][级别] 消息
                log_match = re.match(r"\[([^\]]+)\]\[([^\]]+)\] \[([^\]]+)\] (.+)", line)

                if log_match:
                    timestamp_str, module, level, message = log_match.groups()

                    # 解析时间
                    try:
                        timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S,%f")
                    except ValueError:
                        try:
                            timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
                        except ValueError:
                            timestamp = None

                    # 过滤时间范围
                    if start_date and timestamp:
                        try:
                            start_dt = datetime.strptime(start_date, "%Y-%m-%d")
                            if timestamp.date() < start_dt.date():
                                continue
                        except ValueError:
                            pass

                    if end_date and timestamp:
                        try:
                            end_dt = datetime.strptime(end_date, "%Y-%m-%d")
                            if timestamp.date() > end_dt.date():
                                continue
                        except ValueError:
                            pass

                    # 处理错误级别
                    if level == error_level:
                        # 保存当前错误
                        if current_error:
                            error_logs.append(current_error)

                        current_error = {
                            "timestamp": timestamp_str,
                            "module": module,
                            "level": level,
                            "message": message,
                            "line_number": line_num,
                            "stack_trace": [],
                        }

                        # 统计
                        error_counts[message] += 1
                        module_counts[module] += 1
                        if timestamp:
                            time_counts[timestamp.strftime("%Y-%m-%d")] += 1

                    # 处理堆栈跟踪
                    elif current_error and (line.startswith("  File ") or line.startswith("Traceback")):
                        current_error["stack_trace"].append(line)

                # 处理多行错误消息
                elif current_error and not line.startswith("["):
                    current_error["message"] += " " + line

            # 添加最后一个错误
            if current_error:
                error_logs.append(current_error)

        except Exception as e:
            logger.exception("analyze_logs 读取日志文件失败 path=%s", log_file_path)
            return ErrorResponse(msg=f"读取日志文件失败: {str(e)}")

        error_logs = error_logs[-limit:] if limit > 0 else error_logs
        logger.info("analyze_logs 完成 path=%s total_errors=%s", log_file_path, len(error_logs))

        # 分析错误模式
        error_patterns = []
        for error_msg, count in error_counts.most_common(10):
            # 提取错误模式（去除具体参数）
            pattern = re.sub(r"\d+", "N", error_msg)  # 数字替换为N
            pattern = re.sub(r"[a-f0-9]{8,}", "HASH", pattern)  # 哈希值替换
            pattern = re.sub(r"/[^\s]+", "/PATH", pattern)  # 路径替换
            error_patterns.append({"pattern": pattern, "count": count, "example": error_msg})

        # 构建返回数据
        result_data = {
            "log_file": f"{log_type}.log",
            "summary": {
                "total_errors": len(error_logs),
                "unique_error_types": len(error_counts),
                "top_modules": dict(module_counts.most_common(5)),
                "date_range": {
                    "start": min(time_counts.keys()) if time_counts else None,
                    "end": max(time_counts.keys()) if time_counts else None,
                },
            },
            "error_patterns": error_patterns,
            "module_statistics": [
                {"module": module, "count": count} for module, count in module_counts.most_common(10)
            ],
            "time_statistics": dict(time_counts),
            "recent_errors": [
                {
                    "timestamp": log["timestamp"],
                    "module": log["module"],
                    "level": log["level"],
                    "message": log["message"],
                    "line_number": log["line_number"],
                    "stack_trace": log["stack_trace"][:5],  # 只返回前5行堆栈
                }
                for log in error_logs[-20:]  # 最近20条错误
            ],
        }

        return DetailResponse(data=result_data)

    @action(methods=["get"], detail=False, permission_classes=[AdminPermission])
    def download_logs(self, request, *args, **kwargs):
        """
        管理员下载日志文件（仅 AdminPermission）。
        仅允许 server.log / error.log，防止路径遍历。
        """
        log_filename = request.GET.get("filename", "server.log")
        logger.info("download_logs 请求 filename=%s user=%s", log_filename, getattr(request.user, "username", None))

        if not log_filename or log_filename not in ["server.log", "error.log"]:
            logger.warning("download_logs 无效文件名 filename=%s", log_filename)
            return ErrorResponse(msg="无效的日志文件名")

        # 构建日志文件路径
        logs_dir = os.path.join(settings.BASE_DIR, "logs")
        log_file_path = os.path.join(logs_dir, log_filename)

        if not os.path.exists(log_file_path):
            logger.warning("download_logs 文件不存在 path=%s", log_file_path)
            return ErrorResponse(msg="日志文件不存在")

        if not os.access(log_file_path, os.R_OK):
            logger.warning("download_logs 文件不可读 path=%s", log_file_path)
            return ErrorResponse(msg="无法读取日志文件")

        response = StreamingHttpResponse(self.file_iterator(log_file_path))
        response["Content-Type"] = "application/octet-stream"
        response["Content-Disposition"] = f'attachment; filename="{quote(log_filename)}"'

        return response

    # 日志行时间戳正则，与 analyze_logs 一致：[时间][模块][级别] 消息
    _LOG_LINE_TIME_RE = re.compile(r"^\[([^\]]+)\]\[([^\]]+)\] \[([^\]]+)\] (.+)$")

    def _parse_log_line_time(self, line):
        """解析日志行首的时间戳，返回 datetime 或 None。支持 %Y-%m-%d %H:%M:%S,%f 与 %Y-%m-%d %H:%M:%S。"""
        m = self._LOG_LINE_TIME_RE.match(line.strip())
        if not m:
            return None
        ts_str = m.group(1)
        for fmt in ("%Y-%m-%d %H:%M:%S,%f", "%Y-%m-%d %H:%M:%S"):
            try:
                return datetime.strptime(ts_str.strip(), fmt)
            except ValueError:
                continue
        return None

    def _tail_log_stream(
        self,
        log_file_path,
        from_beginning=False,
        poll_interval=0.5,
        encoding="utf-8",
        start_ts=None,
        end_ts=None,
        max_duration_seconds=None,
        max_lines=None,
    ):
        """
        类 tail -f：持续读取日志文件新增内容，按行以 SSE 格式 yield。
        可选按时间范围过滤（仅输出在 start_ts～end_ts 内的行，多行堆栈按首行时间判断）。
        可选 max_duration_seconds / max_lines 限制时长或行数后主动结束，避免 curl 等客户端一直挂起。
        :param log_file_path: 日志文件绝对路径
        :param from_beginning: True=从文件开头读起，False=从当前末尾开始（等同 tail -f）
        :param poll_interval: 无新内容时休眠秒数
        :param encoding: 文件编码
        :param start_ts: 仅输出时间 >= start_ts 的日志行（datetime，None 表示不限制）
        :param end_ts: 仅输出时间 <= end_ts 的日志行（datetime，None 表示不限制）
        :param max_duration_seconds: 最多流式传输秒数，超时后结束（None=不限制）
        :param max_lines: 最多输出日志行数（不含首行说明），达到后结束（None=不限制）
        """
        def _sse(line):
            payload = line.replace("\n", "\ndata: ")
            return f"data: {payload}\n\n".encode(encoding)

        time_range_desc = ""
        if start_ts or end_ts:
            time_range_desc = f", time_range=[{start_ts} ~ {end_ts}]"
        yield _sse(f"[tail] {os.path.basename(log_file_path)} (from_beginning={from_beginning}{time_range_desc})")
        yield _sse("")
        # runserver(wsgiref) 默认约 4KB 才刷新，首包加 SSE 注释填充使 curl 能立即看到输出
        yield (f": {' ' * 4092}\n\n".encode(encoding))

        deadline = None
        if max_duration_seconds is not None and max_duration_seconds > 0:
            deadline = time.monotonic() + max_duration_seconds
        line_count = 0

        def _in_time_range(ts):
            if ts is None:
                return True
            if start_ts is not None and ts < start_ts:
                return False
            if end_ts is not None and ts > end_ts:
                return False
            return True

        try:
            with open(log_file_path, "r", encoding=encoding, errors="replace") as f:
                if from_beginning:
                    f.seek(0)
                else:
                    f.seek(0, 2)
                last_size = f.tell()
                last_ts = None
                while True:
                    if deadline is not None and time.monotonic() >= deadline:
                        yield _sse("[tail] 已达 max_duration_seconds，结束流")
                        return
                    if max_lines is not None and line_count >= max_lines:
                        yield _sse(f"[tail] 已达 max_lines={max_lines}，结束流")
                        return

                    line = f.readline()
                    if line:
                        line = line.rstrip("\n\r")
                        if not line:
                            continue
                        ts = self._parse_log_line_time(line)
                        if ts is not None:
                            last_ts = ts
                        if _in_time_range(last_ts if ts is None else ts):
                            yield _sse(line)
                            line_count += 1
                    else:
                        try:
                            curr = os.path.getsize(log_file_path)
                            if curr < last_size:
                                f.seek(0)
                            last_size = curr
                        except OSError:
                            pass
                        time.sleep(poll_interval)
        except (FileNotFoundError, PermissionError) as e:
            logger.warning("tail_log 读文件异常 path=%s: %s", log_file_path, e)
            yield _sse(f"[错误] 无法读取日志: {e}")
        except GeneratorExit:
            logger.debug("tail_log 客户端断开 path=%s", log_file_path)
            raise

    def _parse_time_range(self, time_range_str, start_time_str, end_time_str):
        """
        解析时间范围。返回 (start_ts, end_ts)，均为 datetime 或 None。
        - time_range: 1m | 10m | 30m | 1h，表示「最近 N」；end=now，start=now-N。
        - start_time / end_time: 自定义起止，格式 YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD。
        若 time_range 与 start/end 同时传，time_range 优先。
        """
        now = datetime.now()
        if time_range_str:
            s = (time_range_str or "").strip().lower()
            delta = None
            if s == "1m":
                delta = timedelta(minutes=1)
            elif s == "10m":
                delta = timedelta(minutes=10)
            elif s == "30m":
                delta = timedelta(minutes=30)
            elif s == "1h":
                delta = timedelta(hours=1)
            if delta is not None:
                return now - delta, now

        def _parse_dt(s):
            if not s:
                return None
            s = s.strip()
            for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
                try:
                    return datetime.strptime(s, fmt)
                except ValueError:
                    continue
            return None

        start_ts = _parse_dt(start_time_str)
        end_ts = _parse_dt(end_time_str)
        if end_ts is None and start_ts is not None:
            end_ts = now
        return start_ts, end_ts

    @action(methods=["get"], detail=False, permission_classes=[AdminPermission])
    def tail_logs(self, request, *args, **kwargs):
        """
        使用 Server-Sent Events (EventSource) 实时推送日志内容，类似 Linux tail -f。
        支持按时间范围过滤，仅输出指定时间段内的日志行。
        未传 max_duration_seconds/max_lines 时默认 30 秒后结束；传 max_duration_seconds=0 表示不限制（长连）。
        查询参数:
          - filename: server.log | error.log，默认 server.log
          - from_beginning: 1 或 true 表示从文件开头读起，否则从当前末尾开始
          - poll_interval: 轮询间隔秒数，默认 0.5
          - time_range: 1m | 10m | 30m | 1h，表示最近 1 分钟 / 10 分钟 / 30 分钟 / 1 小时（优先）
          - start_time: 自定义开始时间，格式 YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD
          - end_time: 自定义结束时间，格式同上；不传则默认为当前时间
          - max_duration_seconds: 最多流式传输秒数，超时后结束；默认 30；传 0 表示不限制
          - max_lines: 最多输出日志行数，达到后结束
        """
        log_filename = request.GET.get("filename", "server.log")
        logger.info("tail_logs 请求 filename=%s from_beginning param=%s", log_filename, request.GET.get("from_beginning"))

        if log_filename not in ("server.log", "error.log"):
            logger.warning("tail_logs 无效文件名 filename=%s", log_filename)
            return ErrorResponse(msg="无效的日志文件名，仅支持 server.log 或 error.log")

        logs_dir = os.path.join(settings.BASE_DIR, "logs")
        log_file_path = os.path.join(logs_dir, log_filename)
        if not os.path.exists(log_file_path):
            logger.warning("tail_logs 文件不存在 path=%s", log_file_path)
            return ErrorResponse(msg=f"日志文件不存在: {log_filename}")

        from_beginning = request.GET.get("from_beginning", "").lower() in ("1", "true", "yes")
        try:
            poll_interval = max(0.1, min(2.0, float(request.GET.get("poll_interval", 0.5))))
        except (TypeError, ValueError):
            poll_interval = 0.5

        time_range = request.GET.get("time_range", "").strip()
        start_time = request.GET.get("start_time", "").strip()
        end_time = request.GET.get("end_time", "").strip()
        start_ts, end_ts = self._parse_time_range(time_range, start_time, end_time)
        if (start_ts or end_ts) and not from_beginning:
            from_beginning = True

        max_duration_seconds = None
        try:
            v = request.GET.get("max_duration_seconds")
            if v is not None and str(v).strip() != "":
                n = int(float(v))
                if n > 0:
                    max_duration_seconds = max(1, min(86400, n))
                # n<=0 表示显式不限制，保持 None
        except (TypeError, ValueError):
            pass
        max_lines = None
        try:
            v = request.GET.get("max_lines")
            if v is not None and v != "":
                max_lines = max(1, min(1000000, int(v)))
        except (TypeError, ValueError):
            pass
        # 未传任何限制时默认 30 秒结束，避免 curl 等客户端一直无返回
        if max_duration_seconds is None and max_lines is None:
            max_duration_seconds = 30
            # 默认限时场景下从文件开头读，先输出已有日志，否则只等新行会一直无内容
            if request.GET.get("from_beginning", "").lower() not in ("0", "false", "no"):
                from_beginning = True

        stream = self._tail_log_stream(
            log_file_path,
            from_beginning=from_beginning,
            poll_interval=poll_interval,
            start_ts=start_ts,
            end_ts=end_ts,
            max_duration_seconds=max_duration_seconds,
            max_lines=max_lines,
        )
        response = StreamingHttpResponse(stream, content_type="text/event-stream; charset=utf-8")
        response["Cache-Control"] = "no-cache"
        response["X-Accel-Buffering"] = "no"
        response["Connection"] = "keep-alive"
        return response
