import frappe
from frappe import _
from frappe.utils import now_datetime, flt
from datetime import datetime
from kapital_bank.api import BIRBankClient
from kapital_bank.bank_api import _api_error_message

_AZ_MAP = str.maketrans("ƏəİışŞÇçÖöÜüĞğ", "EeIissCcOoUuGg")


def _latinize(text: str, max_len: int = None) -> str:
	"""Transliterate Azərbaycan → Latin, strip non-ASCII, truncate."""
	text = str(text or "").translate(_AZ_MAP)
	text = text.encode("ascii", errors="ignore").decode("ascii").strip()
	return text[:max_len] if max_len else text


def _fetch_banks_for_iban(client, iban):
	"""GET /b2b/dictionaries/portal/v1/banks?iban= → list of branch dicts.

	The endpoint returns every branch of the bank that owns the given IBAN
	(matched by the IBAN's 4-letter SWIFT prefix), each carrying its own
	6-digit bankCode plus the numeric bank id required for transfers.
	"""
	resp = client.get_json("/b2b/dictionaries/portal/v1/banks", params={"iban": iban})
	return resp.get("banks", [])


def _resolve_party_tax_id(pr, payee_ba):
	"""Resolve the beneficiary's VÖEN (tax id) from the party (Customer/Supplier).
	Tries the Payment Request's party first, then the Bank Account's linked party."""
	for ptype, pname in (
		(pr.get("party_type"), pr.get("party")),
		(payee_ba.get("party_type"), payee_ba.get("party")),
	):
		if ptype in ("Customer", "Supplier") and pname:
			tax_id = frappe.db.get_value(ptype, pname, "tax_id")
			if tax_id:
				return tax_id.strip()
	return ""


def _resolve_beneficiary_bank(client, payee_iban, kb_bank_code):
	"""Return the branch dict whose bankCode matches the configured 6-digit
	kb_bank_code for the beneficiary's IBAN, or None if there is no match.
	The branch carries the numeric `id` and `bicCode` needed to route a transfer."""
	target = (kb_bank_code or "").strip()
	for b in _fetch_banks_for_iban(client, payee_iban):
		if (b.get("bankCode") or "").strip() == target:
			return b
	return None


# Maps the bank's free-form transfer `frontState` onto our tracking statuses.
# The portal exposes frontState as an opaque string; we classify by keyword so
# new/renamed states degrade gracefully (unknown → left pending).
def _map_front_state(state):
	s = (state or "").upper()
	if "CANCEL" in s:
		return "Cancelled"
	if any(k in s for k in ("REJECT", "DECLIN", "FAIL", "ERROR")):
		return "Rejected"
	if any(k in s for k in ("SUCCESS", "CONFIRMED", "COMPLET", "DONE", "EXECUT", "PAID")):
		return "Success"
	if any(k in s for k in ("CREAT", "SIGN", "WAIT", "PEND", "PROCESS", "NEW", "SENT", "QUEUE")):
		# CREATED = transfer accepted by the bank, awaiting signature/confirmation
		# in Birbank Business — still pending from our side.
		return "Confirm Wait"
	return None


@frappe.whitelist()
def sync_bank_codes(login_name=None):
	"""Refresh Kapital Bank Bank Code reference data.

	The new API has no "list all banks" endpoint — banks are looked up per IBAN.
	We seed/refresh the dictionary from every registered Kapital Bank Account
	(which yields all Kapital branches) and from any beneficiary bank accounts
	configured locally. Existing rows for other banks are preserved.
	"""
	client = BIRBankClient(login_name)

	ibans = set()
	for iban in frappe.get_all("Kapital Bank Account", pluck="iban"):
		if iban:
			ibans.add(iban)
	for iban in frappe.get_all("Bank Account", filters={"iban": ["is", "set"]}, pluck="iban"):
		if iban:
			ibans.add(iban)

	seen = {}
	for iban in ibans:
		try:
			for b in _fetch_banks_for_iban(client, iban):
				code = (b.get("bankCode") or "").strip()
				if code:
					seen[code] = b
		except Exception:
			continue

	added = updated = 0
	for code, b in seen.items():
		values = {
			"bank_name": (b.get("name") or "").strip(),
			"bank_id": str(b.get("id") or "").strip(),
			"bic_code": (b.get("bicCode") or "").strip(),
		}
		if frappe.db.exists("Kapital Bank Bank Code", code):
			current = frappe.db.get_value("Kapital Bank Bank Code", code, list(values.keys()), as_dict=True)
			if any((current.get(k) or "") != v for k, v in values.items()):
				frappe.db.set_value("Kapital Bank Bank Code", code, values)
				updated += 1
		else:
			doc = frappe.new_doc("Kapital Bank Bank Code")
			doc.bank_code = code
			doc.update(values)
			doc.insert(ignore_permissions=True)
			added += 1

	frappe.db.commit()
	return {"success": True, "added": added, "updated": updated, "total": len(seen)}


