"""
指标服务层

提供任务提交和任务执行两个维度的指标查询服务：
- tasks: 任务提交指标（来自 task_metrics_minute）
- runs: 任务执行指标（来自 task_runs_metrics_minute）
"""
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta, timezone
from enum import Enum
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
import logging
import base64
import json
import pickle

from jettask.db.models.task_metrics_minute import TaskMetricsMinute
from jettask.db.models.task_runs_metrics_minute import TaskRunsMetricsMinute
from jettask.db.models.task_run import TaskRun
from jettask.webui.services.base_service import BaseService

try:
    from ddsketch import DDSketch
    DDSKETCH_AVAILABLE = True
except ImportError:
    DDSKETCH_AVAILABLE = False
    DDSketch = None

from jettask.webui.config import webui_config

logger = logging.getLogger(__name__)


class RunsMetric(str, Enum):
    """任务执行指标枚举"""
    TOTAL_COUNT = "total_count"
    SUCCESS_COUNT = "success_count"
    FAILED_COUNT = "failed_count"
    RETRY_COUNT = "retry_count"
    AVG_DURATION = "avg_duration"
    MAX_DURATION = "max_duration"
    MIN_DURATION = "min_duration"
    AVG_DELAY = "avg_delay"
    MAX_DELAY = "max_delay"
    MIN_DELAY = "min_delay"
    MAX_CONCURRENCY = "max_concurrency"
    SUCCESS_RATE = "success_rate"
    DURATION_PERCENTILES = "duration_percentiles"  
    DELAY_PERCENTILES = "delay_percentiles"        
    BACKLOG = "backlog"                            
    ERROR_RATE = "error_rate"


