import datetime
import logging
import typing

from django.core.cache import cache
from django.db.models import Prefetch
from django.utils import timezone
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
from apps.alerts.models import AlertGroup
from apps.alerts.models.alert_group import PagedUser
from apps.slack.models import SlackMessage
from apps.telegram.models import TelegramMessage
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin

from .alert import AlertSerializer
from .alert_receive_channel import FastAlertReceiveChannelSerializer
from .alerts_field_cache_buster_mixin import AlertsFieldCacheBusterMixin
from .user import FastUserSerializer, UserShortSerializer

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


class ExternalURL(typing.TypedDict):
    integration: str
    integration_type: str
    external_id: str
    url: str


class RenderForWeb(typing.TypedDict):
    title: str
    message: str
    image_url: str | None
    source_link: str | None


class EmptyRenderForWeb(typing.TypedDict):
    pass


class AlertGroupFieldsCacheSerializerMixin(AlertsFieldCacheBusterMixin):
    CACHE_KEY_FORMAT_TEMPLATE = "{field_name}_alert_group_{object_id}"

    @classmethod
    def get_or_set_web_template_field(
        cls,
        obj,
        last_alert,
        field_name,
        renderer_class,
        cache_lifetime=60 * 60 * 24,
    ):
        CACHE_KEY = cls.calculate_cache_key(field_name, obj)
        cached_field = cache.get(CACHE_KEY, None)

        web_templates_modified_at = obj.channel.web_templates_modified_at
        last_alert_created_at = last_alert.created_at

        # use cache only if cache exists
        # and cache was created after the last alert created
        # and either web templates never modified
        # or cache was created after templates were modified
        if (
            cached_field is not None
            and cached_field.get("cache_created_at") > last_alert_created_at
            and (web_templates_modified_at is None or cached_field.get("cache_created_at") > web_templates_modified_at)
        ):
            field = cached_field.get(field_name)
        else:
            field = renderer_class(obj, last_alert).render()
            cache.set(CACHE_KEY, {"cache_created_at": timezone.now(), field_name: field}, cache_lifetime)

        return field


class AlertGroupLabelSerializer(serializers.Serializer):
    class KeySerializer(serializers.Serializer):
        id = serializers.CharField(source="key_name")
        name = serializers.CharField(source="key_name")

    class ValueSerializer(serializers.Serializer):
        id = serializers.CharField(source="value_name")
        name = serializers.CharField(source="value_name")

    key = KeySerializer(source="*")
    value = ValueSerializer(source="*")


class ShortAlertGroupSerializer(AlertGroupFieldsCacheSerializerMixin, serializers.ModelSerializer):
    pk = serializers.CharField(read_only=True, source="public_primary_key")
    alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
    render_for_web = serializers.SerializerMethodField()

    class Meta:
        model = AlertGroup
        fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
        read_only_fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]

    def get_render_for_web(self, obj: "AlertGroup") -> RenderForWeb | EmptyRenderForWeb:
        last_alert = obj.alerts.last()
        if last_alert is None:
            return {}
        return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
            obj,
            last_alert,
            AlertGroupFieldsCacheSerializerMixin.RENDER_FOR_WEB_FIELD_NAME,
            AlertGroupWebRenderer,
        )


