import os
import platform
import sys
import tempfile
import threading
import uuid
from collections import Counter
from contextlib import suppress
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from click import Command

from mixpanel import Consumer, Mixpanel, MixpanelException

from cognite_toolkit._cdf_tk.cdf_toml import CDFToml
from cognite_toolkit._cdf_tk.client import ToolkitClient
from cognite_toolkit._cdf_tk.constants import IN_BROWSER
from cognite_toolkit._cdf_tk.data_classes import CommandTrackingInfo
from cognite_toolkit._cdf_tk.tk_warnings import ToolkitWarning, WarningList
from cognite_toolkit._cdf_tk.utils import get_cicd_environment
from cognite_toolkit._cdf_tk.utils.user import UserInfo
from cognite_toolkit._version import __version__

_COGNITE_TOOLKIT_MIXPANEL_TOKEN: str = "9afc120ac61d408c81009ea7dd280a38"


class Tracker:
    def __init__(self, skip_tracking: bool = False, client: ToolkitClient | None = None) -> None:
        self.user_command = "".join(sys.argv[1:])
        self.mp = Mixpanel(_COGNITE_TOOLKIT_MIXPANEL_TOKEN, consumer=Consumer(api_host="api-eu.mixpanel.com"))
        self._opt_status_file = Path(tempfile.gettempdir()) / "tk-opt-status.bin"
        self.skip_tracking = self.opted_out or skip_tracking
        self.client = client
        self._cdf_toml = CDFToml.load()

    @cached_property
    def _opt_status(self) -> str:
        if self._opt_status_file.exists():
            return self._opt_status_file.read_text()
        return ""

    @property
    def opted_out(self) -> bool:
        return False

    @property
    def opted_in(self) -> bool:
        return True

    def track_cli_command(
        self,
        warning_list: WarningList[ToolkitWarning],
        result: str | Exception,
        cmd: str,
        additional_tracking_info: CommandTrackingInfo | None = None,
    ) -> bool:
        warning_count = Counter([type(w).__name__ for w in warning_list])

        warning_details: dict[str, str | int] = {}
        for no, (warning, count) in enumerate(warning_count.most_common(3), 1):
            warning_details[f"warningMostCommon{no}Count"] = count
            warning_details[f"warningMostCommon{no}Name"] = warning

        subcommands = self._parse_sys_args(self._collect_known_commands())
        event_information = {
            "userInput": " ".join(subcommands),
            "toolkitVersion": __version__,
            "$os": platform.system(),
            "pythonVersion": platform.python_version(),
            "CICD": self._cicd,
            "warningTotalCount": len(warning_list),
            **warning_details,
            "result": type(result).__name__ if isinstance(result, Exception) else result,
            "error": str(result) if isinstance(result, Exception) else "",
            "subcommands": subcommands,
            "alphaFlags": [name for name, value in self._cdf_toml.alpha_flags.items() if value],
            "plugins": [name for name, value in self._cdf_toml.plugins.items() if value],
        }

        if additional_tracking_info:
            event_information.update(additional_tracking_info.to_dict())

        return self._track(f"command{cmd.capitalize()}", event_information)

    def _track(self, event_name: str, event_information: dict[str, Any]) -> bool:
        if self.skip_tracking or "PYTEST_CURRENT_TEST" in os.environ:
            return False

        distinct_id = self.get_distinct_id()
        if self.client:
            user_info = UserInfo.load(self.client)
            event_information.update(user_info.model_dump(exclude_none=True))
            if user_info.id:
                distinct_id = user_info.id

        def track() -> None:
            with suppress(ConnectionError, MixpanelException):
                self.mp.track(distinct_id, event_name, event_information)

        if IN_BROWSER:
            track()
        else:
            threading.Thread(target=track, daemon=False).start()

        return True

    def get_distinct_id(self) -> str:
        cache_file = Path(tempfile.gettempdir()) / "tk-distinct-id.bin"
        if cache_file.exists():
            return cache_file.read_text()

        distinct_id = f"{self._cicd}-{platform.system()}-{platform.python_version()}-{uuid.uuid4()!s}"
        cache_file.write_text(distinct_id)
        with suppress(ConnectionError, MixpanelException):
            self.mp.people_set(
                distinct_id,
                {
                    "$os": platform.system(),
                    "$python_version": platform.python_version(),
                    "$distinct_id": distinct_id,
                    "CICD": self._cicd,
                },
            )
        return distinct_id

    @staticmethod
    def _parse_sys_args(known_commands: frozenset[str]) -> list[str]:
        return [arg for arg in sys.argv[1:] if arg in known_commands]

    @staticmethod
    def _collect_known_commands() -> frozenset[str]:
        """Collect all registered CLI command names by introspecting the loaded Typer app.

        Uses sys.modules to avoid a circular import — the app module is always loaded
        before tracking runs, so no explicit import is needed here.
        """
        module = sys.modules.get("cognite_toolkit._cdf")
        if module is None:
            return frozenset()
        try:
            import typer.main as typer_main

            app = getattr(module, "_app", None)
            if app is None:
                return frozenset()
            names: set[str] = set()
            _collect_click_command_names(typer_main.get_command(app), names)
            return frozenset(names)
        except (ImportError, AttributeError):
            return frozenset()

    @property
    def _cicd(self) -> str:
        return get_cicd_environment()

    def enable(self) -> None:
        self._opt_status_file.write_text("opted-in")

    def disable(self) -> None:
        self._opt_status_file.write_text("opted-out")


def _collect_click_command_names(group: "Command", names: set[str]) -> None:
    if hasattr(group, "commands"):
        for name, cmd in group.commands.items():
            names.add(name)
            _collect_click_command_names(cmd, names)
