import json

import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import cstr, flt, nowdate
from frappe.utils.background_jobs import enqueue

from erpnext import _refuse, get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
	get_accounting_dimensions,
)
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account
from erpnext.accounts.doctype.mode_of_payment.mode_of_payment import validate_enabled_mode_of_payment
from erpnext.accounts.doctype.payment_entry.payment_entry import (
	get_payment_entry,
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import get_account_currency, get_currency_precision
from erpnext.utilities import payment_app_import_guard

ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
	"Sales Order",
	"Purchase Order",
	"Sales Invoice",
	"Purchase Invoice",
	"POS Invoice",
	"Fees",
]


def _get_payment_gateway_controller(*args, **kwargs):
	with payment_app_import_guard():
		from payments.utils import get_payment_gateway_controller

	return get_payment_gateway_controller(*args, **kwargs)


class PaymentRequest(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

		from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import (
			SubscriptionPlanDetail,
		)

		account: DF.ReadOnly | None
		amended_from: DF.Link | None
		bank: DF.Link | None
		bank_account: DF.Link | None
		bank_account_no: DF.ReadOnly | None
		branch_code: DF.ReadOnly | None
		company: DF.Link | None
		cost_center: DF.Link | None
		currency: DF.Link | None
		email_to: DF.Data | None
		grand_total: DF.Currency
		iban: DF.ReadOnly | None
		is_a_subscription: DF.Check
		make_sales_invoice: DF.Check
		message: DF.Text | None
		mode_of_payment: DF.Link | None
		mute_email: DF.Check
		naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
		outstanding_amount: DF.Currency
		party: DF.DynamicLink | None
		party_account_currency: DF.Link | None
		party_name: DF.Data | None
		party_type: DF.Link | None
		payment_account: DF.ReadOnly | None
		payment_channel: DF.Literal["", "Email", "Phone", "Other"]
		payment_gateway: DF.ReadOnly | None
		payment_gateway_account: DF.Link | None
		payment_order: DF.Link | None
		payment_request_type: DF.Literal["Outward", "Inward"]
		payment_url: DF.Data | None
		print_format: DF.Literal[None]
		project: DF.Link | None
		reference_doctype: DF.Link | None
		reference_name: DF.DynamicLink | None
		status: DF.Literal[
			"",
			"Draft",
			"Requested",
			"Initiated",
			"Partially Paid",
			"Payment Ordered",
			"Paid",
			"Failed",
			"Cancelled",
		]
		subject: DF.Data | None
		subscription_plans: DF.Table[SubscriptionPlanDetail]
		swift_number: DF.ReadOnly | None
		transaction_date: DF.Date | None
	# end: auto-generated types

	def validate(self):
		validate_enabled_mode_of_payment(self.mode_of_payment)
		if self.get("__islocal"):
			self.status = "Draft"
		self.validate_reference_document()
		ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
		ref_doc.check_permission("read")
		self.validate_reference_company(ref_doc)
		self.validate_bank_account()
		self.validate_print_format()
		self.validate_payment_request_amount(ref_doc)
		# self.validate_currency()
		self.validate_subscription_details()

	def validate_bank_account(self):
		if not self.bank_account:
			return

		bank_account = frappe.get_cached_value(
			"Bank Account",
			self.bank_account,
			["disabled", "is_company_account", "company", "party_type", "party"],
			as_dict=True,
		)
		if not bank_account or bank_account.disabled:
			frappe.throw(_("Bank Account {0} is missing or disabled").format(self.bank_account))

		if bank_account.is_company_account:
			if not self.company or bank_account.company != self.company:
				frappe.throw(
					_("Bank Account {0} does not belong to Company {1}").format(
						self.bank_account, self.company
					)
				)
		elif (
			not self.party_type
			or not self.party
			or bank_account.party_type != self.party_type
			or bank_account.party != self.party
		):
			frappe.throw(
				_("Bank Account {0} does not belong to {1} {2}").format(
					self.bank_account, self.party_type, self.party
				)
			)

	def validate_reference_document(self):
		if not self.reference_doctype or not self.reference_name:
			frappe.throw(_("To create a Payment Request reference document is required"))

		if self.reference_doctype not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
			frappe.throw(
				_("Payment Requests cannot be created against: {0}").format(
					frappe.bold(self.reference_doctype)
				)
			)

	def validate_print_format(self):
		if not self.print_format or self.print_format == "Standard":
			return

		if frappe.db.get_value("Print Format", self.print_format, "doc_type") != self.reference_doctype:
			frappe.throw(
				_("Print Format {0} is not valid for {1}").format(
					frappe.bold(self.print_format), _(self.reference_doctype)
				),
				title=_("Invalid Print Format"),
			)

	def validate_reference_company(self, ref_doc):
		if self.company != ref_doc.company:
			frappe.throw(
				_("Company {0} does not match the company {1} of {2} {3}").format(
					frappe.bold(self.company),
					frappe.bold(ref_doc.company),
					_(self.reference_doctype),
					frappe.bold(self.reference_name),
				),
				title=_("Invalid Company"),
			)

	def validate_payment_request_amount(self, ref_doc):
		if self.grand_total == 0:
			frappe.throw(
				_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
				title=_("Invalid Amount"),
			)

		if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart":
			ref_amount = get_amount(ref_doc, self.payment_account)
			if not ref_amount:
				frappe.throw(_("Payment Entry is already created"))

			existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))

			if (
				flt(
					existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
					get_currency_precision(),
				)
				> ref_amount
			):
				frappe.throw(
					_("Total Payment Request amount cannot be greater than {0} amount").format(
						self.reference_doctype
					)
				)

	def validate_currency(self):
		ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
		if self.payment_account and ref_doc.currency != frappe.get_cached_value(
			"Account", self.payment_account, "account_currency"
		):
			frappe.throw(_("Transaction currency must be same as Payment Gateway currency"))

	def validate_subscription_details(self):
		if self.is_a_subscription:
			amount = 0
			for subscription_plan in self.subscription_plans:
				payment_gateway = frappe.db.get_value(
					"Subscription Plan", subscription_plan.plan, "payment_gateway"
				)
				if payment_gateway != self.payment_gateway_account:
					frappe.throw(
						_(
							"The payment gateway account in plan {0} is different from the payment gateway account in this payment request"
						).format(subscription_plan.name)
					)

				rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty)

				amount += rate

			if amount != self.grand_total:
				frappe.msgprint(
					_(
						"The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document."
					).format(self.grand_total, amount)
				)

	def before_submit(self):
		self.outstanding_amount = get_requested_amount_in_party_account_currency(self)

	def on_submit(self):
		if self.payment_request_type == "Outward":
			self.db_set("status", "Initiated")
			return
		elif self.payment_request_type == "Inward":
			self.db_set("status", "Requested")

		send_mail = self.payment_gateway_validation() if self.payment_gateway else None
		ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)

		if (
			hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
		) or self.flags.mute_email:
			send_mail = False

		if send_mail and self.payment_channel != "Phone":
			self.set_payment_request_url()
			self.send_email()
			self.make_communication_entry()

		elif self.payment_channel == "Phone":
			self.request_phone_payment()

	def request_phone_payment(self):
		controller = _get_payment_gateway_controller(self.payment_gateway)
		request_amount = self.get_request_amount()

		payment_record = dict(
			reference_doctype="Payment Request",
			reference_docname=self.name,
			payment_reference=self.reference_name,
			request_amount=request_amount,
			sender=self.email_to,
			currency=self.currency,
			payment_gateway=self.payment_gateway,
			phone_number=self.phone_number,
		)

		controller.validate_transaction_currency(self.currency)
		controller.request_for_payment(**payment_record)

	def get_request_amount(self):
		data_of_completed_requests = frappe.get_all(
			"Integration Request",
			filters={
				"reference_doctype": self.doctype,
				"reference_docname": self.name,
				"status": "Completed",
			},
			pluck="data",
		)

		if not data_of_completed_requests:
			return self.grand_total

		request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
		return request_amounts

	def on_cancel(self):
		self.check_if_payment_entry_exists()
		self.set_as_cancelled()

	def make_invoice(self):
		ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
		if hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart":
			from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice

			si = make_sales_invoice(self.reference_name, ignore_permissions=True)
			si.allocate_advances_automatically = True
			si = si.insert(ignore_permissions=True)
			si.submit()

	def payment_gateway_validation(self):
		try:
			controller = _get_payment_gateway_controller(self.payment_gateway)
			if hasattr(controller, "on_payment_request_submission"):
				return controller.on_payment_request_submission(self)
			else:
				return True
		except Exception:
			return False

	def set_payment_request_url(self):
		if self.payment_account and self.payment_channel != "Phone":
			self.payment_url = self.get_payment_url()

		if self.payment_url:
			self.db_set("payment_url", self.payment_url)

		if (
			self.payment_url
			or not self.payment_gateway_account
			or (self.payment_gateway_account and self.payment_channel == "Phone")
		):
			self.db_set("status", "Initiated")

	def get_payment_url(self):
		if self.reference_doctype != "Fees":
			data = frappe.db.get_value(
				self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
			)
		else:
			data = frappe.db.get_value(
				self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
			)
			data.update({"company": frappe.defaults.get_defaults().company})

		controller = _get_payment_gateway_controller(self.payment_gateway)
		controller.validate_transaction_currency(self.currency)

		if hasattr(controller, "validate_minimum_transaction_amount"):
			controller.validate_minimum_transaction_amount(self.currency, self.grand_total)

		return controller.get_payment_url(
			**{
				"amount": flt(self.grand_total, self.precision("grand_total")),
				"title": data.company,
				"description": self.subject,
				"reference_doctype": "Payment Request",
				"reference_docname": self.name,
				"payer_email": self.email_to or frappe.session.user,
				"payer_name": data.customer_name,
				"order_id": self.name,
				"currency": self.currency,
				"payment_gateway": self.payment_gateway,
			}
		)

	def set_as_paid(self):
		if self.payment_channel == "Phone":
			self.db_set({"status": "Paid", "outstanding_amount": 0})

		else:
			payment_entry = self.create_payment_entry()
			self.make_invoice()

			return payment_entry

	def create_payment_entry(self, submit=True):
		"""create entry"""
		frappe.flags.ignore_account_permission = True

		ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)

		if self.reference_doctype in ["Sales Invoice", "POS Invoice"]:
			party_account = ref_doc.debit_to
		elif self.reference_doctype == "Purchase Invoice":
			party_account = ref_doc.credit_to
		else:
			party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)

		party_account_currency = (
			self.get("party_account_currency")
			or ref_doc.get("party_account_currency")
			or get_account_currency(party_account)
		)

		party_amount = bank_amount = self.outstanding_amount

		if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
			exchange_rate = ref_doc.get("conversion_rate")
			bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))

		# outstanding amount is already in Part's account currency
		payment_entry = get_payment_entry(
			self.reference_doctype,
			self.reference_name,
			party_amount=party_amount,
			bank_account=self.payment_account,
			bank_amount=bank_amount,
			created_from_payment_request=True,
		)
		payment_entry.set_missing_ref_details(force=True)

		payment_entry.update(
			{
				"mode_of_payment": self.mode_of_payment,
				"reference_no": self.name,  # to prevent validation error
				"reference_date": nowdate(),
				"remarks": "Payment Entry against {} {} via Payment Request {}".format(
					self.reference_doctype, self.reference_name, self.name
				),
			}
		)

		# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
		self._allocate_payment_request_to_pe_references(references=payment_entry.references)

		# Update dimensions
		payment_entry.update(
			{
				"cost_center": self.get("cost_center"),
				"project": self.get("project"),
			}
		)

		# Update 'Paid Amount' on Forex transactions
		if self.currency != ref_doc.company_currency:
			if (
				self.payment_request_type == "Outward"
				and payment_entry.paid_from_account_currency == ref_doc.company_currency
				and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
			):
				payment_entry.paid_amount = payment_entry.base_paid_amount = (
					payment_entry.target_exchange_rate * payment_entry.received_amount
				)

		for dimension in get_accounting_dimensions():
			payment_entry.update({dimension: self.get(dimension)})

		if submit:
			payment_entry.insert(ignore_permissions=True)
			payment_entry.submit()

		return payment_entry

	@frappe.whitelist(methods=["POST"])
	def resend_payment_email(self):
		# the caller can supply this document's values, so only the saved ones are trusted
		self.reload()
		self.check_permission("email")
		frappe.has_permission(self.reference_doctype, "read", self.reference_name, throw=True)

		if not (
			self.docstatus == 1
			and self.payment_request_type == "Inward"
			and self.payment_channel != "Phone"
			and self.status not in ["Initiated", "Paid"]
		):
			frappe.throw(_("Payment Link couldn't be sent."))

		self.send_email()

	def send_email(self):
		"""send email with payment link"""
		email_args = {
			"recipients": self.email_to,
			"sender": None,
			"subject": self.subject,
			"message": self.get_message(),
			"now": True,
			"attachments": [
				frappe.attach_print(
					self.reference_doctype,
					self.reference_name,
					file_name=self.reference_name,
					print_format=self.print_format,
				)
			],
		}
		job_id = f"send_payment_email::{self.name}"
		enqueue(
			method=frappe.sendmail,
			queue="short",
			timeout=300,
			is_async=True,
			job_id=job_id,
			deduplicate=True,
			enqueue_after_commit=True,
			**email_args,
		)

	def get_message(self):
		"""return message with payment gateway link"""

		context = {
			"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
			"payment_url": self.payment_url,
		}

		if self.message:
			return frappe.render_template(self.message, context, restrict_globals=True)

	def set_failed(self):
		pass

	def set_as_cancelled(self):
		self.db_set("status", "Cancelled")

	def check_if_payment_entry_exists(self):
		if self.status == "Paid":
			if frappe.get_all(
				"Payment Entry Reference",
				filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
				fields=["parent"],
				limit=1,
			):
				frappe.throw(_("Payment Entry already exists"), title=_("Error"))

	def make_communication_entry(self):
		"""Make communication entry"""
		comm = frappe.get_doc(
			{
				"doctype": "Communication",
				"subject": self.subject,
				"content": self.get_message(),
				"sent_or_received": "Sent",
				"reference_doctype": self.reference_doctype,
				"reference_name": self.reference_name,
			}
		)
		comm.insert(ignore_permissions=True)

	def get_payment_success_url(self):
		return self.payment_success_url

	def create_subscription(self, payment_provider, gateway_controller, data):
		if payment_provider == "stripe":
			with payment_app_import_guard():
				from payments.payment_gateways.stripe_integration import create_stripe_subscription

			return create_stripe_subscription(gateway_controller, data)

	def _allocate_payment_request_to_pe_references(self, references):
		"""
		Allocate the Payment Request to the Payment Entry references based on\n
		    - Allocated Amount.
		    - Outstanding Amount of Payment Request.\n
		Payment Request is doc itself and references are the rows of Payment Entry.
		"""
		if len(references) == 1:
			references[0].payment_request = self.name
			return

		precision = references[0].precision("allocated_amount")
		outstanding_amount = self.outstanding_amount

		# to manage rows
		row_number = 1
		MOVE_TO_NEXT_ROW = 1
		TO_SKIP_NEW_ROW = 2
		NEW_ROW_ADDED = False

		while row_number <= len(references):
			row = references[row_number - 1]

			# update the idx to maintain the order
			row.idx = row_number

			if outstanding_amount == 0:
				if not NEW_ROW_ADDED:
					break

				row_number += MOVE_TO_NEXT_ROW
				continue

			# allocate the payment request to the row
			row.payment_request = self.name

			if row.allocated_amount <= outstanding_amount:
				outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
				row_number += MOVE_TO_NEXT_ROW
			else:
				remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
				row.allocated_amount = outstanding_amount
				outstanding_amount = 0

				# create a new row without PR for remaining unallocated amount
				new_row = frappe.copy_doc(row)
				references.insert(row_number, new_row)

				# update new row
				new_row.idx = row_number + 1
				new_row.payment_request = None
				new_row.allocated_amount = remaining_allocated_amount

				NEW_ROW_ADDED = True
				row_number += TO_SKIP_NEW_ROW


