"""
CA Automator — Accounting & Financial Statement Automation engines: the tools
from the "Accounting & Financial Statement Automators" list NOT already covered
by engines.fin_statements (Automated Financial Statement Generator, Notes to
Accounts Generator, TB-to-Financial-Statement Mapping Engine), engines.fixed_assets
(Depreciation Schedule Automator), engines.lease_schedule (Lease Accounting
Automator, lessee side) or engines.loan_schedule (a loan's own period interest /
cost of borrowing).

Pure functions only (no DB access), mirrored line-for-line in web/acct_engines.js.
tb_diagnostics() takes the exact same TB row shape used by engines.fin_statements
(name, group, dr, cr, head) so a preparer can reuse the same TB export/paste —
nothing here reads another module's stored data, the rows are the caller's own
input on every call.
"""

import re

R2 = lambda x: round(float(x or 0), 2)


def _n(v, default=0.0):
    if v is None or v == "":
        return default
    try:
        return float(v)
    except (TypeError, ValueError):
        return default


def _safe_div(a, b):
    return (a / b) if b else 0.0


def _ymd(s):
    """Parse a 'YYYY-MM-DD' string into (y, m, d) ints. Returns None if blank/invalid."""
    if not s:
        return None
    try:
        y, m, d = str(s).split("-")
        return int(y), int(m), int(d)
    except (ValueError, TypeError):
        return None


def _days_between(d1, d2):
    """Proleptic-Gregorian day count between two 'YYYY-MM-DD' strings (d2 - d1),
    via each date's Julian day number — deterministic and identical to the JS
    mirror's Date.UTC(...)/86400000 difference, so no cross-language drift."""
    def jdn(y, m, q):
        a = (14 - m) // 12
        yy = y + 4800 - a
        mm = m + 12 * a - 3
        return q + (153 * mm + 2) // 5 + 365 * yy + yy // 4 - yy // 100 + yy // 400 - 32045
    p1, p2 = _ymd(d1), _ymd(d2)
    if not p1 or not p2:
        return 0
    return jdn(*p2) - jdn(*p1)


def _month_index(y, m):
    return y * 12 + (m - 1)


# ---------------------------------------------------------------------------
# 1. Trial Balance Analyzer / Suspense Account Detector / Unusual Balance
#    Detector / Debit-Credit Classification Checker — one stateless diagnostic
#    pass over TB rows (same row shape as engines.fin_statements).
# ---------------------------------------------------------------------------
SUSPENSE_RX = re.compile(r"suspense|clearing account|unidentified|un-?reconciled|difference account", re.I)


def tb_diagnostics(rows, head_nature=None, materiality=0, movement_threshold_pct=20):
    """
    rows: [{name, group, head, dr, cr, prev}] — prev is the prior-year closing
    balance in the same dr-minus-cr sign convention as this year's net, optional.
    head_nature: {head_id: "debit"|"credit"} — the caller supplies this (from
    engines.HEAD) rather than this module importing engines, keeping the two
    engines independently testable.
    """
    head_nature = head_nature or {}
    materiality = _n(materiality)
    threshold = _n(movement_threshold_pct, 20)
    out_rows = []
    suspense, unusual_balance, unclassified, large_movements = [], [], [], []
    total_dr = total_cr = 0.0

    for r in rows or []:
        name = r.get("name", "")
        group = r.get("group", "")
        dr, cr = _n(r.get("dr")), _n(r.get("cr"))
        net = R2(dr - cr)
        head = r.get("head") or ""
        total_dr += dr
        total_cr += cr
        entry = {"name": name, "group": group, "head": head, "dr": R2(dr), "cr": R2(cr), "net": net}

        if SUSPENSE_RX.search(f"{name} {group}"):
            suspense.append({**entry, "reason": "Account name suggests a suspense/clearing balance"})

        if not head and abs(net) > materiality:
            unclassified.append(entry)

        if head and head in head_nature:
            expected_credit = head_nature[head] == "credit"
            actual_credit = net < 0
            if net != 0 and expected_credit != actual_credit and abs(net) > materiality:
                unusual_balance.append({**entry,
                    "expected_side": "credit" if expected_credit else "debit",
                    "actual_side": "credit" if actual_credit else "debit"})

        prev = r.get("prev")
        if prev not in (None, ""):
            prev_v = _n(prev)
            variance = R2(net - prev_v)
            variance_pct = R2(_safe_div(variance, abs(prev_v)) * 100) if prev_v else None
            if variance_pct is not None and abs(variance_pct) > threshold and abs(variance) > materiality:
                large_movements.append({**entry, "prev": R2(prev_v), "variance": variance, "variance_pct": variance_pct})

        out_rows.append(entry)

    total_dr, total_cr = R2(total_dr), R2(total_cr)
    return {
        "rows": out_rows, "total_dr": total_dr, "total_cr": total_cr,
        "tie_out_diff": R2(total_dr - total_cr), "balanced": abs(total_dr - total_cr) < 0.01,
        "suspense_candidates": suspense, "unusual_balances": unusual_balance,
        "unclassified": unclassified, "large_movements": large_movements,
    }


