"""
CA Automator — Corporate Advisory computation engines.

Pure functions only (no DB access), mirrored line-for-line in
web/corporate_engines.js — same discipline as tp_engines.py / forensic_engines.py.
Fifteen named tools reduce to a handful of real calculators plus completeness
checks over the ca_share_transaction / ca_board_meeting registers:

  - Rights Issue / Bonus Share / Capital Reduction Calculators — plain
    corporate-finance ratio arithmetic (TERP, value of a right, capitalised
    reserves), never a fabricated statutory percentage.
  - Dividend Distribution Checker — a retained-earnings sufficiency test.
    The mandatory-reserve percentage, where a firm applies one, is always an
    INPUT the preparer supplies (from the entity's own AOA/board policy or a
    verified statutory reading), never hard-coded here.
  - Shareholding Structure Analyzer — ownership %, concentration and
    related-party exposure over the shareholder register.
  - Board Compliance Tracker — meeting cadence and minutes-filing
    completeness against a firm-supplied max-gap-days assumption, not an
    invented statutory frequency.
  - Share Transfer / Private Placement / M&A / Capital Reduction Compliance
    Checkers — ONE completeness check over ca_share_transaction rows
    (does this row have the approval/filing dates its event_type normally
    needs?), not six separate engines.
"""

from datetime import date

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 rights_issue(inputs):
    """
    Standard "X new shares for every Y held" rights issue math.
    TERP = theoretical ex-rights price; value of a right = cum-rights price - TERP.
    """
    existing_shares = _n(inputs.get("existing_shares"))
    ratio_new = _n(inputs.get("ratio_new"), 1)
    ratio_held = _n(inputs.get("ratio_held"), 1)
    subscription_price = _n(inputs.get("subscription_price"))
    cum_rights_price = _n(inputs.get("cum_rights_price"))

    new_shares = existing_shares * (ratio_new / ratio_held) if ratio_held else 0.0
    total_shares_after = existing_shares + new_shares
    proceeds = new_shares * subscription_price
    terp = ((existing_shares * cum_rights_price) + (new_shares * subscription_price)) / total_shares_after \
        if total_shares_after else 0.0
    value_of_right = cum_rights_price - terp

    return {
        "existing_shares": R2(existing_shares), "ratio_new": ratio_new, "ratio_held": ratio_held,
        "new_shares": R2(new_shares), "total_shares_after": R2(total_shares_after),
        "subscription_price": R2(subscription_price), "cum_rights_price": R2(cum_rights_price),
        "proceeds": R2(proceeds), "theoretical_ex_rights_price": R2(terp),
        "value_of_right": R2(value_of_right),
    }


def bonus_issue(inputs):
    """Capitalises free reserves into new fully-paid shares — a fund-sufficiency check, not a legal opinion."""
    existing_shares = _n(inputs.get("existing_shares"))
    ratio_new = _n(inputs.get("ratio_new"), 1)
    ratio_held = _n(inputs.get("ratio_held"), 1)
    face_value = _n(inputs.get("face_value"), 100)
    reserves_available = _n(inputs.get("reserves_available"))

    bonus_shares = existing_shares * (ratio_new / ratio_held) if ratio_held else 0.0
    capitalized_amount = bonus_shares * face_value
    post_bonus_shares = existing_shares + bonus_shares
    reserves_sufficient = capitalized_amount <= reserves_available
    reserves_shortfall = max(0.0, capitalized_amount - reserves_available)

    return {
        "existing_shares": R2(existing_shares), "ratio_new": ratio_new, "ratio_held": ratio_held,
        "face_value": R2(face_value), "bonus_shares": R2(bonus_shares),
        "post_bonus_shares": R2(post_bonus_shares), "capitalized_amount": R2(capitalized_amount),
        "reserves_available": R2(reserves_available), "reserves_sufficient": reserves_sufficient,
        "reserves_shortfall": R2(reserves_shortfall),
    }


def capital_reduction(inputs):
    """Old capital -> new capital under a reduction ratio or a flat amount, with the per-share impact."""
    existing_capital = _n(inputs.get("existing_capital"))
    existing_shares = _n(inputs.get("existing_shares"))
    face_value = _n(inputs.get("face_value"), 100)
    mode = inputs.get("mode") or "amount"

    if mode == "ratio":
        reduction_pct = _n(inputs.get("reduction_pct"))
        reduction_amount = existing_capital * reduction_pct / 100
    else:
        reduction_amount = _n(inputs.get("reduction_amount"))
        reduction_pct = (reduction_amount / existing_capital * 100) if existing_capital else 0.0

    new_capital = max(0.0, existing_capital - reduction_amount)
    new_shares = (new_capital / face_value) if face_value else 0.0
    shares_cancelled = max(0.0, existing_shares - new_shares)

    return {
        "existing_capital": R2(existing_capital), "existing_shares": R2(existing_shares),
        "face_value": R2(face_value), "reduction_pct": R2(reduction_pct),
        "reduction_amount": R2(reduction_amount), "new_capital": R2(new_capital),
        "new_shares": R2(new_shares), "shares_cancelled": R2(shares_cancelled),
    }