@frappe.whitelist()
def send_payment_request(name, login_name=None):
	"""Validate Payment Request and send outgoing transfer to Kapital Bank."""
	pr = frappe.get_doc("Payment Request", name)

	if pr.docstatus != 1:
		frappe.throw(_("Payment Request {0} is not submitted.").format(name))

	if pr.payment_request_type != "Outward":
		frappe.throw(_("Payment Request {0} is not of type Outward.").format(name))

	if pr.status not in ("Initiated", "Partially Paid"):
		frappe.throw(_("Payment Request {0} has invalid status: {1}.").format(name, pr.status))

	kb_account = pr.get("kb_account")
	if not kb_account:
		frappe.throw(_("Field 'Kapital Bank Account' is not set."))

	mapping = frappe.db.get_value(
		"Kapital Bank Account Mapping",
		{"iban": kb_account, "parenttype": "Kapital Bank Settings"},
		["iban", "bank_account"],
		as_dict=True,
	)
	if not mapping:
		frappe.throw(_("Kapital Bank Account {0} is not mapped in Kapital Bank Settings.").format(kb_account))

	from_iban = kb_account

	party_bank_account = pr.get("kb_beneficiary_bank_account")
	if not party_bank_account:
		frappe.throw(_("Field 'Beneficiary Bank Account' is not set."))

	payee_ba = frappe.get_doc("Bank Account", party_bank_account)

	errors = []

	if not payee_ba.iban:
		errors.append(_("Field 'IBAN' is not set on Bank Account {0}.").format(party_bank_account))

	kb_bank_code = payee_ba.get("kb_bank_code") or ""
	if not kb_bank_code:
		errors.append(_("Field 'KB Bank Code' is not set on Bank Account {0}.").format(party_bank_account))
	elif len(kb_bank_code) != 6:
		errors.append(_("Field 'KB Bank Code' on Bank Account {0} must be exactly 6 characters.").format(party_bank_account))

	if not flt(pr.grand_total):
		errors.append(_("Field 'Amount' is zero on Payment Request {0}.").format(name))

	# National-currency transfers (the only outbound flow we support) are AZN only.
	# Guard both the document currency and the sender account currency so a non-AZN
	# amount is never sent to the AZN endpoint.
	payer_ccy = (frappe.db.get_value("Kapital Bank Account", kb_account, "currency") or "").upper()
	if (pr.currency or "").upper() not in ("", "AZN"):
		errors.append(_("Only AZN transfers are supported; Payment Request currency is {0}.").format(pr.currency))
	if payer_ccy and payer_ccy != "AZN":
		errors.append(_("Sender account {0} is not in AZN (currency: {1}).").format(kb_account, payer_ccy))

	to_cust_name = _latinize(pr.party_name, 34)
	if not to_cust_name:
		errors.append(_("Party name '{0}' is empty after Latin transliteration.").format(pr.party_name))

	existing = frappe.db.get_value(
		"Kapital Bank Payment",
		{"payment_request": name, "status": ["not in", ["Rejected", "Cancelled"]]},
		"name",
	)
	if existing:
		errors.append(_("Active KB Payment {0} already exists for this Payment Request.").format(existing))

	if errors:
		return {"success": False, "errors": errors}

	transfer_no = f"KB{datetime.now().strftime('%Y%m%d%H%M%S')}"
	payee_iban = payee_ba.iban
	# Bank Account has no tax_id; the beneficiary's VÖEN lives on the party
	# (Customer/Supplier). Prefer the Payment Request's party, then the Bank
	# Account's linked party.
	tax_id = _resolve_party_tax_id(pr, payee_ba)
	to_cust_name = _latinize(pr.party_name, 34)
	description = _latinize(pr.name, 64)
	amount = float(flt(pr.grand_total))

	import requests as _requests

	try:
		client = BIRBankClient(login_name)

		# Resolve the beneficiary's exact branch (and numeric bank id) from the IBAN.
		branch = _resolve_beneficiary_bank(client, payee_iban, kb_bank_code)
		if not branch:
			return {"success": False, "message": _(
				"Bank code {0} is not valid for beneficiary IBAN {1}."
			).format(kb_bank_code, payee_iban)}

		bank_id = str(branch.get("id") or "")
		is_kapital = (branch.get("bicCode") or "").upper().startswith("AIIB")

		if is_kapital:
			# Kapital → Kapital: internal transfer needs only the beneficiary IBAN.
			endpoint = "/b2b/transfers/portal/v1/national-currency/internal"
			payload = {
				"transferNo": transfer_no,
				"payer": {"iban": from_iban},
				"amount": amount,
				"description": description,
				"payee": {"iban": payee_iban},
				"self": False,
			}
		else:
			# Other domestic bank: inland transfer requires the routed bank id
			# and the beneficiary's VÖEN (tin), which the bank treats as mandatory.
			if not tax_id:
				return {"success": False, "errors": [
					_("Beneficiary VÖEN (Tax ID) is required for transfers to other banks. "
					  "Set 'Tax ID' on Bank Account {0}.").format(party_bank_account)
				]}
			endpoint = "/b2b/transfers/portal/v1/national-currency/inland"
			payload = {
				"transferNo": transfer_no,
				"payer": {"iban": from_iban},
				"amount": amount,
				"description": description,
				"payee": {
					"bank": {"id": int(bank_id) if bank_id.isdigit() else bank_id},
					"iban": payee_iban,
					"name": to_cust_name,
					"tin": tax_id,
				},
			}

		resp = client.post_json(endpoint, json=payload)
		transfer_id = str(resp.get("id") or "")
		process_key = str(resp.get("processKey") or "")

		kbp = frappe.new_doc("Kapital Bank Payment")
		kbp.update({
			"payment_request": name,
			"transfer_no": transfer_no,
			"transfer_id": transfer_id,
			"process_key": process_key,
			"status": "Sent",
			"from_account": from_iban,
			"to_account": payee_iban,
			"to_tax_no": tax_id,
			"to_cust_name": to_cust_name,
			"ben_bank_code": kb_bank_code,
			"ben_bank_id": bank_id,
			"amount": flt(pr.grand_total),
			"description": description,
			"sent_at": now_datetime(),
			"status_updated_at": now_datetime(),
		})
		kbp.insert(ignore_permissions=True)
		frappe.db.commit()
	except _requests.HTTPError as e:
		frappe.db.rollback()
		return {"success": False, "message": str(e)}
	except Exception:
		frappe.log_error(frappe.get_traceback(), f"KB Send Payment Request {name}")
		frappe.db.rollback()
		raise

	return {"success": True}


