import os
from typing import TYPE_CHECKING, Any, Optional

import sentry_sdk
from django.utils.translation import override as override_language
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.tornado import TornadoIntegration
from sentry_sdk.utils import capture_internal_exceptions

from version import ZULIP_VERSION
from zproject.config import DEPLOY_ROOT

if TYPE_CHECKING:
    from sentry_sdk._types import Event, Hint


def add_context(event: "Event", hint: "Hint") -> Optional["Event"]:
    if "exc_info" in hint:
        _, exc_value, _ = hint["exc_info"]
        # Ignore GeneratorExit, KeyboardInterrupt, and SystemExit exceptions
        if not isinstance(exc_value, Exception):
            return None
    from django.conf import settings

    from zerver.models.users import get_user_profile_by_id

    with capture_internal_exceptions():
        # event.user is the user context, from Sentry, which is
        # pre-populated with some keys via its Django integration:
        # https://docs.sentry.io/platforms/python/guides/django/enriching-error-data/additional-data/identify-user/
        event.setdefault("tags", {})
        user_info = event.get("user", {})
        user_id = user_info.get("id")
        if isinstance(user_id, str):
            user_profile = get_user_profile_by_id(int(user_id))
            event["tags"]["realm"] = user_info["realm"] = user_profile.realm.string_id or "root"
            with override_language(settings.LANGUAGE_CODE):
                # str() to force the lazy-translation to apply now,
                # since it won't serialize into json for Sentry otherwise
                user_info["role"] = str(user_profile.get_role_name())

        # These are PII, and should be scrubbed
        if "username" in user_info:
            del user_info["username"]
        if "email" in user_info:
            del user_info["email"]

    return event


def traces_sampler(sampling_context: dict[str, Any]) -> float | bool:
    from django.conf import settings

    queue = sampling_context.get("queue")
    if queue is not None and isinstance(queue, str):
        if isinstance(settings.SENTRY_TRACE_WORKER_RATE, dict):
            return settings.SENTRY_TRACE_WORKER_RATE.get(queue, 0.0)
        else:
            return settings.SENTRY_TRACE_WORKER_RATE
    else:
        return settings.SENTRY_TRACE_RATE


def setup_sentry(dsn: str | None, environment: str) -> None:
    from django.conf import settings

    if not dsn:
        return

    sentry_release = ZULIP_VERSION
    if os.path.exists(os.path.join(DEPLOY_ROOT, "sentry-release")):
        with open(os.path.join(DEPLOY_ROOT, "sentry-release")) as sentry_release_file:
            sentry_release = sentry_release_file.readline().strip()
    integrations = [
        RedisIntegration(),
        SqlalchemyIntegration(),
    ]
    disabled_integrations = []
    if settings.RUNNING_INSIDE_TORNADO:
        integrations.append(TornadoIntegration())
        disabled_integrations.append(DjangoIntegration())
    else:
        integrations.append(DjangoIntegration())
    sentry_sdk.init(
        dsn=dsn,
        environment=environment,
        release=sentry_release,
        integrations=integrations,
        disabled_integrations=disabled_integrations,
        before_send=add_context,
        # Increase possible max wait to send exceptions during
        # shutdown, from 2 to 10; potentially-large exceptions are of
        # value to catch during shutdown.
        shutdown_timeout=10,
        # Because we strip the email/username from the Sentry data
        # above, the effect of this flag is that the requests/users
        # involved in exceptions will be identified in Sentry only by
        # their IP address, user ID, realm, and role.  We consider
        # this an appropriate balance between avoiding Sentry getting
        # PII while having the identifiers needed to determine that an
        # exception only affects a small subset of users or realms.
        send_default_pii=True,
        traces_sampler=traces_sampler,
        profiles_sample_rate=settings.SENTRY_PROFILE_RATE,
    )

    # Ignore all of the loggers from django.security that are for user
    # errors; see https://docs.djangoproject.com/en/5.0/ref/exceptions/#suspiciousoperation
    ignore_logger("django.security.SuspiciousOperation")
    ignore_logger("django.security.DisallowedHost")
    ignore_logger("django.security.DisallowedModelAdminLookup")
    ignore_logger("django.security.DisallowedModelAdminToField")
    ignore_logger("django.security.DisallowedRedirect")
    ignore_logger("django.security.InvalidSessionKey")
    ignore_logger("django.security.RequestDataTooBig")
    ignore_logger("django.security.SuspiciousFileOperation")
    ignore_logger("django.security.SuspiciousMultipartForm")
    ignore_logger("django.security.SuspiciousSession")
    ignore_logger("django.security.TooManyFieldsSent")
