# -*- coding: utf-8 -*-
"""
model_params.py — C6 FINANCIAL MODEL.

Every input below carries the SRC-### or ASM-### it comes from. Nothing is hard-coded
without a reference. Serves REQ-SYS-05, REQ-SYS-07, REQ-SYS-10, REQ-SYS-11.

Run directly to print the computed result set.
"""

# =========================================================================
# BLOCK 1 — SOURCED RATE INPUTS
# =========================================================================
INPUTS = {
    # key: (value, unit, source_id, confidence)
    "price_selfcare_wd":      (73.58,  "AUD/hr",  "SRC-002", "Medium"),
    "price_community_wd":     (73.58,  "AUD/hr",  "SRC-004", "Medium"),
    "price_supportcoord_l2":  (100.14, "AUD/hr",  "SRC-005", "Medium"),
    "wage_sacs_l2_casual":    (45.28,  "AUD/hr",  "SRC-009", "Medium"),
    "wage_sacs_l3_casual":    (50.61,  "AUD/hr",  "SRC-009", "Medium"),
    "oncost_super":           (0.1200, "rate",    "SRC-011", "High"),
    "oncost_workcover":       (0.0180, "rate",    "SRC-012", "Medium"),
    "oncost_portable_lsl":    (0.0165, "rate",    "SRC-013", "High"),
    "oncost_payroll_tax":     (0.0000, "rate",    "ASM-003", "Medium"),   # excluded below threshold
}

# =========================================================================
# BLOCK 2 — ASSUMED DRIVERS (change these; everything downstream recomputes)
# =========================================================================
DRIVERS = {
    "admin_hours_per_billable_hour": (0.25,  "hr",       "ASM-008", "Low"),
    "payment_lag_days":              (14,    "days",     "ASM-007", "Low"),
    "bookkeeping_per_month":         (300,   "AUD/mo",   "ASM-005", "Low"),
    "general_overhead_per_month":    (200,   "AUD/mo",   "ASM-014", "Low"),
    "website_one_off":               (2500,  "AUD",      "ASM-006", "Low"),
    "sc_utilisation":                (0.55,  "rate",     "ASM-013", "Low"),
    "sc_hours_available_per_week":   (38,    "hr",       "ASM-011", "Low"),
    "first_client_month":            (4,     "month",    "ASM-015", "Low"),
}

# =========================================================================
# BLOCK 3 — SOURCED ONE-OFF AND RECURRING COSTS
# =========================================================================
# (label, low, base, high, source_id, confidence)
ONE_OFF_COSTS = [
 ("ASIC Pty Ltd company registration",          636,   636,   636,   "SRC-025", "Medium"),
 ("ASIC business name, three years",            108,   108,   108,   "SRC-027", "Medium"),
 ("ABN, TFN, GST and PAYG registration",          0,     0,     0,   "SRC-028", "High"),
 ("myID and RAM setup",                           0,     0,     0,   "SRC-030", "High"),
 ("NDIS Worker Screening Check, per person",    139.20,139.20,139.20,"SRC-031", "High"),
 ("First aid HLTAID011 plus CPR HLTAID009",     290,   290,   290,   "SRC-041", "Medium"),
 ("NDIS Worker Orientation Module",               0,     0,     0,   "SRC-042", "Medium"),
 ("Website and brand identity",                2500,  2500,  2500,   "ASM-006", "Low"),
 ("Policy and procedure manual, verification pack", 649, 1080, 1825, "SRC-036", "High"),
 ("Verification audit fee",                    3500,  4750,  6000,   "SRC-034", "Medium"),
]
RECURRING_MONTHLY = [
 ("CRM and rostering (ShiftCare Essentials)",    45,    45,   125,   "SRC-037", "High"),
 ("Accounting and payroll (Xero)",               78,    78,   143,   "SRC-038", "High"),
 ("Bookkeeping",                                200,   300,   500,   "ASM-005", "Low"),
 ("General operating overhead",                 150,   200,   350,   "ASM-014", "Low"),
]
RECURRING_ANNUAL = [
 ("ASIC company annual review",                 342,   342,   342,   "SRC-026", "Medium"),
 ("Insurance: public liability, professional indemnity, personal accident",
                                               1200,  2350,  3500,   "SRC-040", "Low"),
]