@frappe.whitelist()
def check_payment_status(name, login_name=None):
	"""Poll status for pending KB Payments of a Payment Request."""
	pending = frappe.get_all(
		"Kapital Bank Payment",
		filters={"payment_request": name, "status": ["in", ["Sent", "Confirm Wait"]]},
		fields=["name", "transfer_no", "payment_request", "transfer_id"],
	)

	if not pending:
		return {"success": True, "updated": 0, "message": _("No pending transfers")}

	client = BIRBankClient(login_name)

	updated = 0
	errors = []

	for kbp in pending:
		try:
			if not kbp.transfer_id:
				frappe.logger().warning(f"[KB] Status check for {kbp.transfer_no}: no transfer_id")
				continue

			resp = client.get_json(f"/b2b/transfers/portal/v1/{kbp.transfer_id}")
			front_state = resp.get("frontState", "")
			new_status = _map_front_state(front_state)

			if new_status is None:
				frappe.db.set_value(
					"Kapital Bank Payment",
					kbp.name,
					{
						"front_state": front_state,
						"error_message": f"Unmapped bank state: {front_state}",
						"status_updated_at": now_datetime(),
					},
					update_modified=False,
				)
				frappe.db.commit()
				continue

			frappe.db.set_value(
				"Kapital Bank Payment",
				kbp.name,
				{
					"status": new_status,
					"front_state": front_state,
					"status_updated_at": now_datetime(),
					"error_message": resp.get("description", ""),
				},
				update_modified=False,
			)
			frappe.db.commit()
			updated += 1

			if new_status == "Success":
				pe_name = _create_pe_from_pr(kbp.payment_request)
				if pe_name:
					frappe.db.set_value(
						"Kapital Bank Payment", kbp.name, "payment_entry", pe_name, update_modified=False
					)
					frappe.db.commit()
			elif new_status == "Rejected":
				_handle_rejected_pr(kbp.payment_request, resp.get("description", ""))

		except Exception as e:
			frappe.log_error(frappe.get_traceback(), f"KB Check Status {kbp.transfer_no}")
			errors.append(str(e))

	return {"success": True, "updated": updated, "errors": errors}


