"""
CA Automator — India-specific calculation engines.

Nepal's engines (server/engines.py) bake in Nepal Income Tax Act 2058
mechanics directly (Schedule 2 absorption tranches by Bikram Sambat month,
Sec 117/118/119 instalment structure, single-rate VAT) — not just different
numbers, a genuinely different computational shape. This module is the
India-shaped counterpart for the three domains flagged in PROJECT_STATE.md
as needing real engine work rather than a rate_master seed:

  india_depreciation()     Sec 32 block-of-assets WDV, 180-day put-to-use rule
  india_gst_return()       multi-slab GST, CGST/SGST vs IGST, input tax credit
  india_interest_penalty() Sec 234A/234B/234C/234F

Same design rule as engines.py: no statutory number is hard-coded here — every
rate/threshold is read from rate_master (domain-scoped, country='IN' via the
ambient country_service context, same as every other engine) and every
UNVERIFIED code is surfaced back to the caller, never silently trusted.

These are deliberately reasonable, defensible core models, not exhaustive,
filing-ready implementations of Indian tax law — several genuine simplifying
choices are called out in comments at the point they're made. None of this
has been reviewed by a qualified Indian tax professional; see the UNVERIFIED
convention this whole app already uses for regulatory figures.
"""
from db import rate_dict

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


def _val(rates, code, default=None):
    r = rates.get(code)
    if not r:
        return default
    return r["value_num"] if r["value_num"] is not None else default


def _dynamic_categories(rates, prefix):
    """Same discovery mechanism engines.py already uses for company_rate_/
    tds_rate_ — a rate_master row starting with `prefix` becomes a category
    with no code change, here used for depreciation blocks and GST slabs."""
    out = []
    for code, row in rates.items():
        if code.startswith(prefix) and code != prefix:
            out.append({"code": code, "slug": code[len(prefix):], "label": row["label"], "row": row})
    out.sort(key=lambda c: c["label"])
    return out


# --------------------------------------------------------- 1. DEPRECIATION

def india_depreciation(fy, blocks):
    """
    Income-tax Act, 1961, Sec 32 — block-of-assets WDV depreciation.

    Structurally simpler than Nepal's Schedule 2 (engines.depreciation): no
    absorption tranches, no repair-ceiling capitalisation (a "current
    repair" is ordinary revenue expenditure, fully deductible outside this
    engine entirely, not a capitalised addition to the block) — just the
    180-day "put to use" test: an addition used for 180 days or more in the
    year gets the block's full rate; used for fewer than 180 days gets HALF
    the rate, on that addition's own value only.

    blocks: [{block: a slug matching a depreciation.block_<slug>_rate row
              (e.g. 'building_5', 'plant_15', 'computer_40',
              'intangible_25' — whatever's seeded; add a category by
              adding a rate_master row, same dynamic-category mechanism as
              company_rate_/tds_rate_ in engines.py),
             opening_wdv, additions: [{desc, amount, put_to_use_days}],
             disposal_proceeds}]
    """
    rates, unverified = rate_dict("depreciation", fy)
    results, totals = [], {"opening": 0.0, "additions": 0.0, "disposals": 0.0,
                            "depreciation": 0.0, "stcg": 0.0, "closing": 0.0}
    for b in blocks:
        key = str(b.get("block", "")).strip()
        rate = _val(rates, f"block_{key}_rate", 0) or 0
        opening = float(b.get("opening_wdv", 0) or 0)

        full_rate_additions, half_rate_additions, add_lines = 0.0, 0.0, []
        for a in b.get("additions", []) or []:
            amt = float(a.get("amount", 0) or 0)
            days = int(a.get("put_to_use_days", 365) or 365)
            eligible_full = days >= 180
            if eligible_full:
                full_rate_additions += amt
            else:
                half_rate_additions += amt
            add_lines.append({"desc": a.get("desc", ""), "amount": R2(amt), "put_to_use_days": days,
                              "rate_eligibility": "full" if eligible_full else "half (< 180 days)"})

        disposal_proceeds = float(b.get("disposal_proceeds", 0) or 0)
        wdv_before_dep = opening + full_rate_additions + half_rate_additions - disposal_proceeds

        note = ""
        if wdv_before_dep <= 0:
            # Sec 50: WDV reduced to nil or negative by disposal proceeds is
            # a short-term capital gain, not a depreciation computation —
            # the block closes either way.
            dep, stcg, closing = 0.0, abs(wdv_before_dep), 0.0
            note = ("Disposal proceeds exceed the block's WDV — short-term capital gain"
                    " under Sec 50; block closed." if wdv_before_dep < 0 else
                    "Block WDV reduced to nil by disposal.")
        else:
            # Disposal proceeds are modelled as reducing the block's OLDER
            # value (opening + full-rate additions) before this year's
            # half-rate additions — a defensible, commonly-used ordering,
            # not the only one a preparer might justify. Reviewed against a
            # specific fact pattern before relying on this for a filing.
            half_rate_base = min(half_rate_additions, wdv_before_dep)
            full_rate_base = wdv_before_dep - half_rate_base
            dep = full_rate_base * rate + half_rate_base * (rate / 2)
            stcg, closing = 0.0, wdv_before_dep - dep

        results.append({
            "block": key, "rate": rate, "opening_wdv": R2(opening),
            "additions": add_lines, "full_rate_additions": R2(full_rate_additions),
            "half_rate_additions": R2(half_rate_additions), "disposal_proceeds": R2(disposal_proceeds),
            "wdv_before_depreciation": R2(max(0.0, wdv_before_dep)),
            "depreciation": R2(dep), "short_term_capital_gain": R2(stcg),
            "closing_wdv": R2(closing), "note": note,
        })
        totals["opening"] += opening
        totals["additions"] += full_rate_additions + half_rate_additions
        totals["disposals"] += disposal_proceeds
        totals["depreciation"] += dep
        totals["stcg"] += stcg
        totals["closing"] += max(0.0, closing)

    return {"blocks": results, "totals": {k: R2(v) for k, v in totals.items()}, "unverified": unverified}


