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


import xhiveframework
from xhiveframework import _, qb
from xhiveframework.model.document import Document
from xhiveframework.model.meta import get_field_precision
from xhiveframework.query_builder import Criterion, Order
from xhiveframework.query_builder.functions import NullIf, Sum
from xhiveframework.utils import flt, get_link_to_form

import xhiveerp
from xhiveerp.accounts.doctype.journal_entry.journal_entry import get_balance_on
from xhiveerp.setup.utils import get_exchange_rate


class ExchangeRateRevaluation(Document):
	def validate(self):
		self.set_total_gain_loss()

	def set_total_gain_loss(self):
		total_gain_loss = 0

		gain_loss_booked = 0
		gain_loss_unbooked = 0

		for d in self.accounts:
			if not d.zero_balance:
				d.gain_loss = flt(
					d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
				) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))

			if d.zero_balance:
				gain_loss_booked += flt(d.gain_loss, d.precision("gain_loss"))
			else:
				gain_loss_unbooked += flt(d.gain_loss, d.precision("gain_loss"))

			total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))

		self.gain_loss_booked = gain_loss_booked
		self.gain_loss_unbooked = gain_loss_unbooked
		self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))

	def validate_mandatory(self):
		if not (self.company and self.posting_date):
			xhiveframework.throw(_("Please select Company and Posting Date to getting entries"))

	def on_cancel(self):
		self.ignore_linked_doctypes = "GL Entry"

	@xhiveframework.whitelist()
	def check_journal_entry_condition(self):
		exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()

		jea = qb.DocType("Journal Entry Account")
		journals = (
			qb.from_(jea)
			.select(jea.parent)
			.distinct()
			.where(
				(jea.reference_type == "Exchange Rate Revaluation")
				& (jea.reference_name == self.name)
				& (jea.docstatus == 1)
			)
			.run()
		)

		if journals:
			gle = qb.DocType("GL Entry")
			total_amt = (
				qb.from_(gle)
				.select((Sum(gle.credit) - Sum(gle.debit)).as_("total_amount"))
				.where(
					(gle.voucher_type == "Journal Entry")
					& (gle.voucher_no.isin(journals))
					& (gle.account == exchange_gain_loss_account)
					& (gle.is_cancelled == 0)
				)
				.run()
			)

			if total_amt and total_amt[0][0] != self.total_gain_loss:
				return True
			else:
				return False

		return True

	@xhiveframework.whitelist()
	def get_accounts_data(self):
		self.validate_mandatory()
		account_details = self.get_account_balance_from_gle(
			company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
		)
		accounts_with_new_balance = self.calculate_new_account_balance(
			self.company, self.posting_date, account_details
		)

		if not accounts_with_new_balance:
			self.throw_invalid_response_message(account_details)

		return accounts_with_new_balance

	@staticmethod
	def get_account_balance_from_gle(company, posting_date, account, party_type, party):
		account_details = []

		if company and posting_date:
			company_currency = xhiveerp.get_company_currency(company)

			acc = qb.DocType("Account")
			if account:
				accounts = [account]
			else:
				res = (
					qb.from_(acc)
					.select(acc.name)
					.where(
						(acc.is_group == 0)
						& (acc.report_type == "Balance Sheet")
						& (acc.root_type.isin(["Asset", "Liability", "Equity"]))
						& (acc.account_type != "Stock")
						& (acc.company == company)
						& (acc.account_currency != company_currency)
					)
					.orderby(acc.name)
					.run(as_list=True)
				)
				accounts = [x[0] for x in res]

			if accounts:
				having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
					(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
				)

				gle = qb.DocType("GL Entry")

				# conditions
				conditions = []
				conditions.append(gle.account.isin(accounts))
				conditions.append(gle.posting_date.lte(posting_date))
				conditions.append(gle.is_cancelled == 0)

				if party_type:
					conditions.append(gle.party_type == party_type)
				if party:
					conditions.append(gle.party == party)

				account_details = (
					qb.from_(gle)
					.select(
						gle.account,
						gle.party_type,
						gle.party,
						gle.account_currency,
						(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
							"balance_in_account_currency"
						),
						(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
						(Sum(gle.debit) - Sum(gle.credit) == 0)
						^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
							"zero_balance"
						),
					)
					.where(Criterion.all(conditions))
					.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))
					.having(having_clause)
					.orderby(gle.account)
					.run(as_dict=True)
				)

		return account_details

	@staticmethod
	def calculate_new_account_balance(company, posting_date, account_details):
		accounts = []
		company_currency = xhiveerp.get_company_currency(company)
		precision = get_field_precision(
			xhiveframework.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
			company_currency,
		)

		if account_details:
			# Handle Accounts with balance in both Account/Base Currency
			for d in [x for x in account_details if not x.zero_balance]:
				current_exchange_rate = (
					d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
				)
				new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
				new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
				gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
				if gain_loss:
					accounts.append(
						{
							"account": d.account,
							"party_type": d.party_type,
							"party": d.party,
							"account_currency": d.account_currency,
							"balance_in_base_currency": d.balance,
							"balance_in_account_currency": d.balance_in_account_currency,
							"zero_balance": d.zero_balance,
							"current_exchange_rate": current_exchange_rate,
							"new_exchange_rate": new_exchange_rate,
							"new_balance_in_base_currency": new_balance_in_base_currency,
							"new_balance_in_account_currency": d.balance_in_account_currency,
							"gain_loss": gain_loss,
						}
					)

			# Handle Accounts with '0' balance in Account/Base Currency
			for d in [x for x in account_details if x.zero_balance]:

				if d.balance != 0:
					current_exchange_rate = new_exchange_rate = 0

					new_balance_in_account_currency = 0  # this will be '0'
					new_balance_in_base_currency = 0  # this will be '0'
					gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
				else:
					new_exchange_rate = 0
					new_balance_in_base_currency = 0
					new_balance_in_account_currency = 0

					current_exchange_rate = calculate_exchange_rate_using_last_gle(
						company, d.account, d.party_type, d.party
					)

					gain_loss = new_balance_in_account_currency - (
						current_exchange_rate * d.balance_in_account_currency
					)

				if gain_loss:
					accounts.append(
						{
							"account": d.account,
							"party_type": d.party_type,
							"party": d.party,
							"account_currency": d.account_currency,
							"balance_in_base_currency": d.balance,
							"balance_in_account_currency": d.balance_in_account_currency,
							"zero_balance": d.zero_balance,
							"current_exchange_rate": current_exchange_rate,
							"new_exchange_rate": new_exchange_rate,
							"new_balance_in_base_currency": new_balance_in_base_currency,
							"new_balance_in_account_currency": new_balance_in_account_currency,
							"gain_loss": gain_loss,
						}
					)

		return accounts

	def throw_invalid_response_message(self, account_details):
		if account_details:
			message = _("No outstanding invoices require exchange rate revaluation")
		else:
			message = _("No outstanding invoices found")
		xhiveframework.msgprint(message)

	def get_for_unrealized_gain_loss_account(self):
		unrealized_exchange_gain_loss_account = xhiveframework.get_cached_value(
			"Company", self.company, "unrealized_exchange_gain_loss_account"
		)
		if not unrealized_exchange_gain_loss_account:
			xhiveframework.throw(
				_("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
			)
		return unrealized_exchange_gain_loss_account

	@xhiveframework.whitelist()
	def make_jv_entries(self):
		zero_balance_jv = self.make_jv_for_zero_balance()
		if zero_balance_jv:
			xhiveframework.msgprint(
				f"Zero Balance Journal: {get_link_to_form('Journal Entry', zero_balance_jv.name)}"
			)

		revaluation_jv = self.make_jv_for_revaluation()
		if revaluation_jv:
			xhiveframework.msgprint(
				f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}"
			)

		return {
			"revaluation_jv": revaluation_jv.name if revaluation_jv else None,
			"zero_balance_jv": zero_balance_jv.name if zero_balance_jv else None,
		}

	def make_jv_for_zero_balance(self):
		if self.gain_loss_booked == 0:
			return

		accounts = [x for x in self.accounts if x.zero_balance]

		if not accounts:
			return

		unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()

		journal_entry = xhiveframework.new_doc("Journal Entry")
		journal_entry.voucher_type = "Exchange Gain Or Loss"
		journal_entry.company = self.company
		journal_entry.posting_date = self.posting_date
		journal_entry.multi_currency = 1

		journal_entry_accounts = []
		for d in accounts:
			journal_account = xhiveframework._dict(
				{
					"account": d.get("account"),
					"party_type": d.get("party_type"),
					"party": d.get("party"),
					"account_currency": d.get("account_currency"),
					"balance": flt(
						d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
					),
					"exchange_rate": 0,
					"cost_center": xhiveerp.get_default_cost_center(self.company),
					"reference_type": "Exchange Rate Revaluation",
					"reference_name": self.name,
				}
			)

			# Account Currency has balance
			if d.get("balance_in_account_currency") and not d.get("new_balance_in_account_currency"):
				dr_or_cr = (
					"credit_in_account_currency"
					if d.get("balance_in_account_currency") > 0
					else "debit_in_account_currency"
				)
				reverse_dr_or_cr = (
					"debit_in_account_currency"
					if dr_or_cr == "credit_in_account_currency"
					else "credit_in_account_currency"
				)
				journal_account.update(
					{
						dr_or_cr: flt(
							abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
						),
						reverse_dr_or_cr: 0,
						"debit": 0,
						"credit": 0,
					}
				)
			elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
				# Base currency has balance
				dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
				reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
				journal_account.update(
					{
						dr_or_cr: flt(
							abs(d.get("balance_in_base_currency")), d.precision("balance_in_base_currency")
						),
						reverse_dr_or_cr: 0,
						"debit_in_account_currency": 0,
						"credit_in_account_currency": 0,
					}
				)

			journal_entry_accounts.append(journal_account)

		journal_entry_accounts.append(
			{
				"account": unrealized_exchange_gain_loss_account,
				"balance": get_balance_on(unrealized_exchange_gain_loss_account),
				"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
				"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
				"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
				"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
				"cost_center": xhiveerp.get_default_cost_center(self.company),
				"exchange_rate": 1,
				"reference_type": "Exchange Rate Revaluation",
				"reference_name": self.name,
			}
		)

		journal_entry.set("accounts", journal_entry_accounts)
		journal_entry.set_total_debit_credit()
		journal_entry.save()
		return journal_entry

	def make_jv_for_revaluation(self):
		if self.gain_loss_unbooked == 0:
			return

		accounts = [x for x in self.accounts if not x.zero_balance]
		if not accounts:
			return

		unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()

		journal_entry = xhiveframework.new_doc("Journal Entry")
		journal_entry.voucher_type = "Exchange Rate Revaluation"
		journal_entry.company = self.company
		journal_entry.posting_date = self.posting_date
		journal_entry.multi_currency = 1

		journal_entry_accounts = []
		for d in accounts:
			if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
				continue

			dr_or_cr = (
				"debit_in_account_currency"
				if d.get("balance_in_account_currency") > 0
				else "credit_in_account_currency"
			)

			reverse_dr_or_cr = (
				"debit_in_account_currency"
				if dr_or_cr == "credit_in_account_currency"
				else "credit_in_account_currency"
			)

			journal_entry_accounts.append(
				{
					"account": d.get("account"),
					"party_type": d.get("party_type"),
					"party": d.get("party"),
					"account_currency": d.get("account_currency"),
					"balance": flt(
						d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
					),
					dr_or_cr: flt(
						abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
					),
					"cost_center": xhiveerp.get_default_cost_center(self.company),
					"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
					"reference_type": "Exchange Rate Revaluation",
					"reference_name": self.name,
				}
			)
			journal_entry_accounts.append(
				{
					"account": d.get("account"),
					"party_type": d.get("party_type"),
					"party": d.get("party"),
					"account_currency": d.get("account_currency"),
					"balance": flt(
						d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
					),
					reverse_dr_or_cr: flt(
						abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
					),
					"cost_center": xhiveerp.get_default_cost_center(self.company),
					"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
					"reference_type": "Exchange Rate Revaluation",
					"reference_name": self.name,
				}
			)

		journal_entry.set("accounts", journal_entry_accounts)
		journal_entry.set_amounts_in_company_currency()
		journal_entry.set_total_debit_credit()

		self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
		journal_entry.append(
			"accounts",
			{
				"account": unrealized_exchange_gain_loss_account,
				"balance": get_balance_on(unrealized_exchange_gain_loss_account),
				"debit_in_account_currency": abs(self.gain_loss_unbooked)
				if self.gain_loss_unbooked < 0
				else 0,
				"credit_in_account_currency": self.gain_loss_unbooked if self.gain_loss_unbooked > 0 else 0,
				"cost_center": xhiveerp.get_default_cost_center(self.company),
				"exchange_rate": 1,
				"reference_type": "Exchange Rate Revaluation",
				"reference_name": self.name,
			},
		)

		journal_entry.set_amounts_in_company_currency()
		journal_entry.set_total_debit_credit()
		journal_entry.save()
		return journal_entry