WORKERS_AT_START = 3          # support workers screened and trained before first shift
CERT_AUDIT = (6000, 9000, 12000, "SRC-034")   # certification pathway, for reference only


# =========================================================================
# BLOCK 4 — COMPUTATION
# =========================================================================
def v(d, k):
    return d[k][0]

def oncost_multiplier():
    """Fully-loaded multiplier applied to the casual hourly rate."""
    return (1.0
            + v(INPUTS, "oncost_super")
            + v(INPUTS, "oncost_workcover")
            + v(INPUTS, "oncost_portable_lsl")
            + v(INPUTS, "oncost_payroll_tax"))

def loaded_wage(level="l2"):
    base = v(INPUTS, "wage_sacs_l2_casual") if level == "l2" else v(INPUTS, "wage_sacs_l3_casual")
    return base * oncost_multiplier()

def gross_margin_per_hour():
    """REQ-SYS-05. Price minus fully-loaded direct labour. BEFORE administration."""
    return v(INPUTS, "price_selfcare_wd") - loaded_wage("l2")

def contribution_per_hour(admin_paid: bool):
    """
    Contribution after the administration burden each billable hour creates.
    admin_paid=False : the owner does the administration unpaid (capacity-limited, not free).
    admin_paid=True  : administration is done by a paid coordinator at SACS Level 3 loaded.
    """
    gm = gross_margin_per_hour()
    if not admin_paid:
        return gm
    return gm - (loaded_wage("l3") * v(DRIVERS, "admin_hours_per_billable_hour"))

def band(rows, idx):
    return sum(r[idx] for r in rows)

def one_off_total(workers=WORKERS_AT_START, include_registration=True):
    """Low, base, high one-off capital. Per-person items scale by (owner + workers)."""
    people = workers + 1
    out = []
    for i in (1, 2, 3):
        t = 0.0
        for label, lo, ba, hi, src, conf in ONE_OFF_COSTS:
            val = (lo, ba, hi)[i - 1]
            if "per person" in label or "First aid" in label or "Orientation" in label:
                val *= people
            if not include_registration and ("audit" in label.lower() or "Policy and procedure" in label):
                val = 0
            t += val
        out.append(round(t, 2))
    return tuple(out)

def monthly_fixed():
    ann = [band(RECURRING_ANNUAL, i) / 12.0 for i in (1, 2, 3)]
    mon = [band(RECURRING_MONTHLY, i) for i in (1, 2, 3)]
    return tuple(round(m + a, 2) for m, a in zip(mon, ann))

def breakeven_hours(admin_paid: bool, which=2):
    """REQ-SYS-10. Billable hours per month at which contribution equals fixed cost."""
    return monthly_fixed()[which - 1] / contribution_per_hour(admin_paid)

def working_capital(billable_hours_per_month):
    """REQ-SYS-11. Cash needed to fund wages before payment arrives."""
    wages = billable_hours_per_month * loaded_wage("l2")
    return wages * (v(DRIVERS, "payment_lag_days") / 30.0)

# ---- Ramp and cash curve -------------------------------------------------
RAMP_BASE = [0, 0, 0, 40, 80, 130, 180, 230, 280, 320, 350, 380]   # ASM-015
RAMP_SLOW = [0, 0, 0, 0, 0, 0, 30, 70, 110, 150, 190, 230]         # first client month 7
RAMP_ZERO = [0] * 12                                                # REQ-SYS-07 downside

