import asyncio
import os
import socket
import sys
import uuid
from contextlib import suppress
from typing import Any

import aiohttp

from astrbot.core import db_helper, logger
from astrbot.core.config import VERSION


class Metric:
    _iid_cache = None
    _has_uploaded_once = False
    _upload_interval_seconds = 10 * 60
    _max_pending_metric_groups = 64
    _counter_fields = {"llm_tick", "msg_event_tick"}
    _pending_metrics: dict[tuple[tuple[str, str], ...], dict[str, Any]] = {}
    _flush_task: asyncio.Task | None = None
    _lock: asyncio.Lock | None = None
    _lock_loop: asyncio.AbstractEventLoop | None = None

    @staticmethod
    def _is_disabled() -> bool:
        """检查是否禁用指标上传（配置或环境变量）"""
        if os.environ.get("ASTRBOT_DISABLE_METRICS", "0") == "1":
            return True
        try:
            from astrbot.core import astrbot_config

            return astrbot_config.get("disable_metrics", False)
        except (ImportError, AttributeError, KeyError):
            return False

    @staticmethod
    def get_installation_id():
        """获取或创建一个唯一的安装ID"""
        if Metric._iid_cache is not None:
            return Metric._iid_cache

        config_dir = os.path.join(os.path.expanduser("~"), ".astrbot")
        id_file = os.path.join(config_dir, ".installation_id")

        if os.path.exists(id_file):
            try:
                with open(id_file) as f:
                    Metric._iid_cache = f.read().strip()
                    return Metric._iid_cache
            except Exception:
                pass
        try:
            os.makedirs(config_dir, exist_ok=True)
            installation_id = str(uuid.uuid4())
            with open(id_file, "w") as f:
                f.write(installation_id)
            Metric._iid_cache = installation_id
            return installation_id
        except Exception:
            Metric._iid_cache = "null"
            return "null"

    @staticmethod
    def _get_lock() -> asyncio.Lock:
        loop = asyncio.get_running_loop()
        if Metric._lock is None or Metric._lock_loop is not loop:
            Metric._lock = asyncio.Lock()
            Metric._lock_loop = loop
        return Metric._lock

    @staticmethod
    def _format_group_value(value: Any) -> str:
        return repr(value)

    @staticmethod
    def _get_metric_group_key(kwargs: dict[str, Any]) -> tuple[tuple[str, str], ...]:
        return tuple(
            sorted(
                (key, Metric._format_group_value(value))
                for key, value in kwargs.items()
                if key not in Metric._counter_fields
            )
        )

    @staticmethod
    def _get_metric_group_fields(kwargs: dict[str, Any]) -> dict[str, Any]:
        return {
            key: value
            for key, value in kwargs.items()
            if key not in Metric._counter_fields
        }

    @staticmethod
    def _coerce_counter(value: Any) -> int:
        try:
            return int(value)
        except (TypeError, ValueError):
            return 0

    @staticmethod
    def _ensure_flush_task_locked() -> None:
        if Metric._flush_task is None or Metric._flush_task.done():
            Metric._flush_task = asyncio.create_task(Metric._flush_periodically())

    @staticmethod
    async def _save_platform_stats(kwargs: dict[str, Any]) -> None:
        try:
            if "adapter_name" in kwargs:
                await db_helper.insert_platform_stats(
                    platform_id=kwargs["adapter_name"],
                    platform_type=kwargs.get("adapter_type", "unknown"),
                )
        except Exception as e:
            logger.error(f"保存指标到数据库失败: {e}")

    @staticmethod
    async def _add_pending_metrics(kwargs: dict[str, Any]) -> None:
        key = Metric._get_metric_group_key(kwargs)
        immediate_metrics = None
        should_flush = False
        lock = Metric._get_lock()
        async with lock:
            if not Metric._has_uploaded_once:
                Metric._has_uploaded_once = True
                immediate_metrics = dict(kwargs)
            else:
                pending = Metric._pending_metrics.setdefault(
                    key,
                    Metric._get_metric_group_fields(kwargs),
                )
                for counter_field in Metric._counter_fields:
                    if counter_field in kwargs:
                        pending[counter_field] = pending.get(
                            counter_field,
                            0,
                        ) + Metric._coerce_counter(kwargs[counter_field])
                Metric._ensure_flush_task_locked()
                should_flush = (
                    len(Metric._pending_metrics) > Metric._max_pending_metric_groups
                )

        if immediate_metrics is not None:
            await Metric._post_metrics(immediate_metrics)
            return
        if should_flush:
            await Metric.flush()

    @staticmethod
    async def _flush_periodically() -> None:
        try:
            while True:
                await asyncio.sleep(Metric._upload_interval_seconds)
                await Metric.flush()

                lock = Metric._get_lock()
                async with lock:
                    if not Metric._pending_metrics:
                        Metric._flush_task = None
                        return
        except asyncio.CancelledError:
            raise
        except Exception:
            pass
        finally:
            current_task = asyncio.current_task()
            with suppress(RuntimeError):
                lock = Metric._get_lock()
                async with lock:
                    if Metric._flush_task is current_task:
                        Metric._flush_task = None

    @staticmethod
    async def flush() -> None:
        """Flush pending metrics immediately."""
        lock = Metric._get_lock()
        async with lock:
            pending_metrics = list(Metric._pending_metrics.values())
            Metric._pending_metrics = {}

        for metrics_data in pending_metrics:
            await Metric._post_metrics(metrics_data)
            await asyncio.sleep(0.25)

    @staticmethod
    async def _post_metrics(metrics_data: dict[str, Any]) -> None:
        if Metric._is_disabled():
            return

        base_url = "https://tickstats.soulter.top/api/metric/90a6c2a1"
        payload_metrics = dict(metrics_data)
        payload_metrics["v"] = VERSION
        payload_metrics["os"] = sys.platform
        try:
            payload_metrics["hn"] = socket.gethostname()
        except Exception:
            pass
        try:
            payload_metrics["iid"] = Metric.get_installation_id()
        except Exception:
            pass
        payload = {"metrics_data": payload_metrics}

        try:
            async with aiohttp.ClientSession(trust_env=True) as session:
                async with session.post(base_url, json=payload, timeout=3) as response:
                    if response.status != 200:
                        pass
        except Exception:
            pass

    @staticmethod
    async def upload(**kwargs) -> None:
        """上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。

        Powered by TickStats.
        """
        if Metric._is_disabled():
            return

        await Metric._save_platform_stats(kwargs)
        await Metric._add_pending_metrics(dict(kwargs))
