import json
import logging
import typing
from json import JSONDecodeError

import requests
from celery.utils.log import get_task_logger
from django.conf import settings
from django.core.validators import MinLengthValidator
from django.db import models
from django.db.models import F
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from mirage import fields as mirage_fields
from requests.auth import HTTPBasicAuth

from apps.webhooks.utils import (
    InvalidWebhookData,
    InvalidWebhookHeaders,
    InvalidWebhookTrigger,
    InvalidWebhookUrl,
    apply_jinja_template_for_json,
    parse_url,
)
from common.jinja_templater import apply_jinja_template
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length

if typing.TYPE_CHECKING:
    from django.db.models.manager import RelatedManager

    from apps.alerts.models import EscalationPolicy

WEBHOOK_FIELD_PLACEHOLDER = "****************"
PUBLIC_WEBHOOK_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]

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


def generate_public_primary_key_for_webhook():
    prefix = "WH"
    new_public_primary_key = generate_public_primary_key(prefix)

    failure_counter = 0
    while Webhook.objects.filter(public_primary_key=new_public_primary_key).exists():
        new_public_primary_key = increase_public_primary_key_length(
            failure_counter=failure_counter, prefix=prefix, model_name="Webhook"
        )
        failure_counter += 1

    return new_public_primary_key


class WebhookSession(requests.Session):
    def send(self, request, **kwargs):
        parse_url(request.url)  # validate URL on every redirect
        return super().send(request, **kwargs)


class WebhookQueryset(models.QuerySet):
    def delete(self):
        self.update(deleted_at=timezone.now(), name=F("name") + "_deleted_" + F("public_primary_key"))


class WebhookManager(models.Manager):
    def get_queryset(self):
        return WebhookQueryset(self.model, using=self._db).filter(deleted_at=None)

    def hard_delete(self):
        return self.get_queryset().hard_delete()


