# Copyright (c) 2017, JINGROW
# License: MIT. See LICENSE

import base64
import hashlib
import hmac
import json
from time import sleep
from urllib.parse import urlparse

import requests

import jingrow
from jingrow import _
from jingrow.model.page import Page
from jingrow.utils.background_jobs import get_queues_timeout
from jingrow.utils.jinja import validate_template
from jingrow.utils.safe_exec import get_safe_globals

WEBHOOK_SECRET_HEADER = "X-Jingrow-Webhook-Signature"


class Webhook(Page):
	# begin: auto-generated types
	# This code is auto-generated. Do not modify anything in this block.

	from typing import TYPE_CHECKING

	if TYPE_CHECKING:
		from jingrow.integrations.pagetype.webhook_data.webhook_data import WebhookData
		from jingrow.integrations.pagetype.webhook_header.webhook_header import WebhookHeader
		from jingrow.types import DF

		background_jobs_queue: DF.Autocomplete | None
		condition: DF.SmallText | None
		enable_security: DF.Check
		enabled: DF.Check
		is_dynamic_url: DF.Check
		meets_condition: DF.Data | None
		preview_document: DF.DynamicLink | None
		preview_request_body: DF.Code | None
		request_method: DF.Literal["POST", "PUT", "DELETE"]
		request_structure: DF.Literal["", "Form URL-Encoded", "JSON"]
		request_url: DF.SmallText
		timeout: DF.Int
		webhook_data: DF.Table[WebhookData]
		webhook_docevent: DF.Literal[
			"after_insert",
			"on_update",
			"on_submit",
			"on_cancel",
			"on_trash",
			"on_update_after_submit",
			"on_change",
		]
		webhook_pagetype: DF.Link
		webhook_headers: DF.Table[WebhookHeader]
		webhook_json: DF.Code | None
		webhook_secret: DF.Password | None
	# end: auto-generated types

	def validate(self):
		self.validate_docevent()
		self.validate_condition()
		self.validate_request_url()
		self.validate_request_body()
		self.validate_repeating_fields()
		self.validate_secret()
		self.preview_document = None

	def on_update(self):
		jingrow.cache.delete_value("webhooks")

	def validate_docevent(self):
		if self.webhook_pagetype:
			is_submittable = jingrow.get_value("PageType", self.webhook_pagetype, "is_submittable")
			if not is_submittable and self.webhook_docevent in [
				"on_submit",
				"on_cancel",
				"on_update_after_submit",
			]:
				jingrow.throw(_("PageType must be Submittable for the selected Pg Event"))

	def validate_condition(self):
		temp_pg = jingrow.new_pg(self.webhook_pagetype)
		if self.condition:
			try:
				jingrow.safe_eval(self.condition, eval_locals=get_context(temp_pg))
			except Exception as e:
				jingrow.throw(_("Invalid Condition: {}").format(e))

	def validate_request_url(self):
		try:
			request_url = urlparse(self.request_url).netloc
			if not request_url:
				raise jingrow.ValidationError
		except Exception as e:
			jingrow.throw(_("Check Request URL"), exc=e)

	def validate_request_body(self):
		if self.request_structure:
			if self.request_structure == "Form URL-Encoded":
				self.webhook_json = None
			elif self.request_structure == "JSON":
				validate_template(self.webhook_json)
				self.webhook_data = []

	def validate_repeating_fields(self):
		"""Error when Same Field is entered multiple times in webhook_data"""
		webhook_data = [entry.fieldname for entry in self.webhook_data]
		if len(webhook_data) != len(set(webhook_data)):
			jingrow.throw(_("Same Field is entered more than once"))

	def validate_secret(self):
		if self.enable_security:
			try:
				self.get_password("webhook_secret", False).encode("utf8")
			except Exception:
				jingrow.throw(_("Invalid Webhook Secret"))

	@jingrow.whitelist()
	def generate_preview(self):
		# This function doesn't need to do anything specific as virtual fields
		# get evaluated automatically.
		pass

	@property
	def meets_condition(self):
		if not self.condition:
			return _("Yes")

		if not (self.preview_document and self.webhook_pagetype):
			return _("Select a document to check if it meets conditions.")

		try:
			pg = jingrow.get_cached_pg(self.webhook_pagetype, self.preview_document)
			met_condition = jingrow.safe_eval(self.condition, eval_locals=get_context(pg))
		except Exception as e:
			return _("Failed to evaluate conditions: {}").format(e)
		return _("Yes") if met_condition else _("No")

	@property
	def preview_request_body(self):
		if not (self.preview_document and self.webhook_pagetype):
			return _("Select a document to preview request data")

		try:
			pg = jingrow.get_cached_pg(self.webhook_pagetype, self.preview_document)
			return jingrow.as_json(get_webhook_data(pg, self))
		except Exception as e:
			return _("Failed to compute request body: {}").format(e)


