# Copyright (c) 2015, SolutionERP LLC and contributors
# For license information, please see license.txt


import json

import xhiveframework
from xhiveframework import _
from xhiveframework.model.document import Document
from xhiveframework.utils import flt, get_url, nowdate
from xhiveframework.utils.background_jobs import enqueue

from xhiveerp.accounts.doctype.accounting_dimension.accounting_dimension import (
	get_accounting_dimensions,
)
from xhiveerp.accounts.doctype.payment_entry.payment_entry import (
	get_company_defaults,
	get_payment_entry,
)
from xhiveerp.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from xhiveerp.accounts.party import get_party_account, get_party_bank_account
from xhiveerp.accounts.utils import get_account_currency
from xhiveerp.xhiveerp_integrations.stripe_integration import create_stripe_subscription
from xhiveerp.utilities import payment_app_import_guard


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):
	def validate(self):
		if self.get("__islocal"):
			self.status = "Draft"
		self.validate_reference_document()
		self.validate_payment_request_amount()
		self.validate_currency()
		self.validate_subscription_details()

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

	def validate_payment_request_amount(self):
		existing_payment_request_amount = flt(
			get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
		)

		ref_doc = xhiveframework.get_doc(self.reference_doctype, self.reference_name)
		if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
			ref_amount = get_amount(ref_doc, self.payment_account)

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

	def validate_currency(self):
		ref_doc = xhiveframework.get_doc(self.reference_doctype, self.reference_name)
		if self.payment_account and ref_doc.currency != xhiveframework.db.get_value(
			"Account", self.payment_account, "account_currency"
		):
			xhiveframework.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 = xhiveframework.db.get_value(
					"Subscription Plan", subscription_plan.plan, "payment_gateway"
				)
				if payment_gateway != self.payment_gateway_account:
					xhiveframework.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:
				xhiveframework.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 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 = xhiveframework.get_doc(self.reference_doctype, self.reference_name)

		if (
			hasattr(ref_doc, "order_type") and getattr(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,
		)

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

	def get_request_amount(self):
		data_of_completed_requests = xhiveframework.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 = xhiveframework.get_doc(self.reference_doctype, self.reference_name)
		if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
			from xhiveerp.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 = xhiveframework.db.get_value(
				self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
			)
		else:
			data = xhiveframework.db.get_value(
				self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
			)
			data.update({"company": xhiveframework.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.encode("utf-8"),
				"description": self.subject.encode("utf-8"),
				"reference_doctype": "Payment Request",
				"reference_docname": self.name,
				"payer_email": self.email_to or xhiveframework.session.user,
				"payer_name": xhiveframework.safe_encode(data.customer_name),
				"order_id": self.name,
				"currency": self.currency,
			}
		)

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

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

			return payment_entry

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

		ref_doc = xhiveframework.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 = ref_doc.get("party_account_currency") or get_account_currency(
			party_account
		)

		bank_amount = self.grand_total
		if (
			party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
		):
			party_amount = ref_doc.base_grand_total
		else:
			party_amount = self.grand_total

		payment_entry = get_payment_entry(
			self.reference_doctype,
			self.reference_name,
			party_amount=party_amount,
			bank_account=self.payment_account,
			bank_amount=bank_amount,
		)

		payment_entry.update(
			{
				"mode_of_payment": self.mode_of_payment,
				"reference_no": self.name,
				"reference_date": nowdate(),
				"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
					self.reference_doctype, self.reference_name, self.name
				),
			}
		)

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

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

		if payment_entry.difference_amount:
			company_details = get_company_defaults(ref_doc.company)

			payment_entry.append(
				"deductions",
				{
					"account": company_details.exchange_gain_loss_account,
					"cost_center": company_details.cost_center,
					"amount": payment_entry.difference_amount,
				},
			)

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

		return payment_entry

	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": [
				xhiveframework.attach_print(
					self.reference_doctype,
					self.reference_name,
					file_name=self.reference_name,
					print_format=self.print_format,
				)
			],
		}
		enqueue(method=xhiveframework.sendmail, queue="short", timeout=300, is_async=True, **email_args)

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

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

		if self.message:
			return xhiveframework.render_template(self.message, context)

	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 xhiveframework.get_all(
				"Payment Entry Reference",
				filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
				fields=["parent"],
				limit=1,
			):
				xhiveframework.throw(_("Payment Entry already exists"), title=_("Error"))

	def make_communication_entry(self):
		"""Make communication entry"""
		comm = xhiveframework.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 on_payment_authorized(self, status=None):
		if not status:
			return

		shopping_cart_settings = xhiveframework.get_doc("E Commerce Settings")

		if status in ["Authorized", "Completed"]:
			redirect_to = None
			self.set_as_paid()

			# if shopping cart enabled and in session
			if (
				shopping_cart_settings.enabled
				and hasattr(xhiveframework.local, "session")
				and xhiveframework.local.session.user != "Guest"
			) and self.payment_channel != "Phone":

				success_url = shopping_cart_settings.payment_success_url
				if success_url:
					redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get(
						success_url, "/me"
					)
				else:
					redirect_to = get_url("/orders/{0}".format(self.reference_name))

			return redirect_to

	def create_subscription(self, payment_provider, gateway_controller, data):
		if payment_provider == "stripe":
			return create_stripe_subscription(gateway_controller, data)