class Webhook(models.Model):
    escalation_policies: "RelatedManager['EscalationPolicy']"

    objects = WebhookManager()
    objects_with_deleted = models.Manager()

    (
        TRIGGER_MANUAL,
        TRIGGER_ALERT_GROUP_CREATED,
        TRIGGER_ACKNOWLEDGE,
        TRIGGER_RESOLVE,
        TRIGGER_SILENCE,
        TRIGGER_UNSILENCE,
        TRIGGER_UNRESOLVE,
        TRIGGER_UNACKNOWLEDGE,
        TRIGGER_STATUS_CHANGE,
    ) = range(9)

    # Must be the same order as previous
    TRIGGER_TYPES = (
        (TRIGGER_MANUAL, "Manual or escalation step"),
        (TRIGGER_ALERT_GROUP_CREATED, "Alert Group Created"),
        (TRIGGER_ACKNOWLEDGE, "Acknowledged"),
        (TRIGGER_RESOLVE, "Resolved"),
        (TRIGGER_SILENCE, "Silenced"),
        (TRIGGER_UNSILENCE, "Unsilenced"),
        (TRIGGER_UNRESOLVE, "Unresolved"),
        (TRIGGER_UNACKNOWLEDGE, "Unacknowledged"),
        (TRIGGER_STATUS_CHANGE, "Status change"),
    )

    ALL_TRIGGER_TYPES = [i[0] for i in TRIGGER_TYPES]
    STATUS_CHANGE_TRIGGERS = {
        TRIGGER_ACKNOWLEDGE,
        TRIGGER_RESOLVE,
        TRIGGER_SILENCE,
        TRIGGER_UNSILENCE,
        TRIGGER_UNRESOLVE,
        TRIGGER_UNACKNOWLEDGE,
    }

    PUBLIC_TRIGGER_TYPES_MAP = {
        TRIGGER_MANUAL: "escalation",
        TRIGGER_ALERT_GROUP_CREATED: "alert group created",
        TRIGGER_ACKNOWLEDGE: "acknowledge",
        TRIGGER_RESOLVE: "resolve",
        TRIGGER_SILENCE: "silence",
        TRIGGER_UNSILENCE: "unsilence",
        TRIGGER_UNRESOLVE: "unresolve",
        TRIGGER_UNACKNOWLEDGE: "unacknowledge",
        TRIGGER_STATUS_CHANGE: "status change",
    }

    PUBLIC_ALL_TRIGGER_TYPES = [i for i in PUBLIC_TRIGGER_TYPES_MAP.values()]

    public_primary_key = models.CharField(
        max_length=20,
        validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
        unique=True,
        default=generate_public_primary_key_for_webhook,
    )

    organization = models.ForeignKey(
        "user_management.Organization", null=True, on_delete=models.CASCADE, related_name="webhooks", default=None
    )

    team = models.ForeignKey(
        "user_management.Team", null=True, on_delete=models.SET_NULL, related_name="webhooks", default=None
    )

    user = models.ForeignKey(
        "user_management.User", null=True, on_delete=models.CASCADE, related_name="webhooks", default=None
    )

    created_at = models.DateTimeField(auto_now_add=True)
    deleted_at = models.DateTimeField(blank=True, null=True)
    name = models.CharField(max_length=100, null=True, default=None)
    username = models.CharField(max_length=100, null=True, default=None)
    password = mirage_fields.EncryptedCharField(max_length=1000, null=True, default=None)
    authorization_header = mirage_fields.EncryptedCharField(max_length=2000, null=True, default=None)
    trigger_template = models.TextField(null=True, default=None)
    headers = models.TextField(null=True, default=None)
    url = models.TextField(null=True, default=None)
    data = models.TextField(null=True, default=None)
    forward_all = models.BooleanField(default=True)
    http_method = models.CharField(max_length=32, default="POST", null=True)
    trigger_type = models.IntegerField(choices=TRIGGER_TYPES, default=TRIGGER_MANUAL, null=True)
    is_webhook_enabled = models.BooleanField(null=True, default=True)
    # NOTE: integration_filter is deprecated (to be removed), use filtered_integrations instead
    integration_filter = models.JSONField(default=None, null=True, blank=True)
    filtered_integrations = models.ManyToManyField("alerts.AlertReceiveChannel", related_name="webhooks")
    is_legacy = models.BooleanField(null=True, default=False)
    preset = models.CharField(max_length=100, null=True, blank=True, default=None)

    is_from_connected_integration = models.BooleanField(null=True, default=False)

    class Meta:
        unique_together = ("name", "organization")

    def __str__(self):
        return str(self.name)

    def delete(self):
        # TODO: delete related escalation policies on delete, once implemented
        # self.escalation_policies.all().delete()
        self.deleted_at = timezone.now()
        # 100 - 22 = 78. 100 is max len of name field, and 22 is len of suffix _deleted_<public_primary_key>
        # So for case when user created an entry with maximum length name it is needed to trim it to 78 chars
        # to be able to add suffix.
        self.name = f"{self.name[:78]}_deleted_{self.public_primary_key}"
        self.save()

    def hard_delete(self):
        super().delete()

    def get_source_alert_receive_channel(self):
        """Return the webhook source channel if it is connected to an integration."""
        result = None
        if self.is_from_connected_integration:
            filtered_integration = (
                Webhook.filtered_integrations.through.objects.filter(
                    alertreceivechannel__additional_settings__isnull=False, webhook=self
                )
                .order_by("id")
                .first()
            )
            result = filtered_integration.alertreceivechannel if filtered_integration else None
        return result

    def build_request_kwargs(self, event_data, raise_data_errors=False):
        request_kwargs = {}
        if self.username and self.password:
            request_kwargs["auth"] = HTTPBasicAuth(self.username, self.password)

        request_kwargs["headers"] = {}
        if self.headers:
            try:
                rendered_headers = apply_jinja_template_for_json(
                    self.headers,
                    event_data,
                )
                request_kwargs["headers"] = json.loads(rendered_headers)
            except (JinjaTemplateError, JinjaTemplateWarning) as e:
                raise InvalidWebhookHeaders(e.fallback_message)
            except JSONDecodeError:
                raise InvalidWebhookHeaders("Template did not result in json/dict")

        if self.authorization_header:
            request_kwargs["headers"]["Authorization"] = self.authorization_header

        if self.http_method in ["POST", "PUT", "PATCH"]:
            if self.forward_all:
                request_kwargs["json"] = event_data
                if self.is_legacy:
                    request_kwargs["json"] = event_data["alert_payload"]
            elif self.data:
                context_data = event_data
                if self.is_legacy:
                    context_data = {
                        "alert_payload": event_data.get("alert_payload", {}),
                        "alert_group_id": event_data.get("alert_group_id"),
                    }
                try:
                    rendered_data = apply_jinja_template_for_json(
                        self.data,
                        context_data,
                    )
                    try:
                        request_kwargs["json"] = json.loads(rendered_data)
                    except (JSONDecodeError, TypeError):
                        # utf-8 encoding addresses https://github.com/grafana/oncall/issues/3831
                        request_kwargs["data"] = rendered_data.encode("utf-8")
                except (JinjaTemplateError, JinjaTemplateWarning) as e:
                    if raise_data_errors:
                        raise InvalidWebhookData(e.fallback_message)
                    else:
                        request_kwargs["json"] = {"error": e.fallback_message}

        return request_kwargs

    def build_url(self, event_data):
        try:
            url = apply_jinja_template(
                self.url,
                **event_data,
            )
        except (JinjaTemplateError, JinjaTemplateWarning) as e:
            raise InvalidWebhookUrl(e.fallback_message)

        # raise if URL is not valid
        parse_url(url)

        return url

    def check_integration_filter(self, alert_group):
        if self.filtered_integrations.count() == 0:
            return True
        return self.filtered_integrations.filter(id=alert_group.channel.id).exists()

    def check_trigger(self, event_data):
        if not self.trigger_template:
            return True, ""

        try:
            result = apply_jinja_template(self.trigger_template, **event_data)
            return result.lower() in ["true", "1"], result
        except (JinjaTemplateError, JinjaTemplateWarning) as e:
            raise InvalidWebhookTrigger(e.fallback_message)

    def make_request(self, url, request_kwargs):
        if self.http_method not in ("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"):
            raise ValueError(f"Unsupported http method: {self.http_method}")

        with WebhookSession() as session:
            response = session.request(
                self.http_method, url, timeout=settings.OUTGOING_WEBHOOK_TIMEOUT, **request_kwargs
            )

        return response

    # Insight logs
    @property
    def insight_logs_type_verbal(self):
        return "webhook"

    @property
    def insight_logs_verbal(self):
        return self.name

    def _insight_log_team(self):
        result = {"team": "General"}
        if self.team:
            result["team"] = self.team.name
            result["team_id"] = self.team.public_primary_key
        return result

    @property
    def insight_logs_serialized(self):
        result = {
            "name": self.name,
            "trigger_type": self.trigger_type,
            "url": self.url,
            "data": self.data,
            "forward_all": self.forward_all,
        }
        result.update(self._insight_log_team())
        return result

    @property
    def insight_logs_metadata(self):
        result = {}
        result.update(self._insight_log_team())
        return result