# --------------------------------------------------------------------- 2. GST

def india_gst_return(fy, data):
    """
    Multi-slab GST (CGST Act 2017 / IGST Act 2017) — structurally different
    from Nepal's single-rate VAT (engines.py's `vat` domain), which is why
    this needs its own domain ('gst') and its own engine rather than a
    rate_master seed against the existing vat_return().

    Slabs are discovered dynamically from rate_master (domain='gst', prefix
    'gst_rate_') — same mechanism as company_rate_/tds_rate_ elsewhere.
    An intra-state line splits its tax into CGST+SGST (half each); an
    inter-state line charges the full rate as IGST.

    data: {
      outward_supplies: [{description, taxable_value, gst_rate_slug, is_interstate}],
      inward_supplies:  [{description, taxable_value, gst_rate_slug, is_interstate, itc_eligible}],
    }

    Input tax credit cross-utilisation (Sec 49/49A/49B) is modelled only for
    the single most-relied-on rule — leftover IGST credit applied against
    CGST then SGST liability. Anything more granular (the specific ordering
    when a return has multiple simultaneous credit/liability heads) needs a
    professional's review before this is used for an actual filing.
    """
    rates, unverified = rate_dict("gst", fy)
    categories = _dynamic_categories(rates, "gst_rate_")
    rate_by_slug = {c["slug"]: float(c["row"]["value_num"] or 0) for c in categories}

    def _line_tax(line):
        slug = line.get("gst_rate_slug")
        rate = rate_by_slug.get(slug, 0.0)
        taxable = float(line.get("taxable_value", 0) or 0)
        tax = R2(taxable * rate)
        if line.get("is_interstate"):
            return {"taxable_value": R2(taxable), "rate": rate, "igst": tax, "cgst": 0.0, "sgst": 0.0, "total_tax": tax}
        half = R2(tax / 2)
        return {"taxable_value": R2(taxable), "rate": rate, "igst": 0.0, "cgst": half,
                "sgst": R2(tax - half), "total_tax": tax}

    outward = []
    for l in data.get("outward_supplies", []) or []:
        row = _line_tax(l); row["description"] = l.get("description", ""); outward.append(row)

    inward = []
    itc_igst = itc_cgst = itc_sgst = 0.0
    for l in data.get("inward_supplies", []) or []:
        row = _line_tax(l); row["description"] = l.get("description", "")
        eligible = l.get("itc_eligible", True)
        row["itc_eligible"] = bool(eligible)
        if eligible:
            itc_igst += row["igst"]; itc_cgst += row["cgst"]; itc_sgst += row["sgst"]
        inward.append(row)

    output_igst = sum(l["igst"] for l in outward)
    output_cgst = sum(l["cgst"] for l in outward)
    output_sgst = sum(l["sgst"] for l in outward)

    net_igst = output_igst - itc_igst
    net_cgst = output_cgst - itc_cgst
    net_sgst = output_sgst - itc_sgst

    igst_credit_carry = max(0.0, -net_igst)
    net_igst = max(0.0, net_igst)
    if igst_credit_carry and net_cgst > 0:
        applied = min(igst_credit_carry, net_cgst)
        net_cgst -= applied; igst_credit_carry -= applied
    if igst_credit_carry and net_sgst > 0:
        applied = min(igst_credit_carry, net_sgst)
        net_sgst -= applied; igst_credit_carry -= applied

    return {
        "outward_supplies": outward, "inward_supplies": inward,
        "output_tax": {"igst": R2(output_igst), "cgst": R2(output_cgst), "sgst": R2(output_sgst)},
        "input_tax_credit": {"igst": R2(itc_igst), "cgst": R2(itc_cgst), "sgst": R2(itc_sgst)},
        "net_payable": {"igst": R2(net_igst), "cgst": R2(net_cgst), "sgst": R2(net_sgst)},
        "igst_credit_carried_forward": R2(igst_credit_carry),
        "total_payable": R2(net_igst + net_cgst + net_sgst),
        "unverified": unverified,
    }