def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
	"""
	Use last GL entry to calculate exchange rate
	"""
	last_exchange_rate = None
	if company and account:
		gl = qb.DocType("GL Entry")

		# build conditions
		conditions = []
		conditions.append(gl.company == company)
		conditions.append(gl.account == account)
		conditions.append(gl.is_cancelled == 0)
		conditions.append((gl.debit > 0) | (gl.credit > 0))
		conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0))
		if party_type:
			conditions.append(gl.party_type == party_type)
		if party:
			conditions.append(gl.party == party)

		voucher_type, voucher_no = (
			qb.from_(gl)
			.select(gl.voucher_type, gl.voucher_no)
			.where(Criterion.all(conditions))
			.orderby(gl.posting_date, order=Order.desc)
			.limit(1)
			.run()[0]
		)

		last_exchange_rate = (
			qb.from_(gl)
			.select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency))
			.where(
				(gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account)
			)
			.orderby(gl.posting_date, order=Order.desc)
			.limit(1)
			.run()[0][0]
		)

	return last_exchange_rate


@xhiveframework.whitelist()
def get_account_details(company, posting_date, account, party_type=None, party=None):
	if not (company and posting_date):
		xhiveframework.throw(_("Company and Posting Date is mandatory"))

	account_currency, account_type = xhiveframework.get_cached_value(
		"Account", account, ["account_currency", "account_type"]
	)

	if account_type in ["Receivable", "Payable"] and not (party_type and party):
		xhiveframework.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))

	account_details = {}
	company_currency = xhiveerp.get_company_currency(company)

	account_details = {
		"account_currency": account_currency,
	}
	account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
		company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
	)

	if account_balance and (
		account_balance[0].balance or account_balance[0].balance_in_account_currency
	):
		account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
			company, posting_date, account_balance
		)
		row = account_with_new_balance[0]
		account_details.update(
			{
				"balance_in_base_currency": row["balance_in_base_currency"],
				"balance_in_account_currency": row["balance_in_account_currency"],
				"current_exchange_rate": row["current_exchange_rate"],
				"new_exchange_rate": row["new_exchange_rate"],
				"new_balance_in_base_currency": row["new_balance_in_base_currency"],
				"new_balance_in_account_currency": row["new_balance_in_account_currency"],
				"zero_balance": row["zero_balance"],
				"gain_loss": row["gain_loss"],
			}
		)

	return account_details