# ---------------------------------------------------------------------------
# 2. Accounting Policy Generator — tailors the policy paragraph set to the FS
#    heads actually present in the client's TB, instead of one static block.
#
# The paragraph text cites a GAAP and a tax statute by name — genuinely
# different per country, not just a number to swap (Nepal: NFRS/NAS,
# statutorily a verbatim local adoption of IFRS/IAS under local numbering;
# India: Ind AS, its own IFRS-converged standards, same numbering
# convention; UAE: IFRS/IAS directly, no local standard-setter of its own).
# _STANDARDS supplies the four citation points every paragraph below needs;
# add a country here (not a new POLICY_LIBRARY) when the next jurisdiction
# needs its own citations.
# ---------------------------------------------------------------------------
_STANDARDS = {
    "NP": {"gaap": "Nepal Financial Reporting Standards (NFRS) / NFRS for SMEs as applicable",
           "leases": "NFRS 16", "borrowing": "NAS 23", "deferred_tax": "NAS 12", "fx": "NAS 21",
           "tax_act": "the Income Tax Act, 2058"},
    "IN": {"gaap": "the Indian Accounting Standards (Ind AS) notified under the Companies (Indian Accounting "
                   "Standards) Rules, 2015, or the Accounting Standards (AS) where Ind AS does not apply",
           "leases": "Ind AS 116", "borrowing": "Ind AS 23", "deferred_tax": "Ind AS 12", "fx": "Ind AS 21",
           "tax_act": "the Income-tax Act, 1961"},
    "AE": {"gaap": "International Financial Reporting Standards (IFRS) / IFRS for SMEs as applicable",
           "leases": "IFRS 16", "borrowing": "IAS 23", "deferred_tax": "IAS 12", "fx": "IAS 21",
           "tax_act": "Federal Decree-Law No. 47 of 2022 on the Taxation of Corporations and Businesses"},
}