class WebhookResponse(models.Model):
    alert_group = models.ForeignKey(
        "alerts.AlertGroup",
        on_delete=models.CASCADE,
        null=True,
        related_name="webhook_responses",
    )
    webhook = models.ForeignKey(
        "webhooks.Webhook",
        on_delete=models.SET_NULL,
        null=True,
        related_name="responses",
    )
    trigger_type = models.IntegerField(choices=Webhook.TRIGGER_TYPES)
    timestamp = models.DateTimeField(default=timezone.now)
    request_trigger = models.TextField(null=True, default=None)
    request_headers = models.TextField(null=True, default=None)
    request_data = models.TextField(null=True, default=None)
    url = models.TextField(null=True, default=None)
    status_code = models.IntegerField(default=None, null=True)
    content = models.TextField(null=True, default=None)
    event_data = models.TextField(null=True, default=None)

    def json(self):
        if self.content:
            return json.loads(self.content)


@receiver(post_save, sender=WebhookResponse)
def webhook_response_post_save(sender, instance, created, *args, **kwargs):
    if not created:
        return

    source_alert_receive_channel = instance.webhook.get_source_alert_receive_channel()
    if source_alert_receive_channel and hasattr(source_alert_receive_channel.config, "on_webhook_response_created"):
        source_alert_receive_channel.config.on_webhook_response_created(instance, source_alert_receive_channel)