class MetricsService(BaseService):
    """指标服务类"""


    @classmethod
    async def get_tasks_summary(
        cls,
        namespace: str,
        pg_session: AsyncSession,
        registry,
        queues: List[str],
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
        time_range: Optional[str] = None
    ) -> Dict[str, Any]:
        try:
            time_range_result = cls._resolve_time_range(
                start_time=start_time,
                end_time=end_time,
                time_range=time_range,
                default_range="1h",
                min_interval_seconds=60
            )
            start_time = time_range_result.start_time
            end_time = time_range_result.end_time

            queue_result = await cls._resolve_queues(queues, registry, expand_priority=True)
            target_queues = queue_result.target_queues
            all_priority_queues = queue_result.all_priority_queues
            base_queue_map = queue_result.base_queue_map

            if not target_queues:
                return {
                    "success": True,
                    "data": [],
                    "time_range": {
                        "start_time": start_time.isoformat() if start_time else None,
                        "end_time": end_time.isoformat() if end_time else None
                    }
                }

            stmt = select(
                TaskMetricsMinute.queue,
                func.sum(TaskMetricsMinute.task_count).label('task_count')
            ).where(
                and_(
                    TaskMetricsMinute.namespace == namespace,
                    TaskMetricsMinute.queue.in_(all_priority_queues),
                    TaskMetricsMinute.time_bucket >= start_time,
                    TaskMetricsMinute.time_bucket <= end_time
                )
            ).group_by(TaskMetricsMinute.queue)

            result = await pg_session.execute(stmt)
            rows = result.all()

            queue_counts = {q: 0 for q in target_queues}
            for row in rows:
                base_queue = base_queue_map.get(row.queue)
                if base_queue:
                    queue_counts[base_queue] += row.task_count or 0

            data = [
                {"queue_name": queue, "task_count": count}
                for queue, count in sorted(queue_counts.items())
            ]

            return {
                "success": True,
                "data": data,
                "time_range": {
                    "start_time": start_time.isoformat(),
                    "end_time": end_time.isoformat()
                }
            }

        except Exception as e:
            logger.error(f"获取任务提交汇总失败: {e}", exc_info=True)
            return {
                "success": False,
                "error": str(e),
                "data": []
            }

    @classmethod
    async def get_tasks_trend(
        cls,
        namespace: str,
        pg_session: AsyncSession,
        registry,
        queues: List[str],
        task_names: Optional[List[str]] = None,
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
        time_range: Optional[str] = None
    ) -> Dict[str, Any]:
        try:
            time_range_result = cls._resolve_time_range(
                start_time=start_time,
                end_time=end_time,
                time_range=time_range,
                default_range="1h",
                min_interval_seconds=60
            )
            start_time = time_range_result.start_time
            end_time = time_range_result.end_time
            interval_seconds = time_range_result.interval_seconds

            queue_result = await cls._resolve_queues(queues, registry, expand_priority=True)
            target_queues = queue_result.target_queues
            all_priority_queues = queue_result.all_priority_queues
            base_queue_map = queue_result.base_queue_map

            if not target_queues:
                return {
                    "success": True,
                    "data": [],
                    "metrics": ["task_count"],
                    "granularity": time_range_result.granularity,
                    "time_range": {
                        "start_time": start_time.isoformat(),
                        "end_time": end_time.isoformat(),
                        "interval": time_range_result.interval,
                        "interval_seconds": interval_seconds
                    }
                }

            stmt = select(
                TaskMetricsMinute.queue,
                TaskMetricsMinute.time_bucket,
                TaskMetricsMinute.task_count
            ).where(
                and_(
                    TaskMetricsMinute.namespace == namespace,
                    TaskMetricsMinute.queue.in_(all_priority_queues),
                    TaskMetricsMinute.time_bucket >= start_time,
                    TaskMetricsMinute.time_bucket <= end_time
                )
            ).order_by(
                TaskMetricsMinute.queue,
                TaskMetricsMinute.time_bucket
            )

            result = await pg_session.execute(stmt)
            rows = result.fetchall()

            time_buckets = cls._generate_time_buckets(start_time, end_time, interval_seconds)

            queue_data_map = {q: {bucket: 0 for bucket in time_buckets} for q in target_queues}

            for row in rows:
                base_queue = base_queue_map.get(row.queue)
                if not base_queue or base_queue not in queue_data_map:
                    continue

                aligned_bucket = cls._align_to_bucket(row.time_bucket, interval_seconds)
                if aligned_bucket in queue_data_map[base_queue]:
                    queue_data_map[base_queue][aligned_bucket] += row.task_count or 0

            data = []
            for base_queue in target_queues:
                trend = [
                    {"time": bucket.isoformat(), "task_count": queue_data_map[base_queue][bucket]}
                    for bucket in sorted(time_buckets)
                ]
                data.append({"queue_name": base_queue, "trend": trend})

            return {
                "success": True,
                "data": data,
                "metrics": ["task_count"],
                "granularity": time_range_result.granularity,
                "time_range": {
                    "start_time": start_time.isoformat(),
                    "end_time": end_time.isoformat(),
                    "interval": time_range_result.interval,
                    "interval_seconds": interval_seconds
                }
            }

        except Exception as e:
            logger.error(f"获取任务提交趋势失败: {e}", exc_info=True)
            return {
                "success": False,
                "error": str(e),
                "data": [],
                "metrics": ["task_count"]
            }


    @classmethod
    async def get_runs_summary(
        cls,
        namespace: str,
        pg_session: AsyncSession,
        registry,
        queues: List[str],
        task_names: Optional[List[str]] = None,
        metrics: Optional[List[RunsMetric]] = None,
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
        time_range: Optional[str] = None,
        aggregate_tasks: bool = False,
        limit: int = 50,
        cursor: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        try:
            time_range_result = cls._resolve_time_range(
                start_time=start_time,
                end_time=end_time,
                time_range=time_range,
                default_range="1h",
                min_interval_seconds=60
            )
            start_time = time_range_result.start_time
            end_time = time_range_result.end_time

            queue_result = await cls._resolve_queues(queues, registry, expand_priority=True)
            target_queues = queue_result.target_queues
            all_priority_queues = queue_result.all_priority_queues
            base_queue_map = queue_result.base_queue_map

            if not target_queues:
                return {
                    "success": True,
                    "data": [],
                    "time_range": {
                        "start_time": start_time.isoformat(),
                        "end_time": end_time.isoformat()
                    },
                    "pagination": {"next_cursor": None, "has_more": False}
                }

            conditions = [
                TaskRunsMetricsMinute.namespace == namespace,
                TaskRunsMetricsMinute.queue.in_(all_priority_queues),
                TaskRunsMetricsMinute.time_bucket >= start_time,
                TaskRunsMetricsMinute.time_bucket <= end_time
            ]

            if task_names:
                conditions.append(TaskRunsMetricsMinute.task_name.in_(task_names))

            if aggregate_tasks:
                return await cls._get_runs_summary_aggregated(
                    pg_session=pg_session,
                    conditions=conditions,
                    target_queues=target_queues,
                    base_queue_map=base_queue_map,
                    metrics=metrics,
                    start_time=start_time,
                    end_time=end_time
                )

            stmt = select(
                TaskRunsMetricsMinute.queue,
                TaskRunsMetricsMinute.task_name,
                func.sum(TaskRunsMetricsMinute.total_count).label('total_count'),
                func.sum(TaskRunsMetricsMinute.success_count).label('success_count'),
                func.sum(TaskRunsMetricsMinute.failed_count).label('failed_count'),
                func.sum(TaskRunsMetricsMinute.retry_count).label('retry_count'),
                func.sum(TaskRunsMetricsMinute.total_duration).label('total_duration'),
                func.max(TaskRunsMetricsMinute.max_duration).label('max_duration'),
                func.min(TaskRunsMetricsMinute.min_duration).label('min_duration'),
                func.sum(TaskRunsMetricsMinute.total_delay).label('total_delay'),
                func.max(TaskRunsMetricsMinute.max_delay).label('max_delay'),
                func.min(TaskRunsMetricsMinute.min_delay).label('min_delay'),
                func.max(TaskRunsMetricsMinute.running_concurrency).label('max_concurrency')
            ).where(
                and_(*conditions)
            ).group_by(
                TaskRunsMetricsMinute.queue,
                TaskRunsMetricsMinute.task_name
            ).order_by(
                TaskRunsMetricsMinute.queue,
                TaskRunsMetricsMinute.task_name
            )

            result = await pg_session.execute(stmt)
            rows = result.all()

            base_queue_tasks: Dict[str, Dict[str, Dict[str, Any]]] = {q: {} for q in target_queues}

            for row in rows:
                base_queue = base_queue_map.get(row.queue)
                if not base_queue or base_queue not in base_queue_tasks:
                    continue

                task_name = row.task_name
                if task_name not in base_queue_tasks[base_queue]:
                    base_queue_tasks[base_queue][task_name] = cls._init_runs_stats()

                cls._aggregate_runs_row(base_queue_tasks[base_queue][task_name], row)

            all_tasks = []
            for queue_name in sorted(target_queues):
                for task_name, stats in sorted(base_queue_tasks[queue_name].items()):
                    cls._compute_derived_metrics(stats)
                    all_tasks.append({
                        "queue_name": queue_name,
                        "task_name": task_name,
                        "stats": stats
                    })

            start_idx = 0
            if cursor:
                cursor_queue = cursor.get("queue")
                cursor_task = cursor.get("task")
                for i, item in enumerate(all_tasks):
                    if item["queue_name"] == cursor_queue and item["task_name"] == cursor_task:
                        start_idx = i + 1
                        break

            paginated_tasks = all_tasks[start_idx:start_idx + limit]
            has_more = start_idx + limit < len(all_tasks)

            next_cursor = None
            if has_more and paginated_tasks:
                last_item = paginated_tasks[-1]
                cursor_data = {"queue": last_item["queue_name"], "task": last_item["task_name"]}
                next_cursor = base64.b64encode(json.dumps(cursor_data).encode('utf-8')).decode('utf-8')

            data = []
            queue_tasks_map: Dict[str, List[Dict[str, Any]]] = {}

            for item in paginated_tasks:
                queue_name = item["queue_name"]
                if queue_name not in queue_tasks_map:
                    queue_tasks_map[queue_name] = []

                task_data = {"task_name": item["task_name"]}
                task_data.update(cls._filter_metrics(item["stats"], metrics))
                queue_tasks_map[queue_name].append(task_data)

            for queue_name in sorted(queue_tasks_map.keys()):
                data.append({
                    "queue_name": queue_name,
                    "task_count": len(queue_tasks_map[queue_name]),
                    "tasks": queue_tasks_map[queue_name]
                })

            return {
                "success": True,
                "data": data,
                "time_range": {
                    "start_time": start_time.isoformat(),
                    "end_time": end_time.isoformat()
                },
                "pagination": {
                    "next_cursor": next_cursor,
                    "has_more": has_more
                }
            }

        except Exception as e:
            logger.error(f"获取任务执行汇总失败: {e}", exc_info=True)
            return {
                "success": False,
                "error": str(e),
                "data": [],
                "pagination": {"next_cursor": None, "has_more": False}
            }

    @classmethod
    async def get_runs_trend(
        cls,
        namespace: str,
        pg_session: AsyncSession,
        registry,
        queues: List[str],
        task_names: Optional[List[str]] = None,
        metrics: Optional[List[RunsMetric]] = None,
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
        time_range: Optional[str] = None
    ) -> Dict[str, Any]:
        try:
            if not metrics:
                metrics = [RunsMetric.TOTAL_COUNT]

            time_range_result = cls._resolve_time_range(
                start_time=start_time,
                end_time=end_time,
                time_range=time_range,
                default_range="1h",
                min_interval_seconds=60
            )
            start_time = time_range_result.start_time
            end_time = time_range_result.end_time
            interval_seconds = time_range_result.interval_seconds

            queue_result = await cls._resolve_queues(queues, registry, expand_priority=True)
            target_queues = queue_result.target_queues
            all_priority_queues = queue_result.all_priority_queues
            base_queue_map = queue_result.base_queue_map

            if not target_queues:
                return {
                    "success": True,
                    "data": [],
                    "metrics": [m.value for m in metrics],
                    "granularity": time_range_result.granularity,
                    "time_range": {
                        "start_time": start_time.isoformat(),
                        "end_time": end_time.isoformat(),
                        "interval": time_range_result.interval,
                        "interval_seconds": interval_seconds
                    }
                }

            select_fields = [
                TaskRunsMetricsMinute.queue,
                TaskRunsMetricsMinute.time_bucket,
            ]
            needed_fields = cls._get_needed_fields_for_metrics(metrics)
            for field_name in needed_fields:
                select_fields.append(getattr(TaskRunsMetricsMinute, field_name))

            conditions = [
                TaskRunsMetricsMinute.namespace == namespace,
                TaskRunsMetricsMinute.queue.in_(all_priority_queues),
                TaskRunsMetricsMinute.time_bucket >= start_time,
                TaskRunsMetricsMinute.time_bucket <= end_time
            ]

            if task_names:
                conditions.append(TaskRunsMetricsMinute.task_name.in_(task_names))

            stmt = select(*select_fields).where(
                and_(*conditions)
            ).order_by(
                TaskRunsMetricsMinute.queue,
                TaskRunsMetricsMinute.time_bucket
            )

            result = await pg_session.execute(stmt)
            rows = result.fetchall()

            time_buckets = cls._generate_time_buckets(start_time, end_time, interval_seconds)

            queue_data_map = {
                q: {bucket: cls._init_trend_bucket(metrics) for bucket in time_buckets}
                for q in target_queues
            }

            for row in rows:
                base_queue = base_queue_map.get(row.queue)
                if not base_queue or base_queue not in queue_data_map:
                    continue

                aligned_bucket = cls._align_to_bucket(row.time_bucket, interval_seconds)
                if aligned_bucket in queue_data_map[base_queue]:
                    cls._aggregate_trend_row(
                        queue_data_map[base_queue][aligned_bucket],
                        row,
                        needed_fields
                    )

            data = []
            for base_queue in target_queues:
                trend = []
                for bucket in sorted(time_buckets):
                    bucket_data = queue_data_map[base_queue][bucket]
                    point = {"time": bucket.isoformat()}
                    point.update(cls._compute_trend_metrics(bucket_data, metrics))
                    trend.append(point)

                data.append({"queue_name": base_queue, "trend": trend})

            return {
                "success": True,
                "data": data,
                "metrics": [m.value for m in metrics],
                "granularity": time_range_result.granularity,
                "time_range": {
                    "start_time": start_time.isoformat(),
                    "end_time": end_time.isoformat(),
                    "interval": time_range_result.interval,
                    "interval_seconds": interval_seconds
                }
            }

        except Exception as e:
            logger.error(f"获取任务执行趋势失败: {e}", exc_info=True)
            return {
                "success": False,
                "error": str(e),
                "data": [],
                "metrics": [m.value for m in (metrics or [RunsMetric.TOTAL_COUNT])]
            }


    @classmethod
    async def get_duration_distribution(
        cls,
        namespace: str,
        pg_session: AsyncSession,
        registry,
        queue: str,
        task_names: Optional[List[str]] = None,
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
        time_range: Optional[str] = None
    ) -> Dict[str, Any]:
        try:
            time_range_result = cls._resolve_time_range(
                start_time=start_time,
                end_time=end_time,
                time_range=time_range,
                default_range="1h",
                min_interval_seconds=60
            )
            start_time = time_range_result.start_time
            end_time = time_range_result.end_time

            queue_result = await cls._resolve_queues([queue], registry, expand_priority=True)
            all_priority_queues = queue_result.all_priority_queues

            if not all_priority_queues:
                return {
                    "success": True,
                    "percentiles": {"p50": None, "p90": None, "p99": None},
                    "time_range": {
                        "start_time": start_time.isoformat(),
                        "end_time": end_time.isoformat()
                    }
                }

            conditions = [
                TaskRunsMetricsMinute.namespace == namespace,
                TaskRunsMetricsMinute.queue.in_(all_priority_queues),
                TaskRunsMetricsMinute.time_bucket >= start_time,
                TaskRunsMetricsMinute.time_bucket <= end_time
            ]

            if task_names:
                conditions.append(TaskRunsMetricsMinute.task_name.in_(task_names))

            stmt = select(
                TaskRunsMetricsMinute.duration_sketch,
            ).where(and_(*conditions))

            result = await pg_session.execute(stmt)
            rows = result.fetchall()

            if not rows:
                return {
                    "success": True,
                    "percentiles": {"p50": None, "p90": None, "p99": None},
                    "time_range": {
                        "start_time": start_time.isoformat(),
                        "end_time": end_time.isoformat()
                    }
                }

            sketch_bytes_list = [row.duration_sketch for row in rows if row.duration_sketch]
            merged_sketch = cls._merge_sketches(sketch_bytes_list)
            percentiles = cls._compute_percentiles_from_sketch(merged_sketch)

            return {
                "success": True,
                "percentiles": percentiles,
                "time_range": {
                    "start_time": start_time.isoformat(),
                    "end_time": end_time.isoformat()
                }
            }

        except Exception as e:
            logger.error(f"获取耗时分布失败: {e}", exc_info=True)
            return {
                "success": False,
                "error": str(e),
                "percentiles": {"p50": None, "p90": None, "p99": None}
            }

    @staticmethod
    def _estimate_percentiles_from_histogram(
        buckets: List[Dict[str, Any]],
        total_count: int
    ) -> Dict[str, Optional[float]]:
        if total_count == 0:
            return {"p50": None, "p90": None, "p99": None}

        bucket_boundaries = [
            (0, 100),      
            (100, 500),    
            (500, 1000),   
            (1000, 5000),  
            (5000, 10000), 
        ]

        cumulative = 0
        cumulative_counts = []
        for b in buckets:
            cumulative += b["count"]
            cumulative_counts.append(cumulative)

        def estimate_percentile(p: float) -> Optional[float]:
            target = total_count * p
            for i, (low, high) in enumerate(bucket_boundaries):
                prev_cumulative = cumulative_counts[i - 1] if i > 0 else 0
                curr_cumulative = cumulative_counts[i]

                if curr_cumulative >= target:
                    bucket_count = buckets[i]["count"]
                    if bucket_count == 0:
                        return float(low)

                    position_in_bucket = (target - prev_cumulative) / bucket_count
                    estimated_value = low + position_in_bucket * (high - low)
                    return round(estimated_value, 1)

            return float(bucket_boundaries[-1][1])

        return {
            "p50": estimate_percentile(0.5),
            "p90": estimate_percentile(0.9),
            "p99": estimate_percentile(0.99)
        }

    @staticmethod
    def _estimate_delay_percentiles_from_histogram(
        buckets: List[Dict[str, Any]],
        total_count: int
    ) -> Dict[str, Optional[float]]:
        if total_count == 0:
            return {"p50": None, "p90": None, "p99": None}

        bucket_boundaries = [
            (0, 1000),       
            (1000, 5000),    
            (5000, 10000),   
            (10000, 30000),  
            (30000, 60000),  
        ]

        cumulative = 0
        cumulative_counts = []
        for b in buckets:
            cumulative += b["count"]
            cumulative_counts.append(cumulative)

        def estimate_percentile(p: float) -> Optional[float]:
            target = total_count * p
            for i, (low, high) in enumerate(bucket_boundaries):
                prev_cumulative = cumulative_counts[i - 1] if i > 0 else 0
                curr_cumulative = cumulative_counts[i]

                if curr_cumulative >= target:
                    bucket_count = buckets[i]["count"]
                    if bucket_count == 0:
                        return float(low)

                    position_in_bucket = (target - prev_cumulative) / bucket_count
                    estimated_value = low + position_in_bucket * (high - low)
                    return round(estimated_value, 1)

            return float(bucket_boundaries[-1][1])

        return {
            "p50": estimate_percentile(0.5),
            "p90": estimate_percentile(0.9),
            "p99": estimate_percentile(0.99)
        }

    @classmethod
    async def get_error_stats(
        cls,
        namespace: str,
        pg_session: AsyncSession,
        registry,
        queue: str,
        task_names: Optional[List[str]] = None,
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
        time_range: Optional[str] = None,
        limit: int = 5
    ) -> Dict[str, Any]:
        try:
            time_range_result = cls._resolve_time_range(
                start_time=start_time,
                end_time=end_time,
                time_range=time_range,
                default_range="1h",
                min_interval_seconds=60
            )
            start_time = time_range_result.start_time
            end_time = time_range_result.end_time


            conditions = [
                TaskRun.status.in_(['failed', 'error']),
                TaskRun.trigger_time >= start_time,
                TaskRun.trigger_time <= end_time,
                TaskRun.error_type.isnot(None),  
            ]

            if task_names:
                conditions.append(TaskRun.task_name.in_(task_names))

            stmt = select(
                TaskRun.error_type,
                func.count().label('count')
            ).where(
                and_(*conditions)
            ).group_by(
                TaskRun.error_type
            ).order_by(
                func.count().desc()
            ).limit(limit + 1)  

            result = await pg_session.execute(stmt)
            rows = result.all()

            total_stmt = select(func.count()).where(and_(*conditions))
            total_result = await pg_session.execute(total_stmt)
            total_errors = total_result.scalar() or 0

            if total_errors == 0:
                return {
                    "success": True,
                    "data": [],
                    "total_errors": 0,
                    "time_range": {
                        "start_time": start_time.isoformat(),
                        "end_time": end_time.isoformat()
                    }
                }

            data = []
            shown_count = 0

            for i, row in enumerate(rows):
                if i >= limit:
                    break
                count = row.count
                percentage = round((count / total_errors) * 100, 1)
                data.append({
                    "error_type": row.error_type,
                    "count": count,
                    "percentage": percentage
                })
                shown_count += count

            other_count = total_errors - shown_count
            if other_count > 0:
                other_percentage = round((other_count / total_errors) * 100, 1)
                data.append({
                    "error_type": "其他",
                    "count": other_count,
                    "percentage": other_percentage
                })

            return {
                "success": True,
                "data": data,
                "total_errors": total_errors,
                "time_range": {
                    "start_time": start_time.isoformat(),
                    "end_time": end_time.isoformat()
                }
            }

        except Exception as e:
            logger.error(f"获取错误类型统计失败: {e}", exc_info=True)
            return {
                "success": False,
                "error": str(e),
                "data": [],
                "total_errors": 0
            }


    @classmethod
    async def _get_runs_summary_aggregated(
        cls,
        pg_session: AsyncSession,
        conditions: list,
        target_queues: List[str],
        base_queue_map: Dict[str, str],
        metrics: Optional[List['RunsMetric']],
        start_time: datetime,
        end_time: datetime
    ) -> Dict[str, Any]:
        stmt = select(
            TaskRunsMetricsMinute.queue,
            func.sum(TaskRunsMetricsMinute.total_count).label('total_count'),
            func.sum(TaskRunsMetricsMinute.success_count).label('success_count'),
            func.sum(TaskRunsMetricsMinute.failed_count).label('failed_count'),
            func.sum(TaskRunsMetricsMinute.retry_count).label('retry_count'),
            func.sum(TaskRunsMetricsMinute.total_duration).label('total_duration'),
            func.max(TaskRunsMetricsMinute.max_duration).label('max_duration'),
            func.min(TaskRunsMetricsMinute.min_duration).label('min_duration'),
            func.sum(TaskRunsMetricsMinute.total_delay).label('total_delay'),
            func.max(TaskRunsMetricsMinute.max_delay).label('max_delay'),
            func.min(TaskRunsMetricsMinute.min_delay).label('min_delay'),
            func.max(TaskRunsMetricsMinute.running_concurrency).label('max_concurrency')
        ).where(
            and_(*conditions)
        ).group_by(
            TaskRunsMetricsMinute.queue
        ).order_by(
            TaskRunsMetricsMinute.queue
        )

        result = await pg_session.execute(stmt)
        rows = result.all()

        base_queue_stats: Dict[str, Dict[str, Any]] = {q: cls._init_runs_stats() for q in target_queues}

        for row in rows:
            base_queue = base_queue_map.get(row.queue)
            if not base_queue or base_queue not in base_queue_stats:
                continue

            cls._aggregate_runs_row(base_queue_stats[base_queue], row)

        data = []
        for queue_name in sorted(target_queues):
            stats = base_queue_stats[queue_name]
            cls._compute_derived_metrics(stats)

            queue_data = {"queue_name": queue_name}
            queue_data.update(cls._filter_metrics(stats, metrics))
            data.append(queue_data)

        return {
            "success": True,
            "data": data,
            "time_range": {
                "start_time": start_time.isoformat(),
                "end_time": end_time.isoformat()
            }
        }

    @staticmethod
    def _generate_time_buckets(start_time: datetime, end_time: datetime, interval_seconds: int) -> List[datetime]:
        buckets = []
        if interval_seconds > 60:
            start_ts = int(start_time.timestamp() / interval_seconds) * interval_seconds
            end_ts = int(end_time.timestamp() / interval_seconds) * interval_seconds
            current = datetime.fromtimestamp(start_ts, tz=timezone.utc)
            end_aligned = datetime.fromtimestamp(end_ts, tz=timezone.utc)
        else:
            current = start_time.replace(second=0, microsecond=0)
            end_aligned = end_time.replace(second=0, microsecond=0)

        while current <= end_aligned:
            buckets.append(current)
            current += timedelta(seconds=interval_seconds)

        return buckets

    @staticmethod
    def _align_to_bucket(time_bucket: datetime, interval_seconds: int) -> datetime:
        if interval_seconds > 60:
            bucket_ts = int(time_bucket.timestamp() / interval_seconds) * interval_seconds
            return datetime.fromtimestamp(bucket_ts, tz=timezone.utc)
        return time_bucket

    @staticmethod
    def _init_runs_stats() -> Dict[str, Any]:
        return {
            "total_count": 0,
            "success_count": 0,
            "failed_count": 0,
            "retry_count": 0,
            "total_duration": 0.0,
            "max_duration": None,
            "min_duration": None,
            "total_delay": 0.0,
            "max_delay": None,
            "min_delay": None,
            "max_concurrency": 0
        }

    @staticmethod
    def _aggregate_runs_row(stats: Dict[str, Any], row: Any):
        stats["total_count"] += row.total_count or 0
        stats["success_count"] += row.success_count or 0
        stats["failed_count"] += row.failed_count or 0
        stats["retry_count"] += row.retry_count or 0
        stats["total_duration"] += row.total_duration or 0.0
        stats["total_delay"] += row.total_delay or 0.0

        if row.max_duration is not None:
            stats["max_duration"] = max(stats["max_duration"] or 0, row.max_duration)
        if row.min_duration is not None:
            if stats["min_duration"] is None:
                stats["min_duration"] = row.min_duration
            else:
                stats["min_duration"] = min(stats["min_duration"], row.min_duration)

        if row.max_delay is not None:
            stats["max_delay"] = max(stats["max_delay"] or 0, row.max_delay)
        if row.min_delay is not None:
            if stats["min_delay"] is None:
                stats["min_delay"] = row.min_delay
            else:
                stats["min_delay"] = min(stats["min_delay"], row.min_delay)

        stats["max_concurrency"] = max(stats["max_concurrency"], row.max_concurrency or 0)

    @staticmethod
    def _compute_derived_metrics(stats: Dict[str, Any]):
        total = stats["total_count"]
        if total > 0:
            stats["avg_duration"] = round(stats["total_duration"] / total, 4)
            stats["avg_delay"] = round(stats["total_delay"] / total, 4)
        else:
            stats["avg_duration"] = 0.0
            stats["avg_delay"] = 0.0

        success = stats["success_count"]
        failed = stats["failed_count"]
        total_finished = success + failed
        if total_finished > 0:
            stats["success_rate"] = round((success / total_finished) * 100, 2)
        else:
            stats["success_rate"] = 0.0

        stats["total_duration"] = round(stats["total_duration"], 4)
        stats["total_delay"] = round(stats["total_delay"], 4)
        if stats["max_duration"] is not None:
            stats["max_duration"] = round(stats["max_duration"], 4)
        if stats["min_duration"] is not None:
            stats["min_duration"] = round(stats["min_duration"], 4)
        if stats["max_delay"] is not None:
            stats["max_delay"] = round(stats["max_delay"], 4)
        if stats["min_delay"] is not None:
            stats["min_delay"] = round(stats["min_delay"], 4)

    @staticmethod
    def _filter_metrics(stats: Dict[str, Any], metrics: Optional[List[RunsMetric]]) -> Dict[str, Any]:
        if not metrics:
            return {
                k: v for k, v in stats.items()
                if not k.startswith('total_') or k in ['total_count']
            }

        result = {}
        for m in metrics:
            if m == RunsMetric.TOTAL_COUNT:
                result["total_count"] = stats["total_count"]
            elif m == RunsMetric.SUCCESS_COUNT:
                result["success_count"] = stats["success_count"]
            elif m == RunsMetric.FAILED_COUNT:
                result["failed_count"] = stats["failed_count"]
            elif m == RunsMetric.RETRY_COUNT:
                result["retry_count"] = stats["retry_count"]
            elif m == RunsMetric.AVG_DURATION:
                result["avg_duration"] = stats["avg_duration"]
            elif m == RunsMetric.MAX_DURATION:
                result["max_duration"] = stats["max_duration"]
            elif m == RunsMetric.MIN_DURATION:
                result["min_duration"] = stats["min_duration"]
            elif m == RunsMetric.AVG_DELAY:
                result["avg_delay"] = stats["avg_delay"]
            elif m == RunsMetric.MAX_DELAY:
                result["max_delay"] = stats["max_delay"]
            elif m == RunsMetric.MIN_DELAY:
                result["min_delay"] = stats["min_delay"]
            elif m == RunsMetric.MAX_CONCURRENCY:
                result["max_concurrency"] = stats["max_concurrency"]
            elif m == RunsMetric.SUCCESS_RATE:
                result["success_rate"] = stats["success_rate"]

        return result

    @staticmethod
    def _get_needed_fields_for_metrics(metrics: List[RunsMetric]) -> List[str]:
        fields = set()
        for m in metrics:
            if m == RunsMetric.TOTAL_COUNT:
                fields.add("total_count")
            elif m == RunsMetric.SUCCESS_COUNT:
                fields.add("success_count")
            elif m == RunsMetric.FAILED_COUNT:
                fields.add("failed_count")
            elif m == RunsMetric.RETRY_COUNT:
                fields.add("retry_count")
            elif m in [RunsMetric.AVG_DURATION, RunsMetric.MAX_DURATION, RunsMetric.MIN_DURATION]:
                fields.add("total_duration")
                fields.add("max_duration")
                fields.add("min_duration")
                fields.add("total_count")
            elif m in [RunsMetric.AVG_DELAY, RunsMetric.MAX_DELAY, RunsMetric.MIN_DELAY]:
                fields.add("total_delay")
                fields.add("max_delay")
                fields.add("min_delay")
                fields.add("total_count")
            elif m == RunsMetric.MAX_CONCURRENCY:
                fields.add("running_concurrency")
            elif m == RunsMetric.SUCCESS_RATE:
                fields.add("success_count")
                fields.add("failed_count")
            elif m == RunsMetric.DURATION_PERCENTILES:
                fields.add("total_count")
                fields.add("duration_sketch")
            elif m == RunsMetric.DELAY_PERCENTILES:
                fields.add("total_count")
                fields.add("delay_sketch")
            elif m == RunsMetric.BACKLOG:
                fields.add("backlog")
                fields.add("pending_read")
            elif m == RunsMetric.ERROR_RATE:
                fields.add("success_count")
                fields.add("failed_count")

        return list(fields)

    @staticmethod
    def _init_trend_bucket(metrics: List[RunsMetric]) -> Dict[str, Any]:
        bucket = {}
        for m in metrics:
            if m in [RunsMetric.TOTAL_COUNT, RunsMetric.AVG_DURATION, RunsMetric.AVG_DELAY,
                     RunsMetric.DURATION_PERCENTILES, RunsMetric.DELAY_PERCENTILES]:
                bucket["_total_count"] = 0
            if m in [RunsMetric.SUCCESS_COUNT, RunsMetric.SUCCESS_RATE, RunsMetric.ERROR_RATE]:
                bucket["_success_count"] = 0
            if m in [RunsMetric.FAILED_COUNT, RunsMetric.SUCCESS_RATE, RunsMetric.ERROR_RATE]:
                bucket["_failed_count"] = 0
            if m == RunsMetric.RETRY_COUNT:
                bucket["_retry_count"] = 0
            if m in [RunsMetric.AVG_DURATION, RunsMetric.MAX_DURATION, RunsMetric.MIN_DURATION]:
                bucket["_total_duration"] = 0.0
                bucket["_max_duration"] = None
                bucket["_min_duration"] = None
            if m in [RunsMetric.AVG_DELAY, RunsMetric.MAX_DELAY, RunsMetric.MIN_DELAY]:
                bucket["_total_delay"] = 0.0
                bucket["_max_delay"] = None
                bucket["_min_delay"] = None
            if m == RunsMetric.MAX_CONCURRENCY:
                bucket["_max_concurrency"] = 0
            if m == RunsMetric.DURATION_PERCENTILES:
                bucket["_duration_sketches"] = []
            if m == RunsMetric.DELAY_PERCENTILES:
                bucket["_delay_sketches"] = []
            if m == RunsMetric.BACKLOG:
                bucket["_backlog_sum"] = 0
                bucket["_backlog_count"] = 0
                bucket["_pending_read_sum"] = 0
        return bucket

    @staticmethod
    def _aggregate_trend_row(bucket: Dict[str, Any], row: Any, needed_fields: List[str]):
        if "total_count" in needed_fields and hasattr(row, 'total_count'):
            bucket["_total_count"] = bucket.get("_total_count", 0) + (row.total_count or 0)
        if "success_count" in needed_fields and hasattr(row, 'success_count'):
            bucket["_success_count"] = bucket.get("_success_count", 0) + (row.success_count or 0)
        if "failed_count" in needed_fields and hasattr(row, 'failed_count'):
            bucket["_failed_count"] = bucket.get("_failed_count", 0) + (row.failed_count or 0)
        if "retry_count" in needed_fields and hasattr(row, 'retry_count'):
            bucket["_retry_count"] = bucket.get("_retry_count", 0) + (row.retry_count or 0)
        if "total_duration" in needed_fields and hasattr(row, 'total_duration'):
            bucket["_total_duration"] = bucket.get("_total_duration", 0) + (row.total_duration or 0)
        if "total_delay" in needed_fields and hasattr(row, 'total_delay'):
            bucket["_total_delay"] = bucket.get("_total_delay", 0) + (row.total_delay or 0)

        if "max_duration" in needed_fields and hasattr(row, 'max_duration') and row.max_duration is not None:
            current = bucket.get("_max_duration")
            bucket["_max_duration"] = max(current, row.max_duration) if current is not None else row.max_duration
        if "min_duration" in needed_fields and hasattr(row, 'min_duration') and row.min_duration is not None:
            current = bucket.get("_min_duration")
            bucket["_min_duration"] = min(current, row.min_duration) if current is not None else row.min_duration
        if "max_delay" in needed_fields and hasattr(row, 'max_delay') and row.max_delay is not None:
            current = bucket.get("_max_delay")
            bucket["_max_delay"] = max(current, row.max_delay) if current is not None else row.max_delay
        if "min_delay" in needed_fields and hasattr(row, 'min_delay') and row.min_delay is not None:
            current = bucket.get("_min_delay")
            bucket["_min_delay"] = min(current, row.min_delay) if current is not None else row.min_delay
        if "running_concurrency" in needed_fields and hasattr(row, 'running_concurrency'):
            bucket["_max_concurrency"] = max(bucket.get("_max_concurrency", 0), row.running_concurrency or 0)

        if "duration_sketch" in needed_fields and hasattr(row, 'duration_sketch') and row.duration_sketch:
            if "_duration_sketches" not in bucket:
                bucket["_duration_sketches"] = []
            bucket["_duration_sketches"].append(row.duration_sketch)

        if "delay_sketch" in needed_fields and hasattr(row, 'delay_sketch') and row.delay_sketch:
            if "_delay_sketches" not in bucket:
                bucket["_delay_sketches"] = []
            bucket["_delay_sketches"].append(row.delay_sketch)

        if "backlog" in needed_fields and hasattr(row, 'backlog'):
            bucket["_backlog_sum"] = bucket.get("_backlog_sum", 0) + (row.backlog or 0)
            bucket["_backlog_count"] = bucket.get("_backlog_count", 0) + 1
        if "pending_read" in needed_fields and hasattr(row, 'pending_read'):
            bucket["_pending_read_sum"] = bucket.get("_pending_read_sum", 0) + (row.pending_read or 0)

    @staticmethod
    def _merge_sketches(sketch_bytes_list: List[bytes]) -> Optional[Any]:
        if not DDSKETCH_AVAILABLE or not sketch_bytes_list:
            return None

        try:
            merged = DDSketch(relative_accuracy=webui_config.ddsketch_relative_accuracy)
            for sketch_bytes in sketch_bytes_list:
                if sketch_bytes:
                    sketch = pickle.loads(sketch_bytes)
                    merged.merge(sketch)

            return merged if merged.count > 0 else None
        except Exception as e:
            logger.warning(f"合并 DDSketch 失败: {e}")
            return None

    @staticmethod
    def _compute_percentiles_from_sketch(sketch: Optional[Any]) -> Dict[str, Optional[float]]:
        if sketch is None or not DDSKETCH_AVAILABLE:
            return {"p50": None, "p90": None, "p99": None}

        try:
            if sketch.count == 0:
                return {"p50": None, "p90": None, "p99": None}

            return {
                "p50": round(sketch.get_quantile_value(0.5), 1),
                "p90": round(sketch.get_quantile_value(0.9), 1),
                "p99": round(sketch.get_quantile_value(0.99), 1),
            }
        except Exception as e:
            logger.warning(f"从 DDSketch 计算分位数失败: {e}")
            return {"p50": None, "p90": None, "p99": None}

    @classmethod
    def _compute_trend_metrics(cls, bucket: Dict[str, Any], metrics: List[RunsMetric]) -> Dict[str, Any]:
        result = {}
        for m in metrics:
            if m == RunsMetric.TOTAL_COUNT:
                result["total_count"] = bucket.get("_total_count", 0)
            elif m == RunsMetric.SUCCESS_COUNT:
                result["success_count"] = bucket.get("_success_count", 0)
            elif m == RunsMetric.FAILED_COUNT:
                result["failed_count"] = bucket.get("_failed_count", 0)
            elif m == RunsMetric.RETRY_COUNT:
                result["retry_count"] = bucket.get("_retry_count", 0)
            elif m == RunsMetric.AVG_DURATION:
                total = bucket.get("_total_count", 0)
                duration = bucket.get("_total_duration", 0)
                result["avg_duration"] = round(duration / total, 4) if total > 0 else 0
            elif m == RunsMetric.MAX_DURATION:
                result["max_duration"] = bucket.get("_max_duration")
            elif m == RunsMetric.MIN_DURATION:
                result["min_duration"] = bucket.get("_min_duration")
            elif m == RunsMetric.AVG_DELAY:
                total = bucket.get("_total_count", 0)
                delay = bucket.get("_total_delay", 0)
                result["avg_delay"] = round(delay / total, 4) if total > 0 else 0
            elif m == RunsMetric.MAX_DELAY:
                result["max_delay"] = bucket.get("_max_delay")
            elif m == RunsMetric.MIN_DELAY:
                result["min_delay"] = bucket.get("_min_delay")
            elif m == RunsMetric.MAX_CONCURRENCY:
                result["max_concurrency"] = bucket.get("_max_concurrency", 0)
            elif m == RunsMetric.SUCCESS_RATE:
                success = bucket.get("_success_count", 0)
                failed = bucket.get("_failed_count", 0)
                total = success + failed
                result["success_rate"] = round((success / total) * 100, 2) if total > 0 else 0
            elif m == RunsMetric.DURATION_PERCENTILES:
                duration_sketches = bucket.get("_duration_sketches", [])
                merged_sketch = cls._merge_sketches(duration_sketches)
                percentiles = cls._compute_percentiles_from_sketch(merged_sketch)
                result["duration_p50"] = percentiles["p50"]
                result["duration_p90"] = percentiles["p90"]
                result["duration_p99"] = percentiles["p99"]
            elif m == RunsMetric.DELAY_PERCENTILES:
                delay_sketches = bucket.get("_delay_sketches", [])
                merged_sketch = cls._merge_sketches(delay_sketches)
                percentiles = cls._compute_percentiles_from_sketch(merged_sketch)
                result["delay_p50"] = percentiles["p50"]
                result["delay_p90"] = percentiles["p90"]
                result["delay_p99"] = percentiles["p99"]
            elif m == RunsMetric.BACKLOG:
                backlog_count = bucket.get("_backlog_count", 0)
                if backlog_count > 0:
                    result["backlog"] = round(bucket.get("_backlog_sum", 0) / backlog_count)
                    result["pending_read"] = round(bucket.get("_pending_read_sum", 0) / backlog_count)
                else:
                    result["backlog"] = 0
                    result["pending_read"] = 0
            elif m == RunsMetric.ERROR_RATE:
                success = bucket.get("_success_count", 0)
                failed = bucket.get("_failed_count", 0)
                total = success + failed
                result["error_rate"] = round((failed / total) * 100, 2) if total > 0 else 0

        return result
