"""sentry utilities."""

import logging
import os
import platform
import typing as ty
import warnings
from contextlib import suppress
from functools import lru_cache
from importlib import metadata
from subprocess import run

from loguru import logger
from sentry_sdk.integrations.loguru import LoggingLevels, LoguruIntegration

try:
    from rich import print as pprint
except ImportError:  # pragma: no cover
    from pprint import pprint

from getpass import getuser

import sentry_sdk
from koyo.utilities import running_as_pyinstaller_app

sentry_sdk.set_user({"username": getuser(), "ip_address": "{{auto}}"})

# disable logging from sentry_sdk
logging.getLogger("sentry_sdk").setLevel(logging.ERROR)
warnings.filterwarnings("ignore", category=ResourceWarning, module="sentry_sdk")

VERSION: str = os.getenv("QTEXTRA_TELEMETRY_VERSION", "")
SENTRY_DSN: str = os.getenv("QTEXTRA_TELEMETRY_SENTRY_DSN", "")
SHOW_HOSTNAME: bool = os.getenv("QTEXTRA_TELEMETRY_SHOW_HOSTNAME", "0") in ("1", "True")
SHOW_LOCALS: bool = os.getenv("QTEXTRA_TELEMETRY_SHOW_LOCALS", "1") in ("1", "True")
DEBUG: bool = os.getenv("QTEXTRA_TELEMETRY_DEBUG") in ("1", "True")
PACKAGE: str = os.getenv("QTEXTRA_TELEMETRY_PACKAGE", "qtextra")


def strip_sensitive_data(event: dict, hint: dict):
    """Pre-send hook to strip sensitive data from `event` dict.
    https://docs.sentry.io/platforms/python/configuration/filtering/#filtering-error-events.
    """
    # modify event here

    # strip `abs_paths` from stack_trace to hide local paths
    with suppress(KeyError, IndexError):
        for exc in event["exception"]["values"]:
            for frame in exc["stacktrace"]["frames"]:
                frame.pop("abs_path", None)
        # only include the name of the executable in sys.argv (remove paths)
        if args := event["extra"]["sys.argv"]:
            args[0] = args[0].split(os.sep)[-1]
    if DEBUG:  # pragma: no cover
        pprint(event)
    logger.debug("Sending sentry event")
    return event


def is_editable_install(dist_name: str) -> bool:
    """Return True if `dist_name` is installed as editable.
    i.e: if the package isn't in site-packages or user site-packages.
    """
    from site import getsitepackages, getusersitepackages

    dist = metadata.distribution(dist_name)
    installed_paths = [*getsitepackages(), getusersitepackages()]
    root = str(dist.locate_file(""))
    return all(loc not in root for loc in installed_paths)


def try_get_git_sha(package: ty.Optional[str] = None) -> str:
    """Try to return a git sha, for `dist_name` and detect if dirty.
    Return empty string on failure.
    """
    package = package or PACKAGE
    try:
        ff = metadata.distribution(package).locate_file("")
        out = run(["git", "-C", ff, "rev-parse", "HEAD"], capture_output=True)
        if out.returncode:  # pragma: no cover
            return ""
        sha = out.stdout.decode().strip()
        # exit with 1 if there are differences and 0 means no differences
        # disallow external diff drivers
        out = run(["git", "-C", ff, "diff", "--no-ext-diff", "--quiet", "--exit-code"])
        if out.returncode:  # pragma: no cover
            sha += "-dirty"
        return sha
    except Exception:  # pragma: no cover
        return ""


@lru_cache
def get_release(package: ty.Optional[str] = None) -> str:
    """Get the current release string for `package`.
    If the package is an editable install, it will return the current git sha.
    Otherwise return version string from package metadata.
    """
    package = package or PACKAGE
    try:
        with suppress(ModuleNotFoundError, ImportError):
            if is_editable_install(package):
                sha = try_get_git_sha(package)
                if sha:
                    return sha
            return metadata.version(package)
    except Exception:
        pass
    return "UNDETECTED"