def _policy_library(country_code):
    s = _STANDARDS.get(country_code, _STANDARDS["NP"])
    return [
        {"key": "basis", "title": "Basis of preparation", "heads": None,
         "text": "These financial statements have been prepared on a going concern basis under the historical cost "
                 f"convention, in accordance with {s['gaap']}."},
        {"key": "estimates", "title": "Use of estimates", "heads": None,
         "text": "The preparation of financial statements requires management to make estimates and assumptions that "
                 "affect the reported amounts of assets, liabilities, income and expenses. Actual results may differ from these estimates."},
        {"key": "cash", "title": "Cash and cash equivalents", "heads": ("cash",),
         "text": "Cash and cash equivalents comprise cash in hand, balances with banks and short-term deposits with an "
                 "original maturity of three months or less."},
        {"key": "revenue", "title": "Revenue recognition", "heads": ("revenue", "other_income"),
         "text": "Revenue is recognised to the extent it is probable that economic benefits will flow to the entity and "
                 "the amount can be reliably measured, net of trade discounts and value added tax."},
        {"key": "ppe", "title": "Property, plant and equipment", "heads": ("ppe", "acc_dep", "cwip"),
         "text": "Property, plant and equipment are stated at cost less accumulated depreciation and impairment, if any. "
                 "Depreciation is provided on a systematic basis over the estimated useful life of each class of asset."},
        {"key": "intangible", "title": "Intangible assets", "heads": ("intangible",),
         "text": "Intangible assets are stated at cost less accumulated amortisation and impairment, if any, and are "
                 "amortised over their estimated useful economic life."},
        {"key": "inventory", "title": "Inventories", "heads": ("inventories", "opening_stock", "closing_stock", "purchases"),
         "text": "Inventories are valued at the lower of cost and net realisable value. Cost is determined on a "
                 "first-in-first-out / weighted average basis as applicable to each class of inventory."},
        {"key": "receivables", "title": "Trade receivables and impairment", "heads": ("trade_rec",),
         "text": "Trade receivables are stated at their transaction value less an allowance for expected credit losses, "
                 "assessed based on the age of the receivable and specific knowledge of the counterparty's ability to pay."},
        {"key": "borrowing", "title": "Borrowings and finance costs", "heads": ("lt_borrow", "st_borrow", "finance"),
         "text": "Borrowings are initially recognised at the amount of proceeds received, net of transaction costs, and "
                 "subsequently measured at amortised cost. Finance costs are recognised in profit or loss in the period "
                 f"incurred, except to the extent capitalised against a qualifying asset under {s['borrowing']}."},
        {"key": "leases", "title": "Leases", "heads": ("lt_prov", "other_ncl"),
         "text": "At inception of a contract, the entity assesses whether the contract is, or contains, a lease under "
                 f"{s['leases']}. Right-of-use assets and lease liabilities are recognised for leases other than short-term and "
                 "low-value leases, which are expensed on a straight-line basis over the lease term."},
        {"key": "provisions", "title": "Provisions and contingent liabilities", "heads": ("lt_prov", "st_prov"),
         "text": "Provisions are recognised when the entity has a present obligation as a result of a past event, it is "
                 "probable that an outflow of resources will be required to settle the obligation, and a reliable "
                 "estimate can be made of the amount. Contingent liabilities are disclosed but not recognised."},
        {"key": "tax", "title": "Taxation", "heads": ("current_tax", "deferred_tax", "dtl", "dta", "tax_prov"),
         "text": "Income tax expense comprises current and deferred tax. Current tax is the expected tax payable on "
                 f"taxable income for the year, computed under {s['tax_act']}. Deferred tax is recognised on "
                 f"temporary differences between the carrying amounts of assets/liabilities and their tax base, under {s['deferred_tax']}."},
        {"key": "fx", "title": "Foreign currency transactions", "heads": ("nc_inv",),
         "text": "Transactions in foreign currencies are recorded at the exchange rate prevailing on the transaction "
                 "date. Monetary assets and liabilities denominated in foreign currency are translated at the closing "
                 f"rate at the reporting date, with exchange differences recognised in profit or loss, under {s['fx']}."},
        {"key": "equity", "title": "Share capital and reserves", "heads": ("share_capital", "reserves"),
         "text": "Ordinary shares are classified as equity. Reserves represent amounts set aside from profit or arising "
                 "on specific transactions, as disclosed in the Statement of Changes in Equity."},
    ]


def suggest_policies(heads_present, country_code="NP"):
    """heads_present: iterable of FS head ids with a non-zero TB balance this year."""
    present = set(heads_present or [])
    out = []
    for p in _policy_library(country_code):
        if p["heads"] is None or present.intersection(p["heads"]):
            out.append({"key": p["key"], "title": p["title"], "text": p["text"]})
    return out