class AlertGroupListSerializer(
    EagerLoadingMixin, AlertGroupFieldsCacheSerializerMixin, serializers.ModelSerializer[AlertGroup]
):
    pk = serializers.CharField(read_only=True, source="public_primary_key")
    alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
    status = serializers.ReadOnlyField()
    resolved_by_user = FastUserSerializer(required=False)
    acknowledged_by_user = FastUserSerializer(required=False)
    silenced_by_user = FastUserSerializer(required=False)
    related_users = serializers.SerializerMethodField()
    dependent_alert_groups = ShortAlertGroupSerializer(many=True)
    root_alert_group = ShortAlertGroupSerializer()
    team = TeamPrimaryKeyRelatedField(source="channel.team", allow_null=True)

    alerts_count = serializers.IntegerField(read_only=True)
    render_for_web = serializers.SerializerMethodField()

    labels = AlertGroupLabelSerializer(many=True, read_only=True)

    PREFETCH_RELATED = [
        "dependent_alert_groups",
        "log_records__author",
        "labels",
        Prefetch(
            "slack_messages",
            queryset=SlackMessage.objects.select_related("_slack_team_identity").order_by("created_at")[:1],
            to_attr="prefetched_slack_messages",
        ),
        Prefetch(
            "telegram_messages",
            queryset=TelegramMessage.objects.filter(
                chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE
            ).order_by("id")[:1],
            to_attr="prefetched_telegram_messages",
        ),
    ]

    SELECT_RELATED = [
        "channel__organization",
        "channel__team",
        "root_alert_group",
        "resolved_by_user",
        "acknowledged_by_user",
        "silenced_by_user",
    ]

    class Meta:
        model = AlertGroup
        fields = [
            "pk",
            "alerts_count",
            "inside_organization_number",
            "alert_receive_channel",
            "resolved",
            "resolved_by",
            "resolved_by_user",
            "resolved_at",
            "acknowledged_at",
            "acknowledged",
            "acknowledged_on_source",
            "acknowledged_at",
            "acknowledged_by_user",
            "silenced",
            "silenced_by_user",
            "silenced_at",
            "silenced_until",
            "started_at",
            "silenced_until",
            "related_users",
            "render_for_web",
            "dependent_alert_groups",
            "root_alert_group",
            "status",
            "declare_incident_link",
            "team",
            "grafana_incident_id",
            "labels",
            "permalinks",
        ]

    def get_render_for_web(self, obj: "AlertGroup") -> RenderForWeb | EmptyRenderForWeb:
        if not obj.last_alert:
            return {}
        return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
            obj,
            obj.last_alert,
            AlertGroupFieldsCacheSerializerMixin.RENDER_FOR_WEB_FIELD_NAME,
            AlertGroupWebRenderer,
        )

    @extend_schema_field(UserShortSerializer(many=True))
    def get_related_users(self, obj: "AlertGroup"):
        from apps.user_management.models import User

        users_ids: typing.Set[str] = set()
        users: typing.List[User] = []

        # add resolved and acknowledged by_user explicitly because logs are already prefetched
        # when def acknowledge/resolve are called in view.
        if obj.resolved_by_user:
            users_ids.add(obj.resolved_by_user.public_primary_key)
            users.append(obj.resolved_by_user)

        if obj.acknowledged_by_user and obj.acknowledged_by_user.public_primary_key not in users_ids:
            users_ids.add(obj.acknowledged_by_user.public_primary_key)
            users.append(obj.acknowledged_by_user)

        if obj.silenced_by_user and obj.silenced_by_user.public_primary_key not in users_ids:
            users_ids.add(obj.silenced_by_user.public_primary_key)
            users.append(obj.silenced_by_user)

        for log_record in obj.log_records.all():
            if log_record.author is not None and log_record.author.public_primary_key not in users_ids:
                users.append(log_record.author)
                users_ids.add(log_record.author.public_primary_key)
        return UserShortSerializer(users, context=self.context, many=True).data


class AlertGroupSerializer(AlertGroupListSerializer):
    alerts = serializers.SerializerMethodField("get_limited_alerts")
    last_alert_at = serializers.SerializerMethodField()
    paged_users = serializers.SerializerMethodField()
    external_urls = serializers.SerializerMethodField()

    class Meta(AlertGroupListSerializer.Meta):
        fields = AlertGroupListSerializer.Meta.fields + [
            "alerts",
            "render_after_resolve_report_json",
            "slack_permalink",  # TODO: make plugin frontend use "permalinks" field to get Slack link
            "last_alert_at",
            "paged_users",
            "external_urls",
        ]

    def get_last_alert_at(self, obj: "AlertGroup") -> datetime.datetime:
        last_alert = obj.alerts.last()

        if not last_alert:
            return obj.started_at

        return last_alert.created_at

    @extend_schema_field(AlertSerializer(many=True))
    def get_limited_alerts(self, obj: "AlertGroup"):
        """
        Overriding default alerts because there are alert_groups with thousands of them.
        It's just too slow, we need to cut here.
        """
        alerts = obj.alerts.order_by("-pk")[:100]
        return AlertSerializer(alerts, many=True).data

    def get_paged_users(self, obj: "AlertGroup") -> typing.List[PagedUser]:
        return obj.get_paged_users()

    def get_external_urls(self, obj: "AlertGroup") -> typing.List[ExternalURL]:
        external_urls = []
        external_ids = obj.external_ids.all()
        for external_id in external_ids:
            source_integration = external_id.source_alert_receive_channel
            get_url = getattr(source_integration.config, "get_url", None)
            if get_url:
                url = source_integration.config.get_url(source_integration, external_id.value)
                external_urls.append(
                    {
                        "integration": source_integration.public_primary_key,
                        "integration_type": source_integration.integration,
                        "external_id": external_id.value,
                        "url": url,
                    }
                )
        return external_urls