# ------------------------------------------------------- 3. INTEREST & LATE FEE

def india_interest_penalty(fy, data):
    """
    Income-tax Act, 1961 — interest for default in furnishing a return or
    paying advance tax (Sec 234A/234B/234C) and the flat fee for late
    filing (Sec 234F). Rates/thresholds come from rate_master (domain
    'penalty', country IN) — see seed.sql's India section.

    data: {
      assessed_tax, tax_already_paid (TDS + advance tax + self-assessment,
        combined — the base 234A interest runs against),
      months_late_filing,
      advance_tax_paid_total, months_since_year_end (234B),
      advance_tax_paid_by_jun15/sep15/dec15/mar15 (234C — cumulative amount
        actually paid by each statutory date),
      total_income, filed_after_due_date (bool),
    }
    """
    rates, unverified = rate_dict("penalty", fy)
    interest_rate = _val(rates, "s234_interest_rate_pct", 0) or 0
    lines = []

    def add(head, basis, amount):
        if amount:
            lines.append({"head": head, "basis": basis, "amount": R2(amount)})

    # Sec 234A — 1%/month simple interest on the shortfall between assessed
    # tax and what was already paid (TDS/advance tax/self-assessment),
    # for each month (or part-month) the return is filed late.
    assessed = float(data.get("assessed_tax", 0) or 0)
    already_paid = float(data.get("tax_already_paid", 0) or 0)
    shortfall_234a = max(0.0, assessed - already_paid)
    months_late = float(data.get("months_late_filing", 0) or 0)
    if shortfall_234a and months_late:
        add("Sec 234A — interest for late filing of return",
            f"{shortfall_234a:,.2f} shortfall x {interest_rate * 100:.0f}% x {months_late:.0f} month(s)",
            shortfall_234a * interest_rate * months_late)

    # Sec 234B — 1%/month from April 1 following the FY until payment/
    # assessment, on the shortfall if advance tax paid is below 90% of
    # assessed tax (no interest at all if the 90% threshold was met).
    advance_paid = float(data.get("advance_tax_paid_total", 0) or 0)
    if assessed and advance_paid < assessed * 0.9:
        shortfall_234b = assessed - advance_paid
        months_234b = float(data.get("months_since_year_end", 0) or 0)
        add("Sec 234B — interest for shortfall in advance tax",
            f"{shortfall_234b:,.2f} shortfall x {interest_rate * 100:.0f}% x {months_234b:.0f} month(s)",
            shortfall_234b * interest_rate * months_234b)

    # Sec 234C — 4 statutory checkpoints; 1%/month for 3 months each,
    # except the final one (Mar 15), which is 1 month only.
    checkpoints = [
        ("Jun 15 (15% due)", 0.15, "advance_tax_paid_by_jun15", 3),
        ("Sep 15 (45% due)", 0.45, "advance_tax_paid_by_sep15", 3),
        ("Dec 15 (75% due)", 0.75, "advance_tax_paid_by_dec15", 3),
        ("Mar 15 (100% due)", 1.00, "advance_tax_paid_by_mar15", 1),
    ]
    for label, required_pct, paid_key, months in checkpoints:
        required = assessed * required_pct
        paid = float(data.get(paid_key, 0) or 0)
        shortfall = max(0.0, required - paid)
        if shortfall:
            # interest_rate (s234_interest_rate_pct) is already a PER-MONTH
            # rate, unlike Nepal's engines.penalty()'s s118_interest_rate
            # (annual, hence that function's own /12) — no /12 here, or
            # this silently charges a twelfth of the real amount.
            add(f"Sec 234C — instalment shortfall, {label}",
                f"Required {required_pct * 100:.0f}% of {assessed:,.2f} ({required:,.2f}) less paid"
                f" ({paid:,.2f}) x {interest_rate * 100:.0f}%/month x {months} month(s)",
                shortfall * interest_rate * months)

    # Sec 234F — flat fee for late filing, tiered by total income.
    if data.get("filed_after_due_date"):
        total_income = float(data.get("total_income", 0) or 0)
        threshold = _val(rates, "s234f_income_threshold", 500000) or 500000
        fee_code = "s234f_fee_high" if total_income > threshold else "s234f_fee_low"
        add("Sec 234F — fee for late filing", "Flat fee", _val(rates, fee_code, 0) or 0)

    return {"lines": lines, "total": R2(sum(l["amount"] for l in lines)), "unverified": unverified}