# ---------------------------------------------------------------------------
# 3. Cash Flow Statement Automator (NAS 7, indirect method) — a guided
#    worksheet: the preparer classifies each adjustment/working-capital/
#    investing/financing line, the engine totals by section and ties out to
#    the actual closing cash balance.
# ---------------------------------------------------------------------------
CF_SECTIONS = ("operating_pat_start", "operating_adjustment", "working_capital", "operating_other", "investing", "financing")


def cash_flow_statement(lines, opening_cash=0, closing_cash_actual=None):
    by_section = {s: [] for s in CF_SECTIONS}
    totals = {s: 0.0 for s in CF_SECTIONS}
    for ln in lines or []:
        section = ln.get("section") if ln.get("section") in CF_SECTIONS else "operating_adjustment"
        amount = _n(ln.get("amount"))
        by_section[section].append({"label": ln.get("label", ""), "amount": R2(amount), "note": ln.get("note", "")})
        totals[section] += amount

    net_operating = totals["operating_pat_start"] + totals["operating_adjustment"] + totals["working_capital"] + totals["operating_other"]
    net_investing = totals["investing"]
    net_financing = totals["financing"]
    net_change = net_operating + net_investing + net_financing
    opening = _n(opening_cash)
    computed_closing = R2(opening + net_change)
    result = {
        "by_section": by_section,
        "totals": {k: R2(v) for k, v in totals.items()},
        "net_operating": R2(net_operating), "net_investing": R2(net_investing), "net_financing": R2(net_financing),
        "net_change": R2(net_change), "opening_cash": R2(opening), "computed_closing_cash": computed_closing,
    }
    if closing_cash_actual not in (None, ""):
        actual = R2(_n(closing_cash_actual))
        result["closing_cash_actual"] = actual
        result["reconciliation_diff"] = R2(computed_closing - actual)
        result["ties_out"] = abs(computed_closing - actual) < 0.01
    return result


# ---------------------------------------------------------------------------
# 4. Statement of Changes in Equity Automator
# ---------------------------------------------------------------------------
def equity_movement(components):
    out = []
    totals = {"opening_balance": 0.0, "profit_for_year": 0.0, "oci": 0.0, "dividend": 0.0,
              "share_issued": 0.0, "share_buyback": 0.0, "transfer_in": 0.0, "transfer_out": 0.0,
              "other_movement": 0.0, "closing_balance": 0.0}
    for c in components or []:
        opening = _n(c.get("opening_balance"))
        profit = _n(c.get("profit_for_year"))
        oci = _n(c.get("oci"))
        dividend = _n(c.get("dividend"))
        issued = _n(c.get("share_issued"))
        buyback = _n(c.get("share_buyback"))
        tin = _n(c.get("transfer_in"))
        tout = _n(c.get("transfer_out"))
        other = _n(c.get("other_movement"))
        closing = opening + profit + oci - dividend + issued - buyback + tin - tout + other
        row = {"name": c.get("name", ""), "opening_balance": R2(opening), "profit_for_year": R2(profit),
               "oci": R2(oci), "dividend": R2(dividend), "share_issued": R2(issued), "share_buyback": R2(buyback),
               "transfer_in": R2(tin), "transfer_out": R2(tout), "other_movement": R2(other), "closing_balance": R2(closing)}
        out.append(row)
        for k in totals:
            totals[k] += row[k]
    return {"components": out, "totals": {k: R2(v) for k, v in totals.items()}}


