# Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt

import json

import jingrow
from frappe.model.document import Document
from frappe.utils import add_days, add_to_date

from press.press.doctype.press_job_step.press_job_step import safe_exec


class PressJob(Document):
	# 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 frappe.types import DF

		arguments: DF.Code
		callback_executed: DF.Check
		callback_failed: DF.Check
		callback_failure_count: DF.Int
		callback_failure_issue_resolved: DF.Check
		callback_retry_limit_reached: DF.Check
		duration: DF.Duration | None
		end: DF.Datetime | None
		job_type: DF.Link
		name: DF.Int | None
		next_callback_retry_at: DF.Datetime | None
		server: DF.DynamicLink | None
		server_type: DF.Link | None
		start: DF.Datetime | None
		status: DF.Literal["Pending", "Running", "Skipped", "Success", "Failure"]
		virtual_machine: DF.Link | None
	# end: auto-generated types

	def before_insert(self):
		frappe.db.get_value(self.server_type, self.server, "status", for_update=True)
		if existing_jobs := frappe.db.get_all(
			self.doctype,
			{
				"status": ("in", ["Pending", "Running"]),
				"server_type": self.server_type,
				"server": self.server,
			},
			["job_type", "status"],
		):
			frappe.throw(
				f"A {existing_jobs[0].job_type} job is already {existing_jobs[0].status}. Please wait for the same."
			)

	def after_insert(self):
		self.create_press_job_steps()
		self.execute()

	def on_update(self):
		if self.has_value_changed("status"):
			self.process_callback(save=True)

	def on_change(self):
		self.publish_update()

	def create_press_job_steps(self):
		job_type = frappe.get_pg("Press Job Type", self.job_type)
		for step in job_type.steps:
			pg = frappe.get_pg(
				{
					"doctype": "Press Job Step",
					"job": self.name,
					"status": "Pending",
					"job_type": self.job_type,
					"step_name": step.step_name,
					"wait_until_true": step.wait_until_true,
				}
			)
			pg.insert()

	def execute(self):
		self.status = "Running"
		self.start = frappe.utils.now_datetime()
		self.save()
		self.next()

	def fail(self, arguments=None):
		self.status = "Failure"
		pending_steps = frappe.get_all("Press Job Step", {"job": self.name, "status": "Pending"})
		for step in pending_steps:
			frappe.db.set_value("Press Job Step", step.name, "status", "Skipped")
		self.end = frappe.utils.now_datetime()
		self.duration = (self.end - self.start).total_seconds()
		self.save()

	def succeed(self):
		self.status = "Success"
		self.end = frappe.utils.now_datetime()
		self.duration = (self.end - self.start).total_seconds()
		self.save()

	@jingrow.whitelist()
	def next(self, arguments=None):
		if arguments:
			old_arguments = json.loads(self.arguments)
			old_arguments.update(arguments)
			self.arguments = json.dumps(old_arguments, indent=2)
		self.status = "Running"
		self.save()
		next_step = self.next_step

		if not next_step:
			self.succeed()
			return

		frappe.enqueue_pg("Press Job Step", next_step, "execute", enqueue_after_commit=True)

	@jingrow.whitelist()
	def force_continue(self):
		for step in frappe.get_all(
			"Press Job Step",
			{"job": self.name, "status": ("in", ("Failure", "Skipped"))},
			pluck="name",
		):
			frappe.db.set_value("Press Job Step", step, "status", "Pending")
		self.next()

	@jingrow.whitelist()
	def force_fail(self):
		for step in frappe.get_all(
			"Press Job Step",
			{"job": self.name, "status": "Pending"},
			pluck="name",
		):
			frappe.db.set_value("Press Job Step", step, "status", "Failure")
		frappe.db.set_value("Press Job", self.name, "status", "Failure")

	@property
	def next_step(self):
		return frappe.db.get_value(
			"Press Job Step",
			{"job": self.name, "status": "Pending"},
			"name",
			order_by="name asc",
			as_dict=True,
		)

	def detail(self):
		steps = frappe.get_all(
			"Press Job Step",
			filters={"job": self.name},
			fields=["name", "step_name", "status", "start", "end", "duration"],
			order_by="name asc",
		)

		for index, step in enumerate(steps):
			if step.status == "Pending" and index and steps[index - 1].status == "Success":
				step.status = "Running"

		return {
			"name": self.name,
			"job_type": self.job_type,
			"server": self.server,
			"server_type": self.server_type,
			"virtual_machine": self.virtual_machine,
			"status": self.status,
			"steps": steps,
		}

	def publish_update(self):
		frappe.publish_realtime(
			"press_job_update", doctype=self.doctype, docname=self.name, message=self.detail()
		)

	@jingrow.whitelist()
	def mark_callback_failure_issue_resolved(self):
		self.callback_failure_issue_resolved = True
		self.save()

	def process_callback(self, save: bool = False):  # noqa: C901
		if self.status not in ["Success", "Failure"]:
			return

		if self.callback_executed or self.callback_failure_issue_resolved:
			return

		job_type = frappe.db.get_value(
			"Press Job Type", self.job_type, ["callback_script", "callback_max_retry"], as_dict=True
		)
		if not job_type.callback_script:
			self.callback_executed = True
			if save:
				self.save()
			# No callback script defined, so just mark as executed
			return

		if self.callback_failed and self.callback_failure_count >= (job_type.callback_max_retry or 0):
			self.callback_retry_limit_reached = True
			self.next_callback_retry_at = None
			if save:
				self.save()
			return

		local = {"arguments": frappe._dict(json.loads(self.arguments)), "pg": self}
		try:
			safe_exec(job_type.callback_script, _locals=local)
			self.callback_failed = False
			self.callback_executed = True
			self.next_callback_retry_at = None
			self.callback_failure_issue_resolved = False
		except Exception:
			frappe.log_error(f"Error executing callback script for {self.name}")
			self.callback_failed = True
			self.callback_failure_count += 1
			self.next_callback_retry_at = add_to_date(None, minutes=5)

		if save:
			self.save()

	def on_trash(self):
		frappe.db.delete("Press Job Step", {"job": self.name})


def fail_stuck_press_jobs():
	jobs = frappe.get_all(
		"Press Job",
		filters={
			"status": ("in", ["Running", "Pending"]),
			"creation": ("<", add_days(None, -1)),
		},
		pluck="name",
		limit=100,
	)
	for job_name in jobs:
		job = PressJob("Press Job", job_name)
		job.force_fail()
		frappe.db.commit()


def process_failed_callbacks():
	jobs = frappe.get_all(
		"Press Job",
		filters={
			"status": ("in", ["Success", "Failure"]),
			"callback_failed": True,
			"callback_executed": False,
			"callback_failure_issue_resolved": False,
			"callback_retry_limit_reached": False,
			"next_callback_retry_at": ("<", frappe.utils.now_datetime()),
		},
		pluck="name",
	)
	for job_name in jobs:
		frappe.enqueue_pg(
			"Press Job",
			job_name,
			"process_callback",
			enqueue_after_commit=True,
			save=True,
		)