@frappe.whitelist()
def make_payment_request(**args):
	"""Make payment request"""

	args = frappe._dict(args)

	if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
		frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))

	if args.dn and not isinstance(args.dn, str):
		frappe.throw(_("Invalid parameter. 'dn' should be of type str"))

	frappe.has_permission("Payment Request", "create", throw=True)
	frappe.has_permission(args.dt, "read", args.dn, throw=True)

	ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
	if not args.get("company"):
		args.company = ref_doc.company
	gateway_account = get_gateway_details(args) or frappe._dict()

	grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
	if not grand_total:
		frappe.throw(_("Payment Entry is already created"))
	if args.loyalty_points and args.dt == "Sales Order":
		from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points

		loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
		frappe.db.set_value(
			"Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False
		)
		frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
		grand_total = grand_total - loyalty_amount

	# fetches existing payment request `grand_total` amount
	existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)

	def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
		grand_total -= existing_payment_request_amount
		if not grand_total:
			frappe.throw(_("Payment Request is already created"))
		return grand_total

	if existing_payment_request_amount:
		if args.order_type == "Shopping Cart":
			# If Payment Request is in an advanced stage, then create for remaining amount.
			if get_existing_payment_request_amount(
				ref_doc, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
			):
				grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
			else:
				# If PR's are processed, cancel all of them.
				cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
		else:
			grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)

	draft_payment_request = frappe.db.get_value(
		"Payment Request",
		{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
	)

	if draft_payment_request:
		frappe.db.set_value(
			"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
		)
		pr = frappe.get_doc("Payment Request", draft_payment_request)
	else:
		bank_account = (
			get_party_bank_account(args.get("party_type"), args.get("party"))
			if args.get("party_type")
			else ""
		)
		pr = frappe.new_doc("Payment Request")

		if not args.get("payment_request_type"):
			args["payment_request_type"] = (
				"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
			)

		party_type = args.get("party_type") or "Customer"
		party_account_currency = ref_doc.get("party_account_currency")

		if not party_account_currency:
			party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
			party_account_currency = get_account_currency(party_account)

		pr.update(
			{
				"payment_gateway_account": gateway_account.get("name"),
				"payment_gateway": gateway_account.get("payment_gateway"),
				"payment_account": gateway_account.get("payment_account"),
				"payment_channel": gateway_account.get("payment_channel"),
				"payment_request_type": args.get("payment_request_type"),
				"currency": ref_doc.currency,
				"party_account_currency": party_account_currency,
				"grand_total": grand_total,
				"mode_of_payment": args.mode_of_payment,
				"email_to": args.recipient_id or ref_doc.owner,
				"subject": _("Payment Request for {0}").format(args.dn),
				"message": gateway_account.get("message") or get_dummy_message(ref_doc),
				"reference_doctype": args.dt,
				"reference_name": args.dn,
				"company": ref_doc.get("company"),
				"party_type": party_type,
				"party": args.get("party") or ref_doc.get("customer"),
				"bank_account": bank_account,
				"party_name": args.get("party_name") or ref_doc.get("customer_name"),
				"phone_number": args.get("phone_number") if args.get("phone_number") else None,
			}
		)

		# Update dimensions
		pr.update(
			{
				"cost_center": ref_doc.get("cost_center"),
				"project": ref_doc.get("project"),
			}
		)

		for dimension in get_accounting_dimensions():
			pr.update({dimension: ref_doc.get(dimension)})

		if args.order_type == "Shopping Cart" or args.mute_email:
			pr.flags.mute_email = True

		if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
			pr.insert(ignore_permissions=True)
		if args.submit_doc:
			if pr.get("__unsaved"):
				pr.insert(ignore_permissions=True)
			pr.submit()

	if args.order_type == "Shopping Cart":
		frappe.db.commit()
		frappe.local.response["type"] = "redirect"
		frappe.local.response["location"] = pr.get_payment_url()

	if args.return_doc:
		return pr

	return pr.as_dict()


def get_amount(ref_doc, payment_account=None):
	"""get amount based on doctype"""
	grand_total = 0

	dt = ref_doc.doctype
	if dt in ["Sales Order", "Purchase Order"]:
		advance_amount = flt(ref_doc.advance_paid)
		if ref_doc.party_account_currency != ref_doc.currency:
			advance_amount = flt(flt(ref_doc.advance_paid) / ref_doc.conversion_rate)

		grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - advance_amount

	elif dt in ["Sales Invoice", "Purchase Invoice"]:
		if (
			dt == "Sales Invoice"
			and ref_doc.is_pos
			and ref_doc.payments
			and any(
				[
					payment.type == "Phone" and payment.account == payment_account
					for payment in ref_doc.payments
				]
			)
		):
			grand_total = sum(
				[
					payment.amount
					for payment in ref_doc.payments
					if payment.type == "Phone" and payment.account == payment_account
				]
			)
		else:
			if ref_doc.party_account_currency == ref_doc.currency:
				grand_total = flt(ref_doc.outstanding_amount)
			else:
				grand_total = flt(flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate)
	elif dt == "POS Invoice":
		for pay in ref_doc.payments:
			if pay.type == "Phone" and pay.account == payment_account:
				grand_total = pay.amount
				break
	elif dt == "Fees":
		grand_total = ref_doc.outstanding_amount

	return flt(grand_total, get_currency_precision()) if grand_total > 0 else 0


def get_irequest_status(payment_requests: None | list = None) -> list:
	IR = frappe.qb.DocType("Integration Request")
	res = []
	if payment_requests:
		res = (
			frappe.qb.from_(IR)
			.select(IR.name)
			.where(IR.reference_doctype.eq("Payment Request"))
			.where(IR.reference_docname.isin(payment_requests))
			.where(IR.status.isin(["Authorized", "Completed"]))
			.run(as_dict=True)
		)
	return res


def cancel_old_payment_requests(ref_dt, ref_dn):
	PR = frappe.qb.DocType("Payment Request")

	if res := (
		frappe.qb.from_(PR)
		.select(PR.name)
		.where(PR.reference_doctype == ref_dt)
		.where(PR.reference_name == ref_dn)
		.where(PR.docstatus == 1)
		.where(PR.status.isin(["Draft", "Requested"]))
		.run(as_dict=True)
	):
		if get_irequest_status([x.name for x in res]):
			frappe.throw(_("Another Payment Request is already processed"))
		else:
			for x in res:
				doc = frappe.get_doc("Payment Request", x.name)
				doc.flags.ignore_permissions = True
				doc.cancel()

				if ireqs := get_irequests_of_payment_request(doc.name):
					for ireq in ireqs:
						frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")


def get_existing_payment_request_amount(ref_doc, statuses: list | None = None) -> list:
	"""
	Return the total amount of Payment Requests against a reference document.
	"""
	PR = frappe.qb.DocType("Payment Request")

	query = (
		frappe.qb.from_(PR)
		.select(Sum(PR.outstanding_amount))
		.where(PR.reference_doctype == ref_doc.doctype)
		.where(PR.reference_name == ref_doc.name)
		.where(PR.docstatus == 1)
	)

	if statuses:
		query = query.where(PR.status.isin(statuses))

	response = query.run()

	os_amount_in_transaction_currency = flt(response[0][0] if response[0] else 0)

	if ref_doc.currency != ref_doc.party_account_currency:
		os_amount_in_transaction_currency = flt(os_amount_in_transaction_currency / ref_doc.conversion_rate)

	return os_amount_in_transaction_currency


def get_gateway_details(args):  # nosemgrep
	"""
	Return gateway and payment account of default payment gateway
	"""
	gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company})
	if gateway_account:
		return get_payment_gateway_account(gateway_account)

	gateway_account = get_payment_gateway_account({"is_default": 1})

	return gateway_account