# ---------------------------------------------------------------------------
# 5. Accrual & Provision Automator (NAS 37-style opening + charge - utilised/reversed)
# ---------------------------------------------------------------------------
def provision_movement(items):
    out = []
    total_opening = total_charge = total_closing = 0.0
    for it in items or []:
        opening = _n(it.get("opening_balance"))
        charge = _n(it.get("charge_for_year"))
        utilized = _n(it.get("utilized"))
        reversed_ = _n(it.get("reversed"))
        closing = opening + charge - utilized - reversed_
        out.append({"item": it.get("item", ""), "category": it.get("category", "provision"),
                    "opening_balance": R2(opening), "charge_for_year": R2(charge), "utilized": R2(utilized),
                    "reversed": R2(reversed_), "closing_balance": R2(closing)})
        total_opening += opening
        total_charge += charge
        total_closing += closing
    return {"rows": out, "total_opening": R2(total_opening), "total_charge": R2(total_charge), "total_closing": R2(total_closing)}


# ---------------------------------------------------------------------------
# 6. Prepayment Amortization Automator — straight-line over whole months,
#    using pure integer month arithmetic (no date-library drift vs the JS mirror).
# ---------------------------------------------------------------------------
def prepayment_amortization(items, as_of):
    as_of_ymd = _ymd(as_of)
    out = []
    for it in items or []:
        total = _n(it.get("total_amount"))
        s = _ymd(it.get("start_date"))
        e = _ymd(it.get("end_date"))
        if not s or not e or not as_of_ymd:
            out.append({"item": it.get("item", ""), "total_amount": R2(total), "total_months": 0,
                       "monthly_amount": 0.0, "elapsed_months": 0, "amortized_to_date": 0.0, "balance_carried_forward": R2(total)})
            continue
        total_months = _month_index(e[0], e[1]) - _month_index(s[0], s[1]) + 1
        total_months = max(1, total_months)
        monthly = R2(_safe_div(total, total_months))
        cutoff = as_of_ymd if _month_index(*as_of_ymd[:2]) < _month_index(e[0], e[1]) else (e[0], e[1], e[2])
        elapsed = _month_index(cutoff[0], cutoff[1]) - _month_index(s[0], s[1]) + 1
        elapsed = max(0, min(elapsed, total_months))
        amortized = R2(min(monthly * elapsed, total))
        out.append({"item": it.get("item", ""), "total_amount": R2(total), "total_months": total_months,
                    "monthly_amount": monthly, "elapsed_months": elapsed, "amortized_to_date": amortized,
                    "balance_carried_forward": R2(total - amortized)})
    total_amount = R2(sum(r["total_amount"] for r in out))
    total_amortized = R2(sum(r["amortized_to_date"] for r in out))
    return {"rows": out, "total_amount": total_amount, "total_amortized": total_amortized,
           "total_balance_carried_forward": R2(total_amount - total_amortized)}


# ---------------------------------------------------------------------------
# 7. Foreign Currency Revaluation Automator (NAS 21 monetary items)
# ---------------------------------------------------------------------------
def fx_revaluation(items):
    out = []
    total_pl = 0.0
    for it in items or []:
        fa = _n(it.get("foreign_amount"))
        booked_rate = _n(it.get("booked_rate"))
        closing_rate = _n(it.get("closing_rate"))
        classification = it.get("classification") or "asset"
        booked_npr = R2(fa * booked_rate)
        revalued_npr = R2(fa * closing_rate)
        raw_movement = R2(revalued_npr - booked_npr)
        pl_impact = raw_movement if classification == "asset" else R2(-raw_movement)
        out.append({"item": it.get("item", ""), "currency": it.get("currency", ""), "foreign_amount": R2(fa),
                    "booked_rate": R2(booked_rate), "closing_rate": R2(closing_rate), "classification": classification,
                    "booked_npr": booked_npr, "revalued_npr": revalued_npr, "gain_loss": pl_impact})
        total_pl += pl_impact
    return {"rows": out, "total_gain_loss": R2(total_pl)}