def cash_curve(ramp, admin_paid=False, owner_draw=0.0, which=2, include_registration=True):
    """Month-by-month cash position. Returns (rows, cumulative_min, month_cash_positive)."""
    one_off = one_off_total(include_registration=include_registration)[which - 1]
    fixed = monthly_fixed()[which - 1]
    cpu = contribution_per_hour(admin_paid)
    rows, cum, worst, first_pos = [], -one_off, -one_off, None
    for m, hrs in enumerate(ramp, start=1):
        rev = hrs * v(INPUTS, "price_selfcare_wd")
        wages = hrs * loaded_wage("l2")
        admin = hrs * loaded_wage("l3") * v(DRIVERS, "admin_hours_per_billable_hour") if admin_paid else 0.0
        contrib = hrs * cpu
        net = contrib - fixed - owner_draw
        cum += net
        worst = min(worst, cum)
        if first_pos is None and cum > 0:
            first_pos = m
        rows.append({
            "month": m, "billable_hours": hrs, "revenue": round(rev, 2),
            "direct_wages": round(wages, 2), "paid_admin": round(admin, 2),
            "contribution": round(contrib, 2), "fixed_costs": round(fixed, 2),
            "owner_draw": round(owner_draw, 2), "net_cash": round(net, 2),
            "cumulative_cash": round(cum, 2),
        })
    return rows, round(worst, 2), first_pos

MANAGER_SALARY_PA = 95000.0   # ASM-011: a passive-investor model must employ a manager

def breakeven_hours_employed_manager(which=2):
    """REQ-SYS-10, ASM-011. Break-even when the owner does NOT work in the business and a
    manager is employed on a fixed salary. This is a DIFFERENT scenario from paid administration
    (which scales with volume) and must not be confused with it."""
    fixed = monthly_fixed()[which - 1] + MANAGER_SALARY_PA / 12.0
    return fixed / contribution_per_hour(False)

def runway_downside(months=6, owner_draw=0.0, which=2, include_registration=True, workers=None):
    """REQ-SYS-07. Cash needed if no participant is onboarded for `months`."""
    if workers is None:
        workers = WORKERS_AT_START
    one_off = one_off_total(workers=workers, include_registration=include_registration)[which - 1]
    fixed = monthly_fixed()[which - 1]
    return round(one_off + (fixed + owner_draw) * months, 2)

# ---- Support coordination branch ----------------------------------------
def sc_annual():
    """Owner-delivered support coordination. No direct wage cost; the constraint is utilisation."""
    hrs_wk = v(DRIVERS, "sc_hours_available_per_week") * v(DRIVERS, "sc_utilisation")
    hrs_mo = hrs_wk * 52.0 / 12.0
    rev_mo = hrs_mo * v(INPUTS, "price_supportcoord_l2")
    fixed = monthly_fixed()[1]
    return {
        "billable_hours_per_week": round(hrs_wk, 1),
        "billable_hours_per_month": round(hrs_mo, 1),
        "revenue_per_month": round(rev_mo, 2),
        "fixed_per_month": round(fixed, 2),
        "net_per_month": round(rev_mo - fixed, 2),
        "net_per_year": round((rev_mo - fixed) * 12, 2),
        "breakeven_hours_per_month": round(fixed / v(INPUTS, "price_supportcoord_l2"), 1),
    }

def sc_sensitivity():
    out = []
    fixed = monthly_fixed()[1]
    for u in (0.25, 0.35, 0.45, 0.55, 0.65, 0.75):
        hrs_mo = v(DRIVERS, "sc_hours_available_per_week") * u * 52.0 / 12.0
        rev = hrs_mo * v(INPUTS, "price_supportcoord_l2")
        out.append((u, round(hrs_mo, 1), round(rev, 2), round((rev - fixed) * 12, 2)))
    return out