def get_context(pg):
	return {"pg": pg, "utils": get_safe_globals().get("jingrow").get("utils")}


def enqueue_webhook(pg, webhook) -> None:
	request_url = headers = data = r = None
	try:
		webhook: Webhook = jingrow.get_pg("Webhook", webhook.get("name"))
		request_url = webhook.request_url
		if webhook.is_dynamic_url:
			request_url = jingrow.render_template(webhook.request_url, get_context(pg))
		headers = get_webhook_headers(pg, webhook)
		data = get_webhook_data(pg, webhook)

	except Exception as e:
		jingrow.logger().debug({"enqueue_webhook_error": e})
		log_request(webhook.name, pg.name, request_url, headers, data)
		return

	for i in range(3):
		try:
			r = requests.request(
				method=webhook.request_method,
				url=request_url,
				data=json.dumps(data, default=str),
				headers=headers,
				timeout=webhook.timeout or 5,
			)
			r.raise_for_status()
			jingrow.logger().debug({"webhook_success": r.text})
			log_request(webhook.name, pg.name, request_url, headers, data, r)
			break

		except requests.exceptions.ReadTimeout as e:
			jingrow.logger().debug({"webhook_error": e, "try": i + 1})
			log_request(webhook.name, pg.name, request_url, headers, data)

		except Exception as e:
			jingrow.logger().debug({"webhook_error": e, "try": i + 1})
			log_request(webhook.name, pg.name, request_url, headers, data, r)
			sleep(3 * i + 1)
			if i != 2:
				continue


def log_request(
	webhook: str,
	pagename: str,
	url: str,
	headers: dict,
	data: dict,
	res: requests.Response | None = None,
):
	request_log = jingrow.get_pg(
		{
			"pagetype": "Webhook Request Log",
			"webhook": webhook,
			"reference_document": pagename,
			"user": jingrow.session.user if jingrow.session.user else None,
			"url": url,
			"headers": jingrow.as_json(headers) if headers else None,
			"data": jingrow.as_json(data) if data else None,
			"response": res.text if res is not None else None,
			"error": jingrow.get_traceback(),
		}
	)

	request_log.save(ignore_permissions=True)


def get_webhook_headers(pg, webhook):
	headers = {}

	if webhook.enable_security:
		data = get_webhook_data(pg, webhook)
		signature = base64.b64encode(
			hmac.new(
				webhook.get_password("webhook_secret").encode("utf8"),
				json.dumps(data).encode("utf8"),
				hashlib.sha256,
			).digest()
		)
		headers[WEBHOOK_SECRET_HEADER] = signature

	if webhook.webhook_headers:
		for h in webhook.webhook_headers:
			if h.get("key") and h.get("value"):
				headers[h.get("key")] = h.get("value")

	return headers


def get_webhook_data(pg, webhook):
	data = {}
	pg = pg.as_dict(convert_dates_to_str=True)

	if webhook.webhook_data:
		data = {w.key: pg.get(w.fieldname) for w in webhook.webhook_data}
	elif webhook.webhook_json:
		data = jingrow.render_template(webhook.webhook_json, get_context(pg))
		data = json.loads(data)

	return data


@jingrow.whitelist()
def get_all_queues():
	"""Fetches all workers and returns a list of available queue names."""
	jingrow.only_for("System Manager")

	return get_queues_timeout().keys()