# ---------------------------------------------------------------------------
# 8. Interest Accrual Automator (generalised beyond loans — FDs, intercompany,
#    staff loans; day-count based, simple or annual compounding)
# ---------------------------------------------------------------------------
def interest_accrual(items):
    out = []
    total = 0.0
    for it in items or []:
        principal = _n(it.get("principal"))
        rate = _n(it.get("rate_pct"))
        days = max(0, _days_between(it.get("from_date"), it.get("to_date")))
        basis = _n(it.get("day_count_basis"), 365) or 365
        compounding = it.get("compounding") or "simple"
        if compounding == "annual":
            years = days / basis
            accrued = principal * (((1 + rate / 100) ** years) - 1) if principal else 0.0
        else:
            accrued = principal * (rate / 100) * (days / basis)
        accrued = R2(accrued)
        out.append({"item": it.get("item", ""), "principal": R2(principal), "rate_pct": rate, "days": days,
                    "day_count_basis": basis, "compounding": compounding, "accrued_interest": accrued})
        total += accrued
    return {"rows": out, "total_accrued_interest": R2(total)}


# ---------------------------------------------------------------------------
# 9. Borrowing Cost Calculator — NAS 23 capitalization to a qualifying asset
# ---------------------------------------------------------------------------
def borrowing_cost_capitalization(specific_borrowings, general_borrowings, capitalizable_expenditure_general,
                                   period_days=365, income_from_temp_investment=0):
    period_days = _n(period_days, 365) or 365
    specific_cost = 0.0
    specific_rows = []
    for b in specific_borrowings or []:
        principal = _n(b.get("principal"))
        rate = _n(b.get("rate_pct"))
        cost = R2(principal * (rate / 100) * (period_days / 365))
        specific_rows.append({"principal": R2(principal), "rate_pct": rate, "cost": cost})
        specific_cost += cost
    specific_cost = R2(specific_cost - _n(income_from_temp_investment))

    gen_principal_total = sum(_n(b.get("principal")) for b in general_borrowings or [])
    gen_cost_total = 0.0
    general_rows = []
    for b in general_borrowings or []:
        principal = _n(b.get("principal"))
        rate = _n(b.get("rate_pct"))
        cost = R2(principal * (rate / 100) * (period_days / 365))
        general_rows.append({"principal": R2(principal), "rate_pct": rate, "cost": cost})
        gen_cost_total += cost
    weighted_avg_rate = R2(_safe_div(sum(_n(b.get("principal")) * _n(b.get("rate_pct")) for b in general_borrowings or []), gen_principal_total))
    gen_cost_total = R2(gen_cost_total)

    capitalizable_expenditure_general = _n(capitalizable_expenditure_general)
    general_capitalized_uncapped = R2(capitalizable_expenditure_general * (weighted_avg_rate / 100) * (period_days / 365))
    general_capitalized = R2(min(general_capitalized_uncapped, gen_cost_total)) if general_borrowings else 0.0

    total_capitalized = R2(max(0.0, specific_cost) + general_capitalized)
    return {
        "specific_borrowings": specific_rows, "specific_cost": specific_cost,
        "general_borrowings": general_rows, "general_borrowing_cost_incurred": gen_cost_total,
        "weighted_avg_capitalization_rate_pct": weighted_avg_rate,
        "capitalizable_expenditure_general": R2(capitalizable_expenditure_general),
        "general_capitalized_uncapped": general_capitalized_uncapped, "general_capitalized": general_capitalized,
        "total_capitalized": total_capitalized,
    }


# ---------------------------------------------------------------------------
# 10. Year-End Closing Automator — checklist completion (same shape as every
#     other checklist-consolidation table this app already uses).
# ---------------------------------------------------------------------------
def checklist_summary(items):
    by_area = {}
    for c in items or []:
        area = c.get("area", "")
        g = by_area.setdefault(area, {"total": 0, "done": 0})
        g["total"] += 1
        if c.get("status") == "done":
            g["done"] += 1
    return {area: {"total": g["total"], "done": g["done"], "completion_pct": R2(_safe_div(g["done"], g["total"]) * 100)}
           for area, g in by_area.items()}