def dividend_distribution_check(inputs):
    """
    Retained-earnings sufficiency test: proposed dividend against what is
    actually available to distribute. `mandatory_reserve_pct` is always the
    preparer's own confirmed input, never assumed here.
    """
    net_profit = _n(inputs.get("net_profit_current_year"))
    accumulated_losses_bf = _n(inputs.get("accumulated_losses_bf"))
    free_reserves = _n(inputs.get("free_reserves"))
    mandatory_reserve_pct = _n(inputs.get("mandatory_reserve_pct"))
    proposed_dividend = _n(inputs.get("proposed_dividend_amount"))

    profit_after_losses = net_profit - accumulated_losses_bf
    mandatory_reserve_amount = max(0.0, profit_after_losses) * mandatory_reserve_pct / 100
    distributable_profit = max(0.0, profit_after_losses - mandatory_reserve_amount) + free_reserves
    shortfall = max(0.0, proposed_dividend - distributable_profit)
    sufficient = proposed_dividend <= distributable_profit

    flags = []
    if accumulated_losses_bf > 0 and net_profit <= accumulated_losses_bf:
        flags.append({"severity": "High", "message": "Current year profit does not even cover brought-forward accumulated losses — no profit is available for distribution before those losses are made good."})
    if not sufficient:
        flags.append({"severity": "High", "message": f"Proposed dividend exceeds distributable profit by {shortfall:,.0f}."})
    if mandatory_reserve_pct <= 0:
        flags.append({"severity": "Low", "message": "No mandatory reserve percentage has been entered — confirm the entity's own AOA / statutory reserve requirement before relying on this figure."})

    return {
        "net_profit_current_year": R2(net_profit), "accumulated_losses_bf": R2(accumulated_losses_bf),
        "profit_after_losses": R2(profit_after_losses), "mandatory_reserve_pct": mandatory_reserve_pct,
        "mandatory_reserve_amount": R2(mandatory_reserve_amount), "free_reserves": R2(free_reserves),
        "distributable_profit": R2(distributable_profit), "proposed_dividend_amount": R2(proposed_dividend),
        "sufficient": sufficient, "shortfall": R2(shortfall), "flags": flags,
    }


def shareholding_analysis(shareholders):
    """Ownership %, concentration and related-party/director exposure over the shareholder register."""
    rows = shareholders or []
    total_shares = sum(_n(s.get("shares_held")) for s in rows)
    out = []
    for s in rows:
        shares = _n(s.get("shares_held"))
        pct = (shares / total_shares * 100) if total_shares else 0.0
        out.append({"id": s.get("id"), "name": s.get("name", ""), "share_class": s.get("share_class", "equity"),
                    "shares_held": R2(shares), "ownership_pct": R2(pct),
                    "is_related_party": bool(s.get("is_related_party")), "is_director": bool(s.get("is_director"))})
    out.sort(key=lambda r: r["ownership_pct"], reverse=True)

    top5_pct = R2(sum(r["ownership_pct"] for r in out[:5]))
    related_party_pct = R2(sum(r["ownership_pct"] for r in out if r["is_related_party"]))
    director_pct = R2(sum(r["ownership_pct"] for r in out if r["is_director"]))
    by_class = {}
    for r in out:
        by_class.setdefault(r["share_class"], 0.0)
        by_class[r["share_class"]] += r["shares_held"]

    return {
        "holders": out, "total_shares": R2(total_shares), "holder_count": len(out),
        "top5_concentration_pct": top5_pct, "related_party_pct": related_party_pct,
        "director_holding_pct": director_pct, "shares_by_class": {k: R2(v) for k, v in by_class.items()},
    }


def board_compliance(meetings, max_gap_days=None):
    """Meeting cadence and minutes-filing completeness. max_gap_days is the firm's own working assumption."""
    board_meetings = sorted(
        [m for m in (meetings or []) if (m.get("meeting_type") or "board") == "board" and m.get("status") != "cancelled"],
        key=lambda m: m.get("meeting_date") or "")
    gaps = []
    for i in range(1, len(board_meetings)):
        try:
            d0 = date.fromisoformat(board_meetings[i - 1]["meeting_date"][:10])
            d1 = date.fromisoformat(board_meetings[i]["meeting_date"][:10])
            gap_days = (d1 - d0).days
        except (ValueError, TypeError, KeyError):
            gap_days = None
        gaps.append({"from": board_meetings[i - 1].get("meeting_date"), "to": board_meetings[i].get("meeting_date"),
                    "gap_days": gap_days,
                    "exceeds_max": bool(max_gap_days and gap_days is not None and gap_days > max_gap_days)})

    total = len(meetings or [])
    minutes_filed = len([m for m in (meetings or []) if m.get("minutes_recorded")])
    minutes_filed_pct = R2(minutes_filed / total * 100) if total else 0.0
    breaches = [g for g in gaps if g["exceeds_max"]]

    return {
        "meetings_count": total, "board_meetings_count": len(board_meetings),
        "minutes_filed_pct": minutes_filed_pct, "gaps": gaps,
        "gap_breaches_count": len(breaches), "max_gap_days": max_gap_days,
    }


# Which capital events, under normal Companies Act practice, need a
# shareholder resolution (AGM/EGM) in addition to board approval — used only
# to decide which completeness checks apply to which row, never as a cited
# legal citation of its own.
_REQUIRES_AGM = {"rights_issue", "bonus_issue", "private_placement", "capital_reduction", "buyback"}


def share_transaction_compliance(transactions):
    """Completeness check over the capital-events register — does each row carry the dates its event_type needs?"""
    out = []
    for t in transactions or []:
        event_type = t.get("event_type", "")
        needs_agm = event_type in _REQUIRES_AGM
        missing = []
        if not t.get("board_approval_date"):
            missing.append("board approval date")
        if needs_agm and not t.get("agm_approval_date"):
            missing.append("AGM/EGM (shareholder) approval date")
        if not t.get("ocr_filed_date") and t.get("status") in ("approved", "completed"):
            missing.append("OCR filing date")
        out.append({"id": t.get("id"), "event_type": event_type, "status": t.get("status"),
                    "needs_agm_approval": needs_agm, "missing": missing, "complete": not missing})

    total = len(out)
    complete = len([r for r in out if r["complete"]])
    return {"rows": out, "total": total, "complete_count": complete,
           "complete_pct": R2(complete / total * 100) if total else 0.0}