@xhiveframework.whitelist(allow_guest=True)
def make_payment_request(**args):
	"""Make payment request"""

	args = xhiveframework._dict(args)

	ref_doc = xhiveframework.get_doc(args.dt, args.dn)
	gateway_account = get_gateway_details(args) or xhiveframework._dict()

	grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
	if args.loyalty_points and args.dt == "Sales Order":
		from xhiveerp.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points

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

	bank_account = (
		get_party_bank_account(args.get("party_type"), args.get("party"))
		if args.get("party_type")
		else ""
	)

	draft_payment_request = xhiveframework.db.get_value(
		"Payment Request",
		{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
	)

	existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)

	if existing_payment_request_amount:
		grand_total -= existing_payment_request_amount

	if draft_payment_request:
		xhiveframework.db.set_value(
			"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
		)
		pr = xhiveframework.get_doc("Payment Request", draft_payment_request)
	else:
		pr = xhiveframework.new_doc("Payment Request")
		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,
				"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,
				"party_type": args.get("party_type") or "Customer",
				"party": args.get("party") or ref_doc.get("customer"),
				"bank_account": bank_account,
			}
		)

		# 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

		pr.insert(ignore_permissions=True)
		if args.submit_doc:
			pr.submit()

	if args.order_type == "Shopping Cart":
		xhiveframework.db.commit()
		xhiveframework.local.response["type"] = "redirect"
		xhiveframework.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"""
	dt = ref_doc.doctype
	if dt in ["Sales Order", "Purchase Order"]:
		grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
	elif dt in ["Sales Invoice", "Purchase Invoice"]:
		if not ref_doc.get("is_pos"):
			if ref_doc.party_account_currency == ref_doc.currency:
				grand_total = flt(ref_doc.outstanding_amount)
			else:
				grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
		elif dt == "Sales Invoice":
			for pay in ref_doc.payments:
				if pay.type == "Phone" and pay.account == payment_account:
					grand_total = pay.amount
					break
	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

	if grand_total > 0:
		return grand_total
	else:
		xhiveframework.throw(_("Payment Entry is already created"))


def get_existing_payment_request_amount(ref_dt, ref_dn):
	"""
	Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
	and get the summation of existing paid payment request for Phone payment channel.
	"""
	existing_payment_request_amount = xhiveframework.db.sql(
		"""
		select sum(grand_total)
		from `tabPayment Request`
		where
			reference_doctype = %s
			and reference_name = %s
			and docstatus = 1
			and (status != 'Paid'
			or (payment_channel = 'Phone'
				and status = 'Paid'))
	""",
		(ref_dt, ref_dn),
	)
	return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0


def get_gateway_details(args):  # nosemgrep
	"""return gateway and payment account of default payment gateway"""
	if args.get("payment_gateway_account"):
		return get_payment_gateway_account(args.get("payment_gateway_account"))

	if args.order_type == "Shopping Cart":
		payment_gateway_account = xhiveframework.get_doc("E Commerce Settings").payment_gateway_account
		return get_payment_gateway_account(payment_gateway_account)

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

	return gateway_account


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


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

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

	return {"print_format": print_format_list}


@xhiveframework.whitelist(allow_guest=True)
def resend_payment_email(docname):
	return xhiveframework.get_doc("Payment Request", docname).send_email()


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


def update_payment_req_status(doc, method):
	from xhiveerp.accounts.doctype.payment_entry.payment_entry import get_reference_details

	for ref in doc.references:
		payment_request_name = xhiveframework.db.get_value(
			"Payment Request",
			{
				"reference_doctype": ref.reference_doctype,
				"reference_name": ref.reference_name,
				"docstatus": 1,
			},
		)

		if payment_request_name:
			ref_details = get_reference_details(
				ref.reference_doctype, ref.reference_name, doc.party_account_currency
			)
			pay_req_doc = xhiveframework.get_doc("Payment Request", payment_request_name)
			status = pay_req_doc.status

			if status != "Paid" and not ref_details.outstanding_amount:
				status = "Paid"
			elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
				status = "Partially Paid"
			elif ref_details.outstanding_amount == ref_details.total_amount:
				if pay_req_doc.payment_request_type == "Outward":
					status = "Initiated"
				elif pay_req_doc.payment_request_type == "Inward":
					status = "Requested"

			pay_req_doc.db_set("status", status)


def get_dummy_message(doc):
	return xhiveframework.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 }}"),
	)


@xhiveframework.whitelist()
def get_subscription_details(reference_doctype, reference_name):
	if reference_doctype == "Sales Invoice":
		subscriptions = xhiveframework.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 = xhiveframework.get_doc("Subscription", subscription.sub_name).plans
			for plan in plans:
				subscription_plans.append(plan)
		return subscription_plans


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

	def set_missing_values(source, target):
		target.payment_order_type = "Payment Request"
		target.append(
			"references",
			{
				"reference_doctype": source.reference_doctype,
				"reference_name": source.reference_name,
				"amount": source.grand_total,
				"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",
			}
		},
		target_doc,
		set_missing_values,
	)

	return doclist


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

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