"""Server side of the declaration formula engine.

Declarations (ФНО) compute their appendix tables from Script Reports. A single
period change resolves hundreds of report references, but they collapse onto a
handful of distinct reports — a VAT declaration pulls 127 values out of only 4
(report, filters) pairs. `run_reports_batch` executes each pair once and slices
every requested column out of that single result.

This module deliberately lives in taxes_az, not in the formula_editor app:
formula_editor is a design-time authoring tool that gets removed from production
machines, and declarations must keep calculating without it.

`extract_field_from_report` is a faithful port of the original implementation in
formula_editor.api — the aggregation rule (take the report's total row, else sum
every row) decides the numbers that end up on a tax return, so it is reproduced
exactly rather than tidied up.
"""

import json

import frappe
from frappe.desk.query_report import run


@frappe.whitelist()
def run_reports_batch(requests):
	"""Resolve many report-field references with one report execution per report.

	`requests` is a list of {key, report, filters, field_name}. `key` is opaque to
	the server and is echoed back as the key of the returned map, so the client can
	pair values with the formula placeholders that asked for them.

	Returns {"values": {key: number}, "errors": [{report, message}]}.

	A report that fails — bad filters, or more commonly a user without report
	permission on the underlying doctype — contributes NO values and one error. It
	deliberately does not contribute zeros: a tax return quietly filled with zeros
	looks correct and gets filed, which is far worse than one that refuses to
	calculate. The other reports in the batch are unaffected.
	"""
	if isinstance(requests, str):
		requests = json.loads(requests)

	groups = {}
	for req in requests or []:
		filters = req.get("filters") or {}
		if isinstance(filters, str):
			filters = json.loads(filters)

		group_key = (req["report"], json.dumps(filters, sort_keys=True, default=str))
		group = groups.setdefault(
			group_key, {"report": req["report"], "filters": filters, "items": []}
		)
		group["items"].append(req)

	values = {}
	errors = []

	for group in groups.values():
		resolved, error = _resolve_group(group)
		values.update(resolved)
		if error:
			errors.append(error)

	return {"values": values, "errors": errors}


def _resolve_group(group):
	"""Run one report once and extract every column the batch asked it for.

	Returns (values, error). On failure `values` is empty — never zeros.
	"""
	report = group["report"]

	try:
		result = run(report, filters=group["filters"])
	except Exception as e:
		frappe.log_error(
			f"Report '{report}' failed for formula batch: {e}", "Formula Runtime"
		)
		# frappe.throw (a PermissionError, typically) has already queued its own text
		# for display. Drop it: the caller reports every failure in one dialog, and
		# leaving these queued shows the user each message twice.
		frappe.clear_messages()
		return {}, {"report": report, "message": str(e)}

	# query_report.run signals a refusal by returning a dict carrying `message`
	# instead of report data.
	if isinstance(result, dict) and result.get("message"):
		message = result.get("message")
		frappe.log_error(f"Report '{report}' returned: {message}", "Formula Runtime")
		frappe.clear_messages()
		return {}, {"report": report, "message": message}

	columns = result.get("columns", [])
	data = result.get("result", [])

	values = {
		item["key"]: extract_field_from_report(data, columns, item["field_name"])
		for item in group["items"]
	}
	return values, None


def extract_field_from_report(data, columns, field_name):
	"""Reduce one report column to a single number.

	Prefers the report's own total row; falls back to summing every row. Ported
	verbatim from formula_editor.api.extract_field_from_report — changing this
	changes the figures on filed tax returns.
	"""
	if not data:
		return 0

	column_idx = None
	for i, col in enumerate(columns):
		if isinstance(col, dict):
			if col.get("fieldname") == field_name or col.get("label") == field_name:
				column_idx = i
				break
		elif isinstance(col, str) and (col == field_name or field_name.lower() in col.lower()):
			column_idx = i
			break

	if column_idx is None:
		for i, col in enumerate(columns):
			col_name = ""
			if isinstance(col, dict):
				col_name = col.get("fieldname", "") or col.get("label", "")
			elif isinstance(col, str):
				col_name = col

			if field_name.lower() in col_name.lower():
				column_idx = i
				break

	if column_idx is None:
		return 0

	total_markers = ["total", "итого", "grand total", "общий итог", "итого по", "total row"]

	for row in data:
		is_total_row = False

		if isinstance(row, dict):
			if row.get("is_total_row") or row.get("bold") or row.get("total_row"):
				is_total_row = True

			if not is_total_row:
				for value in row.values():
					if isinstance(value, str) and any(m in value.lower() for m in total_markers):
						is_total_row = True
						break

		elif isinstance(row, list) and len(row) > 0:
			first_element = str(row[0]) if row[0] is not None else ""
			if any(m in first_element.lower() for m in total_markers):
				is_total_row = True

		if not is_total_row:
			continue

		value = None
		if isinstance(row, dict):
			if field_name in row:
				value = row[field_name]
			else:
				for key in row:
					if key.lower() == field_name.lower():
						value = row[key]
						break
		elif isinstance(row, list) and column_idx < len(row):
			value = row[column_idx]

		try:
			if value is not None:
				return round(float(value), 2)
		except (ValueError, TypeError):
			pass

	total = 0
	for row in data:
		value = None
		if isinstance(row, dict) and field_name in row:
			value = row[field_name]
		elif isinstance(row, dict):
			for key in row:
				if key.lower() == field_name.lower():
					value = row[key]
					break
		elif isinstance(row, list) and column_idx < len(row):
			value = row[column_idx]

		try:
			if value is not None:
				total += float(value)
		except (ValueError, TypeError):
			pass

	return round(total, 2)