def margin_sensitivity():
    """What each uncertain input does to gross margin per billable hour."""
    gm = gross_margin_per_hour()
    price = v(INPUTS, "price_selfcare_wd")
    out = [("Base case, all inputs as sourced", round(gm, 2), "")]
    # ASM-002: Level 3 instead of Level 2
    gm_l3 = price - loaded_wage("l3")
    out.append(("ASM-002 wrong: workers sit at SACS Level 3, not Level 2", round(gm_l3, 2),
                "Margin falls %.0f%%" % ((gm - gm_l3) / gm * 100)))
    # ASM-004: WorkCover at 3.0% instead of 1.8%
    m = 1 + 0.12 + 0.030 + 0.0165
    gm_wc = price - v(INPUTS, "wage_sacs_l2_casual") * m
    out.append(("ASM-004 wrong: WorkCover industry rate is 3.0%, not the 1.8% state average",
                round(gm_wc, 2), "Margin falls %.0f%%" % ((gm - gm_wc) / gm * 100)))
    # ASM-003: payroll tax applies
    m = 1 + 0.12 + 0.018 + 0.0165 + 0.0485
    gm_pt = price - v(INPUTS, "wage_sacs_l2_casual") * m
    out.append(("ASM-003 wrong: the business crosses the payroll tax threshold",
                round(gm_pt, 2), "Margin falls %.0f%%" % ((gm - gm_pt) / gm * 100)))
    # Paid admin at the assumed ratio
    gm_ad = contribution_per_hour(True)
    out.append(("Administration is paid rather than done by the owner (ASM-008 at 0.25 hr)",
                round(gm_ad, 2), "Margin falls %.0f%%" % ((gm - gm_ad) / gm * 100)))
    # Paid admin at a worse ratio
    gm_ad2 = gm - loaded_wage("l3") * 0.40
    out.append(("Administration is paid and runs at 0.40 hr per billable hour",
                round(gm_ad2, 2), "Margin falls %.0f%%" % ((gm - gm_ad2) / gm * 100)))
    return out


# =========================================================================
# BLOCK 5 — HEADLINE RESULT SET (imported by the study and deck builders)
# =========================================================================
def results():
    oo_reg  = one_off_total(include_registration=True)
    oo_sc   = one_off_total(workers=0, include_registration=False)
    fx      = monthly_fixed()
    base_rows, base_worst, base_pos = cash_curve(RAMP_BASE, admin_paid=False)
    slow_rows, slow_worst, slow_pos = cash_curve(RAMP_SLOW, admin_paid=False)
    paid_rows, paid_worst, paid_pos = cash_curve(RAMP_BASE, admin_paid=True)
    return {
        "oncost_multiplier": round(oncost_multiplier(), 4),
        "loaded_wage_l2": round(loaded_wage("l2"), 2),
        "loaded_wage_l3": round(loaded_wage("l3"), 2),
        "gross_margin_hr": round(gross_margin_per_hour(), 2),
        "gross_margin_pct": round(gross_margin_per_hour() / v(INPUTS, "price_selfcare_wd") * 100, 1),
        "contribution_owner_admin": round(contribution_per_hour(False), 2),
        "contribution_paid_admin": round(contribution_per_hour(True), 2),
        "one_off_core": oo_reg,
        "one_off_sc": oo_sc,
        "monthly_fixed": fx,
        "breakeven_hrs_owner_admin": round(breakeven_hours(False), 1),
        "breakeven_hrs_paid_admin": round(breakeven_hours(True), 1),
        "breakeven_hrs_employed_manager": round(breakeven_hours_employed_manager(), 1),
        "manager_salary_pa": MANAGER_SALARY_PA,
        "working_capital_at_300h": round(working_capital(300), 2),
        "working_capital_at_300h_30day": round(300 * loaded_wage("l2"), 2),
        "runway_6mo_core_no_draw": runway_downside(6, 0.0),
        "runway_6mo_core_draw6k": runway_downside(6, 6000.0),
        "runway_6mo_sc_no_draw": runway_downside(6, 0.0, include_registration=False, workers=0),
        "base_rows": base_rows, "base_worst": base_worst, "base_month_positive": base_pos,
        "slow_rows": slow_rows, "slow_worst": slow_worst, "slow_month_positive": slow_pos,
        "paid_rows": paid_rows, "paid_worst": paid_worst, "paid_month_positive": paid_pos,
        "sc": sc_annual(), "sc_sens": sc_sensitivity(), "margin_sens": margin_sensitivity(),
        "hours_for_60k_draw": round((monthly_fixed()[1] + 5000) / contribution_per_hour(False), 0),
        "hours_for_60k_draw_paid_admin": round((monthly_fixed()[1] + 5000) / contribution_per_hour(True), 0),
    }


if __name__ == "__main__":
    import json
    r = results()
    for k, val in r.items():
        if k.endswith("_rows"):
            print(k, "-> %d months, final cumulative %s" % (len(val), val[-1]["cumulative_cash"]))
        else:
            print(k, "=", val)