def get_payment_gateway_account(args):
	return frappe.db.get_value(
		"Payment Gateway Account",
		args,
		["name", "payment_gateway", "payment_account", "message"],
		as_dict=1,
	)


@frappe.whitelist()
def get_print_format_list(ref_doctype):
	print_format_list = ["Standard"]

	print_format_list.extend(
		[p.name for p in frappe.get_all("Print Format", filters={"doc_type": ref_doctype})]
	)

	return {"print_format": print_format_list}


@frappe.whitelist()
def make_payment_entry(docname):
	doc = frappe.get_doc("Payment Request", docname)
	doc.check_permission("read")
	return doc.create_payment_entry(submit=False).as_dict()


def get_requested_amount_in_party_account_currency(doc) -> float:
	"""Return the total requested amount converted to party account currency when applicable."""
	company = doc.get("company")
	if (
		doc.get("currency") != doc.get("party_account_currency")
		and doc.get("party_account_currency") == get_company_currency(company)
		and doc.get("reference_doctype")
		and doc.get("reference_name")
	):
		invoice = frappe.get_value(
			doc.get("reference_doctype"),
			doc.get("reference_name"),
			["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
			as_dict=1,
		)
		if invoice:
			grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
			base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
			if grand_total:
				precision = (
					doc.precision("outstanding_amount")
					if callable(getattr(doc, "precision", None))
					else frappe.get_precision(
						"Payment Request", "outstanding_amount", currency=doc.get("party_account_currency")
					)
				)
				return flt(
					flt(doc.get("grand_total")) / grand_total * base_grand_total,
					precision,
				)

	return flt(doc.get("grand_total"))


def update_payment_requests_as_per_pe_references(references=None, cancel=False):
	"""
	Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
	"""
	if not references:
		return

	precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
	referenced_payment_requests = frappe.get_all(
		"Payment Request",
		filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
		fields=[
			"name",
			"grand_total",
			"outstanding_amount",
			"payment_request_type",
			"currency",
			"party_account_currency",
			"company",
			"reference_doctype",
			"reference_name",
		],
	)

	for pr in referenced_payment_requests:
		pr.total_requested = get_requested_amount_in_party_account_currency(pr)

	referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
	doc_updates = {}

	for ref in references:
		if not ref.payment_request:
			continue

		payment_request = referenced_payment_requests[ref.payment_request]
		pr_outstanding = payment_request["outstanding_amount"]

		# update outstanding amount
		new_outstanding_amount = flt(
			pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
			precision,
		)

		# to handle same payment request for the multiple allocations
		payment_request["outstanding_amount"] = new_outstanding_amount

		if not cancel and new_outstanding_amount < 0:
			frappe.throw(
				msg=_(
					"The allocated amount is greater than the outstanding amount of Payment Request {0}"
				).format(ref.payment_request),
				title=_("Invalid Allocated Amount"),
			)

		# determine status
		total_requested = payment_request.total_requested
		if total_requested > 0 and flt(new_outstanding_amount, precision) >= flt(total_requested, precision):
			status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
		elif new_outstanding_amount <= 0:
			status = "Paid"
		elif new_outstanding_amount > 0:
			status = "Partially Paid"

		# prepare bulk update data
		doc_updates[ref.payment_request] = {
			"outstanding_amount": new_outstanding_amount,
			"status": status,
		}

	# bulk update all payment requests
	if doc_updates:
		frappe.db.bulk_update("Payment Request", doc_updates)


def get_dummy_message(doc):
	return frappe.render_template(
		"""{% if doc.contact_person -%}
<p>Dear {{ doc.contact_person }},</p>
{%- else %}<p>Hello,</p>{% endif %}

<p>{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype,
	doc.name, doc.get_formatted("grand_total")) }}</p>

<a href="{{ payment_url }}">{{ _("Make Payment") }}</a>

<p>{{ _("If you have any questions, please get back to us.") }}</p>

<p>{{ _("Thank you for your business!") }}</p>
""",
		dict(doc=doc, payment_url="{{ payment_url }}"),
	)


@frappe.whitelist()
def get_subscription_details(reference_doctype, reference_name):
	reference_name = cstr(reference_name)
	if not reference_name or not frappe.has_permission(reference_doctype, "read", doc=reference_name):
		_refuse()

	if reference_doctype == "Sales Invoice":
		subscriptions = frappe.db.sql(
			"""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",
			reference_name,
			as_dict=1,
		)
		subscription_plans = []
		for subscription in subscriptions:
			plans = frappe.get_doc("Subscription", subscription.sub_name).plans
			for plan in plans:
				subscription_plans.append(plan)
		return subscription_plans


@frappe.whitelist()
def make_payment_order(source_name, target_doc=None):
	from frappe.model.mapper import get_mapped_doc

	def set_missing_values(source, target):
		target.payment_order_type = "Payment Request"
		target.company = target.company or source.company
		target.append(
			"references",
			{
				"reference_doctype": source.reference_doctype,
				"reference_name": source.reference_name,
				# Journal Entry debits the supplier in party account currency.
				"amount": source.outstanding_amount,
				"supplier": source.party,
				"payment_request": source_name,
				"mode_of_payment": source.mode_of_payment,
				"bank_account": source.bank_account,
				"account": source.account,
			},
		)

	doclist = get_mapped_doc(
		"Payment Request",
		source_name,
		{
			"Payment Request": {
				"doctype": "Payment Order",
				# keep the company of an existing order; references are validated against it
				"field_no_map": ["company"] if target_doc else [],
			}
		},
		target_doc,
		set_missing_values,
	)

	return doclist


def validate_payment(doc, method=None):
	if doc.reference_doctype != "Payment Request" or (
		frappe.db.get_value(doc.reference_doctype, doc.reference_docname, "status") != "Paid"
	):
		return

	frappe.throw(
		_("The Payment Request {0} is already paid, cannot process payment twice").format(
			doc.reference_docname
		)
	)


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_bank_account_query(
	doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict
):
	filters = frappe._dict(filters or {})
	if not filters.company and not (filters.party_type and filters.party):
		return []

	owners = []
	if filters.company:
		owners.append({"is_company_account": 1, "company": filters.company})
	if filters.party_type and filters.party:
		owners.append({"is_company_account": 0, "party_type": filters.party_type, "party": filters.party})

	accounts = []
	for owner in owners:
		accounts.extend(
			frappe.get_list(
				"Bank Account",
				fields=["name", "bank", "account"],
				filters=[
					*[[key, "=", value] for key, value in owner.items()],
					["disabled", "=", 0],
					[searchfield, "like", f"%{txt}%"],
				],
				order_by="name",
				limit_page_length=start + page_len,
				as_list=True,
			)
		)
	return sorted(accounts, key=lambda account: account[0])[start : start + page_len]


@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
	# permission checks in `get_list()`
	filters = frappe._dict(filters)

	if not filters.reference_doctype or not filters.reference_name:
		return []

	if txt:
		filters.name = ["like", f"%{txt}%"]

	open_payment_requests = frappe.get_list(
		"Payment Request",
		filters=filters,
		fields=["name", "grand_total", "outstanding_amount"],
		order_by="transaction_date ASC,creation ASC",
	)

	return [
		(
			pr.name,
			_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
			_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
		)
		for pr in open_payment_requests
	]


def get_irequests_of_payment_request(doc: str | None = None) -> list:
	res = []
	if doc:
		res = frappe.db.get_all(
			"Integration Request",
			{
				"reference_doctype": "Payment Request",
				"reference_docname": doc,
				"status": "Queued",
			},
		)
	return res