def _get_tags() -> dict:
    tags = {"platform.system": platform.system()}

    with suppress(ImportError):
        from napari.utils.info import _sys_name

        sys = _sys_name()
        if sys:
            tags["system_name"] = sys

    with suppress(ImportError):
        import qtpy

        tags["qtpy.API_NAME"] = qtpy.API_NAME
        tags["qtpy.QT_VERSION"] = qtpy.QT_VERSION

    with suppress(ModuleNotFoundError, ImportError):
        tags["editable_install"] = str(is_editable_install(PACKAGE))
    tags["frozen"] = running_as_pyinstaller_app()
    tags["username"] = getuser()
    return tags


def get_sample_event(**kwargs) -> dict:
    """Return an example event as would be generated by an exception."""
    EVENT = {}

    def _transport(event: dict):
        nonlocal EVENT
        EVENT = event

    settings = SENTRY_SETTINGS.copy()
    settings["release"] = get_release()
    settings["dsn"] = ""
    settings["transport"] = _transport
    settings.update(kwargs)

    with sentry_sdk.Client(**settings) as client:
        with sentry_sdk.Hub(client) as hub:
            # remove locals that wouldn't really be there
            del settings, _transport, kwargs, client
            try:
                some_variable = 1  # noqa
                another_variable = "my_string"  # noqa
                1 / 0  # noqa
            except Exception:
                with sentry_sdk.new_scope() as scope:
                    for k, v in _get_tags().items():
                        scope.set_tag(k, v)
                    del v, k, scope
                    hub.capture_exception()

    with suppress(KeyError, IndexError):
        # remove the mock hub from the event
        frames = EVENT["exception"]["values"][0]["stacktrace"]["frames"]
        del frames[-1]["vars"]["hub"]
    return EVENT


SENTRY_LOGURU = LoguruIntegration(
    level=LoggingLevels.CRITICAL.value,  # Capture info and above as breadcrumbs
    event_level=LoggingLevels.CRITICAL.value,  # Send errors as events
)

INTEGRATIONS = [SENTRY_LOGURU]


SENTRY_SETTINGS = {
    "dsn": SENTRY_DSN,
    "release": VERSION,
    # When enabled, local variables are sent along with stackframes.
    # This can have a performance and PII impact.
    # Enabled by default on platforms where this is available.
    "include_local_variables": SHOW_LOCALS,
    # A number between 0 and 1, controlling the percentage chance
    # a given transaction will be sent to Sentry.
    # (0 represents 0% while 1 represents 100%.)
    # Applies equally to all transactions created in the app.
    # Either this or traces_sampler must be defined to enable tracing.
    "enable_tracing": True,
    "traces_sample_rate": 1.0,
    # When provided, the name of the server is sent along and persisted
    # in the event. For many integrations the server name actually
    # corresponds to the device hostname, even in situations where the
    # machine is not actually a server. Most SDKs will attempt to
    # auto-discover this value. (computer name: potentially PII)
    # "server_name": "",
    # If this flag is enabled, certain personally identifiable information (PII)
    # is added by active integrations. By default, no such data is sent.
    "send_default_pii": True,
    # This function is called with an SDK-specific event object, and can return a
    # modified event object or nothing to skip reporting the event.
    # This can be used, for instance, for manual PII stripping before sending.
    "before_send": strip_sensitive_data,
    "debug": DEBUG,
    # -------------------------
    "environment": platform.platform(),
    # max_breadcrumbs=DEFAULT_MAX_BREADCRUMBS,
    # shutdown_timeout=2,
    "integrations": INTEGRATIONS,
    # in_app_include=[],
    # in_app_exclude=[],
    # default_integrations=True,
    # dist=None,
    # transport=None,
    # transport_queue_size=DEFAULT_QUEUE_SIZE,
    # sample_rate=1.0,
    # http_proxy=None,
    # https_proxy=None,
    # ignore_errors=[],
    # max_request_body_size="medium",
    # before_breadcrumb=None,
    # attach_stacktrace=False,
    # ca_certs=None,
    # propagate_traces=True,
    # traces_sampler=None,
    # auto_session_tracking=True,
    "auto_enabling_integrations": False,
    "profiles_sample_rate": 1.0,
    "profiler_mode": "thread",
    # "_experiments": {
    #     "profiles_sample_rate": 1.0,
    # },
}