@frappe.whitelist()
def cancel_payment_request(name, login_name=None, comment=None):
	"""Cancel (reject) pending bank transfers for a Payment Request."""
	pending = frappe.get_all(
		"Kapital Bank Payment",
		filters={"payment_request": name, "status": ["in", ["Sent", "Confirm Wait"]]},
		fields=["name", "transfer_no", "transfer_id"],
	)

	if not pending:
		return {"success": True, "cancelled": 0, "message": _("Nothing to cancel")}

	# The reject endpoint requires a non-empty comment.
	comment = (comment or "").strip() or "Cancelled via ERPNext"

	client = BIRBankClient(login_name)
	cancelled = 0
	errors = []

	for kbp in pending:
		try:
			if not kbp.transfer_id:
				frappe.logger().warning(f"[KB] Cancel: no transfer_id for {kbp.transfer_no}, cancelling locally")
			else:
				resp = client.put(
					f"/b2b/transfers/portal/v1/{kbp.transfer_id}/reject",
					json={"comment": comment},
				)
				if not resp.ok:
					frappe.log_error(
						f"[KB] Cancel reject failed for {kbp.transfer_no}: HTTP {resp.status_code}\n{resp.text[:1000]}",
						"KB Cancel Error",
					)
					# The bank's reject endpoint refuses transfers that are still
					# awaiting signature (frontState CREATED) with an opaque error;
					# those can only be cancelled from Birbank Business directly.
					errors.append(_(
						"Could not cancel transfer {0} via API ({1}). If it is still "
						"awaiting signature, reject it directly in Birbank Business — "
						"its status will sync back here automatically."
					).format(kbp.transfer_no, _api_error_message(resp)))
					continue

			frappe.db.set_value(
				"Kapital Bank Payment",
				kbp.name,
				{"status": "Cancelled", "status_updated_at": now_datetime()},
				update_modified=False,
			)
			frappe.db.commit()
			cancelled += 1

		except Exception as e:
			frappe.log_error(frappe.get_traceback(), f"KB Cancel {kbp.transfer_no}")
			errors.append(str(e))

	return {"success": True, "cancelled": cancelled, "errors": errors}


def poll_pending_payments():
	"""Scheduler: poll status for all Payment Requests with pending KB Payments."""
	rows = frappe.db.sql(
		"""
		SELECT DISTINCT payment_request
		FROM `tabKapital Bank Payment`
		WHERE status IN ('Sent', 'Confirm Wait')
		AND payment_request IS NOT NULL
		""",
		as_dict=True,
	)

	for row in rows:
		try:
			check_payment_status(row.payment_request)
		except Exception:
			frappe.log_error(frappe.get_traceback(), "KB Poll Error")


def on_cancel_payment_request(doc, method):
	"""Doc event: block cancellation of Payment Request if pending bank transfers exist."""
	pending = frappe.get_all(
		"Kapital Bank Payment",
		filters={"payment_request": doc.name, "status": ["in", ["Sent", "Confirm Wait"]]},
		fields=["name", "transfer_no"],
	)
	if pending:
		names = ", ".join(p.transfer_no for p in pending)
		frappe.throw(
			_(
				"Cannot cancel Payment Request {0}: pending bank transfers exist ({1}). "
				"Cancel the bank transfers first using the 'Kapital Bank' menu."
			).format(doc.name, names)
		)


def _create_pe_from_pr(pr_name):
	"""Create and submit a Payment Entry from a Payment Request. Returns PE name or None."""
	pr = frappe.get_doc("Payment Request", pr_name)
	if pr.status not in ("Initiated", "Partially Paid"):
		frappe.log_error(
			f"KB: Payment Request {pr_name} has status '{pr.status}', skipping PE creation",
			"KB Auto PE",
		)
		return None
	pe = pr.create_payment_entry(submit=True)
	frappe.db.commit()
	return pe.name if pe else None


def _handle_rejected_pr(pr_name, description):
	"""Log rejection — PR stays Initiated so user can retry or cancel manually."""
	frappe.log_error(
		f"KB Transfer rejected for Payment Request {pr_name}: {description}",
		"KB Payment Rejected",
	)
