# -*- coding: utf-8 -*-
"""
build_business_plan.py — the Business Plan generator (PIL-BZ, Standard v4.0).

OWNER: 01_System/portable/templates/business_plan/build_business_plan.py
REVIEW: regenerate BUSINESS_PLAN_A3.html whenever business_plan.json changes.

Reads 01_System/business_plan.json and generates 01_System/BUSINESS_PLAN_A3.html —
a single-page A3 landscape document. Implements the twelve validation checks C-BP1..C-BP12.

PORTABLE: this file names no project. It travels to other projects unchanged.
No third-party dependency — standard library only, so it runs anywhere.

Usage:
    python build_business_plan.py --check    validate only, exit non-zero on any failure
    python build_business_plan.py            validate then generate the A3 page
    python build_business_plan.py --json PATH --out PATH   explicit paths
"""
import os, sys, json, html, datetime

GENERATOR_VERSION = "1.0.0"
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_JSON = os.path.abspath(os.path.join(HERE, "..", "..", "..", "business_plan.json"))
DEFAULT_OUT = os.path.abspath(os.path.join(HERE, "..", "..", "..", "BUSINESS_PLAN_A3.html"))
E = lambda s: html.escape(str(s))


# =========================================================================
# VALIDATION — C-BP1 .. C-BP12
# =========================================================================
def validate(d):
    """Returns a list of (check_id, ok, message). Every check states what it tests."""
    R = []

    def chk(cid, ok, msg):
        R.append((cid, bool(ok), msg))

    def sect(name):
        return d.get(name) if isinstance(d, dict) else None

    m = sect("meta") or {}
    need = ["project_name", "owner", "date", "status", "class", "version", "standard"]
    missing = [k for k in need if not str(m.get(k, "")).strip()]
    chk("C-BP1", not missing,
        "project metadata complete" if not missing else "meta missing: " + ", ".join(missing))

    p = sect("purpose") or {}
    need = ["statement", "problem", "current_state", "desired_state", "gap"]
    missing = [k for k in need if len(str(p.get(k, "")).strip()) < 20]
    chk("C-BP2", not missing,
        "purpose, problem, current state, desired state and gap all stated"
        if not missing else "purpose thin or missing: " + ", ".join(missing))

    tm = sect("target_market") or []
    bad = [i for i, s in enumerate(tm) if not all(str(s.get(k, "")).strip()
           for k in ("segment", "size", "size_source", "need", "why_us"))]
    chk("C-BP3", len(tm) >= 2 and not bad,
        "%d market segments, each with a size, a source and a need" % len(tm)
        if len(tm) >= 2 and not bad else
        "need >=2 complete segments; have %d, %d incomplete" % (len(tm), len(bad)))

    vp = sect("value_proposition") or {}
    bens = vp.get("benefits") or []
    unmeasured = [b.get("benefit", "?") for b in bens
                  if not str(b.get("metric", "")).strip() or not str(b.get("target", "")).strip()]
    chk("C-BP4", len(bens) >= 3 and not unmeasured and vp.get("beneficiaries")
        and str(vp.get("realisation_timeline", "")).strip(),
        "%d benefits, every one with a measurable metric and a target" % len(bens)
        if len(bens) >= 3 and not unmeasured else
        "benefits without a measurable metric: " + (", ".join(unmeasured) or "need >=3 benefits"))

    c = sect("cba") or {}
    need = ["lifecycle_years", "development_costs", "running_costs_total", "maintenance_costs",
            "opportunity_costs", "quantitative_benefits", "qualitative_benefits", "roi_pct",
            "break_even"]
    missing = [k for k in need if c.get(k) in (None, "", [])]
    chk("C-BP5", not missing,
        "lifecycle cost-benefit complete: all eight cost and benefit fields plus ROI and break-even"
        if not missing else "cost-benefit missing: " + ", ".join(missing))

    rc = sect("running_costs") or []
    bad = [r.get("category", "?") for r in rc if not r.get("annual")]
    chk("C-BP6", len(rc) >= 5 and not bad,
        "%d running-cost categories, each projected across the lifecycle" % len(rc)
        if len(rc) >= 5 and not bad else
        "need >=5 categories each with annual projections; have %d" % len(rc))

    ms = sect("marketing_strategy") or {}
    need = ["target_audience", "go_to_market", "pricing", "positioning"]
    missing = [k for k in need if len(str(ms.get(k, "")).strip()) < 20]
    if len(ms.get("channels") or []) < 2:
        missing.append("channels")
    chk("C-BP7", not missing,
        "marketing strategy complete: audience, go-to-market, pricing, positioning, channels"
        if not missing else "marketing strategy missing: " + ", ".join(missing))

    rm = sect("revenue_model") or []
    bad = [r.get("use_case", "?") for r in rm if not r.get("year_projection") or not r.get("assumptions")]
    chk("C-BP8", len(rm) >= 1 and not bad,
        "%d revenue lines, each with a projection and its assumptions" % len(rm)
        if len(rm) >= 1 and not bad else
        "revenue lines missing a projection or assumptions: " + (", ".join(bad) or "none present"))

    ac = sect("areas_of_concern") or []
    bad = [a.get("concern", "?")[:32] for a in ac
           if not all(str(a.get(k, "")).strip() for k in ("likelihood", "impact", "mitigation", "early_warning"))]
    chk("C-BP9", len(ac) >= 3 and not bad,
        "%d areas of concern, each with a mitigation and an early warning indicator" % len(ac)
        if len(ac) >= 3 and not bad else
        "need >=3 complete concerns; have %d, %d incomplete" % (len(ac), len(bad)))

    ea = sect("efficiency_audit") or {}
    alts = ea.get("alternatives") or []
    chk("C-BP10", len(alts) >= 2 and len(str(ea.get("build_vs_buy", ""))) >= 20
        and len(str(ea.get("recommendation", ""))) >= 20 and str(ea.get("date", "")).strip(),
        "efficiency audit records %d alternatives, a build-versus-buy decision and a dated recommendation" % len(alts)
        if len(alts) >= 2 else "efficiency audit needs >=2 alternatives, a build-vs-buy decision and a date")

    vv = sect("value_validation") or {}
    ok_verdicts = {"PROCEED", "PROCEED WITH CONDITIONS", "DO NOT PROCEED", "DECISION OWED BY OWNER"}
    chk("C-BP11", vv.get("verdict") in ok_verdicts and len(str(vv.get("justification", ""))) >= 40,
        "value validated: verdict '%s' with a justification" % vv.get("verdict")
        if vv.get("verdict") in ok_verdicts else "value validation needs a recognised verdict and a justification")

    f = sect("footer") or {}
    need = ["generated_from", "generator_version", "produced_by", "verified_by"]
    missing = [k for k in need if not str(f.get(k, "")).strip()]
    chk("C-BP12", not missing,
        "provenance recorded: generated from, generator version, produced by, verified by"
        if not missing else "footer missing: " + ", ".join(missing))
    return R


# =========================================================================
# GENERATION
# =========================================================================
CSS = """
/* A3 landscape, ONE SHEET. Every size below was set by measuring the rendered page, not by
   taste: the sheet must come in under 297mm of content height with the fullest plan the
   schema allows, and the check that enforces that is C33. */
/* margin 0 on purpose: the printed page box must equal the sheet, or the sheet
   overflows the content box and Chromium starts a second page. DEF-025. */
@page{size:A3 landscape;margin:0}
:root{--ink:#0E1C26;--navy:#13293B;--blue:#1F5C8B;--teal:#2E8B8B;--amber:#C77B1F;
--red:#B03A2E;--green:#2E7D4F;--grey:#5A6B77;--line:#DCE5EA;--paper:#F7FAFB}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);
font:8.9px/1.24 -apple-system,BlinkMacSystemFont,"Segoe UI",Calibri,Arial,sans-serif}
.sheet{width:420mm;min-height:297mm;margin:0 auto;background:#fff;padding:5mm 6mm 3mm;
display:flex;flex-direction:column}
@media(max-width:900px){.sheet{width:100%;height:auto;padding:14px}}
header{background:var(--navy);color:#fff;padding:7px 12px;border-radius:6px;
display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
header h1{margin:0 0 2px;font-size:15px;letter-spacing:-.2px}
header .sub{color:#A9BECC;font-size:8.6px}
header .badge{background:var(--teal);color:#08222a;font-weight:700;font-size:8.6px;
padding:3px 8px;border-radius:999px;white-space:nowrap}
/* THREE EXPLICIT COLUMNS, filled by the generator. DEF-036.
   A CSS row grid aligns row heights and wastes the space under the shorter card in every row.
   CSS multi-column packs beautifully on screen and FRAGMENTS ACROSS PAGES in print: Chromium
   filled columns one and two, put column three on a second sheet, and when the container was
   given a height it overflowed into a fourth column off the right edge. Neither failure is
   visible from the DOM. Explicit columns cannot fragment, because there is nothing for the
   engine to decide. */
.grid{display:flex;gap:7px;margin-top:5px;align-items:flex-start}
.col{flex:1 1 0;min-width:0}
@media(max-width:900px){.grid{display:block}}
.card{border:1px solid var(--line);border-radius:6px;padding:5px 8px;background:#fff;
break-inside:avoid;-webkit-column-break-inside:avoid;page-break-inside:avoid;
display:inline-block;width:100%;margin:0 0 5px}
.card h2{margin:0 0 4px;font-size:9.4px;color:var(--navy);text-transform:uppercase;
letter-spacing:.5px;border-bottom:2px solid var(--line);padding-bottom:3px}
p{margin:0 0 3px}
.small{font-size:8.6px;color:var(--grey)}
table{width:100%;border-collapse:collapse;font-size:8px;margin:2px 0}
th{background:var(--navy);color:#fff;text-align:left;padding:3px 4px;font-weight:600}
td{padding:2.5px 4px;border-top:1px solid var(--line);vertical-align:top}
tr:nth-child(even) td{background:#F2F7F9}
.metrics{display:flex;gap:5px;flex-wrap:wrap;margin:4px 0}
.metric{flex:1 1 74px;border:1px solid var(--line);border-radius:5px;padding:5px 6px;background:#F7FAFB}
.metric .v{font-size:13px;font-weight:700;color:var(--navy);line-height:1.1}
.metric .l{font-size:7.6px;color:var(--grey);margin-top:2px}
.verdict{border-radius:6px;padding:7px 10px;margin-top:2px;border:2px solid}
.v-proceed{background:#E7F3EC;border-color:var(--green)}
.v-cond{background:#FDF0E3;border-color:var(--amber)}
.v-stop{background:#FBE9E7;border-color:var(--red)}
.v-owner{background:#EAF1F7;border-color:var(--blue)}
.verdict .t{font-weight:700;font-size:11px;margin-bottom:3px}
footer{margin-top:6px;padding-top:4px;border-top:1px solid var(--line);
font-size:7.8px;color:var(--grey);display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap}
.pill{display:inline-block;padding:1px 5px;border-radius:999px;font-size:7.6px;font-weight:700}
.p-hi{background:#FBE9E7;color:var(--red)}.p-md{background:#FDF0E3;color:var(--amber)}
.p-lo{background:#EDF2F5;color:var(--grey)}
/* PRINT: the sheet must be a fraction UNDER the page box. At exactly 297mm Chromium rounds up and
   starts a second page carrying nothing, which is how DEF-021 came back as DEF-025 — the file
   looked right on screen and printed as two sheets. C34 now counts pages in a real PDF. */
/* PRINT. DEF-025, then DEF-036. The first attempt to make this fit used overflow:hidden, which
   did not make it fit — it CLIPPED it, and the printed PDF silently lost section 8's table, all of
   section 9, the verdict block and the footer while the page count read 1. Independent
   verification V7 rasterised the PDF and read what was missing. Nothing here clips anything now:
   the content fits because it is small enough, and C34 proves it by extracting the text of the
   printed PDF and looking for a sentinel from the LAST element on the sheet. */
@media print{
  html,body{width:420mm;margin:0;padding:0;background:#fff}
  .sheet{width:420mm;min-height:0;height:auto;break-after:avoid;page-break-after:avoid}
  .grid{display:flex}
}
"""


def money(x):
    try:
        return "${:,.0f}".format(float(x))
    except (TypeError, ValueError):
        return E(x)


def tbl(headers, rows):
    o = ["<table><tr>"] + ["<th>%s</th>" % E(h) for h in headers] + ["</tr>"]
    for r in rows:
        o.append("<tr>" + "".join("<td>%s</td>" % c for c in r) + "</tr>")
    o.append("</table>")
    return "".join(o)


def pill(level):
    s = str(level).lower()
    k = "p-hi" if s in ("likely", "almost certain", "major", "severe") else \
        ("p-md" if s in ("possible", "moderate") else "p-lo")
    return '<span class="pill %s">%s</span>' % (k, E(level))


def render(d, checks):
    m, p = d["meta"], d["purpose"]
    vp, c, ms, vv, f = d["value_proposition"], d["cba"], d["marketing_strategy"], d["value_validation"], d["footer"]
    npass = sum(1 for _, ok, _ in checks if ok)

    o = ["<!DOCTYPE html>",
         "<!-- GENERATED FILE - do not hand-edit.\n"
         "     OWNER: 01_System/portable/templates/business_plan/build_business_plan.py\n"
         "     REVIEW: regenerate whenever 01_System/business_plan.json changes.\n"
         "     Hand-editing a generated artefact is a defect (GOV-D4.11). -->",
         "<html lang='en-AU'><head><meta charset='utf-8'>",
         "<meta name='viewport' content='width=device-width,initial-scale=1'>",
         "<title>%s — Business Plan</title><style>%s</style></head><body><div class='sheet'>" %
         (E(m["project_name"]), CSS)]

    o.append("<header><div><h1>%s</h1><div class='sub'>Business Plan &nbsp;·&nbsp; owner %s "
             "&nbsp;·&nbsp; class %s &nbsp;·&nbsp; version %s &nbsp;·&nbsp; %s &nbsp;·&nbsp; %s</div></div>"
             "<div class='badge'>%s &nbsp;·&nbsp; %d/%d checks</div></header>"
             % (E(m["project_name"]), E(m["owner"]), E(m["class"]), E(m["version"]),
                E(m["date"]), E(m["standard"]), E(m["status"]), npass, len(checks)))

    o.append("<div class='grid'><div class='col'>")

    # 1 purpose
    o.append("<div class='card'><h2>1 · Purpose and problem</h2>")
    o.append("<p>%s</p>" % E(p["statement"]))
    o.append("<p class='small'><b>Current state.</b> %s</p>" % E(p["current_state"]))
    o.append("<p class='small'><b>Desired state.</b> %s</p>" % E(p["desired_state"]))
    o.append("<p class='small'><b>Gap.</b> %s</p></div>" % E(p["gap"]))

    # 2 market
    o.append("<div class='card'><h2>2 · Target market</h2>")
    o.append(tbl(["Segment", "Size", "Need", "Source"],
                 [[E(s["segment"]), E(s["size"]), E(s["need"]), E(s["size_source"])] for s in d["target_market"]]))
    o.append("</div>")

    # 3 value proposition
    o.append("<div class='card'><h2>3 · Value proposition</h2>")
    o.append("<p>%s</p>" % E(vp["statement"]))
    o.append(tbl(["Benefit", "Measured by", "Baseline", "Target"],
                 [[E(b["benefit"]), E(b["metric"]), E(b["baseline"]), E(b["target"])] for b in vp["benefits"]]))
    o.append("<p class='small'><b>Who benefits.</b> %s</p>" % E("; ".join(vp["beneficiaries"])))
    o.append("<p class='small'><b>Realised.</b> %s</p></div>" % E(vp["realisation_timeline"]))

    # 4 CBA
    o.append("<div class='card'><h2>4 · Lifecycle cost-benefit</h2>")
    o.append("<div class='metrics'>")
    for v, l in [(money(c["development_costs"]), "Set-up cost"),
                 (money(c["running_costs_total"]), "Running, %g yr" % c["lifecycle_years"]),
                 (money(c["quantitative_benefits"]), "Benefit, %g yr" % c["lifecycle_years"]),
                 ("%.0f%%" % c["roi_pct"], "Return on investment")]:
        o.append("<div class='metric'><div class='v'>%s</div><div class='l'>%s</div></div>" % (v, l))
    o.append("</div>")
    o.append(tbl(["Cost or benefit", "Lifecycle total"], [
        ["Development and set-up", money(c["development_costs"])],
        ["Running and operational", money(c["running_costs_total"])],
        ["Maintenance and compliance", money(c["maintenance_costs"])],
        ["Opportunity cost", money(c["opportunity_costs"])],
        ["<b>Quantified benefit</b>", "<b>%s</b>" % money(c["quantitative_benefits"])],
    ]))
    o.append("<p class='small'><b>Break-even.</b> %s</p>" % E(c["break_even"]))
    if c.get("basis"):
        o.append("<p class='small'>%s</p>" % E(c["basis"]))
    o.append("<p class='small'><b>Benefits that resist a number.</b> %s</p></div>"
             % E("; ".join(c["qualitative_benefits"])))

    # 5 running costs
    o.append("</div><div class='col'>")
    o.append("<div class='card'><h2>5 · Running costs by category</h2>")
    yrs = max(len(r["annual"]) for r in d["running_costs"])
    head = ["Category"] + ["Year %d" % (i + 1) for i in range(yrs)] + ["Source"]
    rows = []
    for r in d["running_costs"]:
        cells = [E(r["category"])] + [money(x) for x in r["annual"]]
        cells += [""] * (yrs - len(r["annual"])) + [E(r["source"])]
        rows.append(cells)
    tot = ["<b>Total</b>"] + ["<b>%s</b>" % money(sum(r["annual"][i] if i < len(r["annual"]) else 0
                                                      for r in d["running_costs"])) for i in range(yrs)] + [""]
    rows.append(tot)
    o.append(tbl(head, rows) + "</div>")

    # 6 marketing
    o.append("<div class='card'><h2>6 · Marketing and selling</h2>")
    for lab, key in [("Who we sell to", "target_audience"), ("How we reach them", "go_to_market"),
                     ("Pricing", "pricing"), ("Positioning", "positioning")]:
        o.append("<p class='small'><b>%s.</b> %s</p>" % (lab, E(ms[key])))
    o.append("<p class='small'><b>Channels.</b> %s</p></div>" % E("; ".join(ms["channels"])))

    # 7 revenue
    o.append("<div class='card'><h2>7 · Revenue model</h2>")
    yrs = max(len(r["year_projection"]) for r in d["revenue_model"])
    head = ["Revenue line", "Unit", "Per unit"] + ["Year %d" % (i + 1) for i in range(yrs)]
    rows = []
    for r in d["revenue_model"]:
        cells = [E(r["use_case"]), E(r["unit"]), money(r["unit_revenue"])]
        cells += [money(x) for x in r["year_projection"]]
        cells += [""] * (yrs - len(r["year_projection"]))
        rows.append(cells)
    rows.append(["<b>Total</b>", "", ""] +
                ["<b>%s</b>" % money(sum(r["year_projection"][i] if i < len(r["year_projection"]) else 0
                                         for r in d["revenue_model"])) for i in range(yrs)])
    o.append(tbl(head, rows))
    asm = []
    for r in d["revenue_model"]:
        asm += r["assumptions"]
    o.append("<p class='small'><b>Resting on.</b> %s</p></div>" % E(" · ".join(dict.fromkeys(asm))))

    # 8 concerns
    o.append("</div><div class='col'>")
    o.append("<div class='card'><h2>8 · Areas of concern</h2>")
    o.append(tbl(["Concern", "Likelihood", "Impact", "Mitigation", "Early warning"],
                 [[E(a["concern"]), pill(a["likelihood"]), pill(a["impact"]),
                   E(a["mitigation"]), E(a["early_warning"])] for a in d["areas_of_concern"]]))
    o.append("</div>")

    # 9 efficiency audit
    ea = d["efficiency_audit"]
    o.append("<div class='card'><h2>9 · Efficiency audit (SP07)</h2>")
    o.append(tbl(["Option considered", "Cost", "Time", "Risk", "Verdict"],
                 [[E(a["option"]), E(a["cost"]), E(a["time"]), E(a["risk"]), E(a["verdict"])]
                  for a in ea["alternatives"]]))
    o.append("<p class='small'><b>Build or buy.</b> %s</p>" % E(ea["build_vs_buy"]))
    o.append("<p class='small'><b>Recommendation (%s).</b> %s</p></div>" % (E(ea["date"]), E(ea["recommendation"])))

    o.append("</div></div>")  # end last column, end grid

    # verdict
    cls = {"PROCEED": "v-proceed", "PROCEED WITH CONDITIONS": "v-cond",
           "DO NOT PROCEED": "v-stop", "DECISION OWED BY OWNER": "v-owner"}[vv["verdict"]]
    o.append("<div class='verdict %s'><div class='t'>Value validation — %s</div><p>%s</p>"
             % (cls, E(vv["verdict"]), E(vv["justification"])))
    if vv.get("conditions"):
        o.append("<p class='small'><b>Conditions.</b> %s</p>" % E(" · ".join(vv["conditions"])))
    if vv.get("what_would_change_this"):
        o.append("<p class='small'><b>What would change this answer.</b> %s</p>"
                 % E(" · ".join(vv["what_would_change_this"])))
    o.append("</div>")

    fails = [cid for cid, ok, _ in checks if not ok]
    o.append("<footer><div>GENERATED FILE — do not hand-edit. OWNER: %s &nbsp;·&nbsp; "
             "REVIEW: regenerate whenever business_plan.json changes.</div>"
             "<div>Generator v%s &nbsp;·&nbsp; produced by %s &nbsp;·&nbsp; verified by %s "
             "&nbsp;·&nbsp; checks %d/%d%s</div></footer>"
             % (E(f["generated_from"]), E(f["generator_version"]), E(f["produced_by"]),
                E(f["verified_by"]), npass, len(checks),
                (" &nbsp;·&nbsp; FAILING: " + ", ".join(fails)) if fails else ""))
    o.append("</div></body></html>")
    return "".join(o)


def main():
    args = sys.argv[1:]
    jpath = DEFAULT_JSON
    opath = DEFAULT_OUT
    if "--json" in args:
        jpath = os.path.abspath(args[args.index("--json") + 1])
    if "--out" in args:
        opath = os.path.abspath(args[args.index("--out") + 1])
    if not os.path.exists(jpath):
        print("MISSING: %s — REQ-BUS-001 makes the business plan twin mandatory at intake." % jpath)
        sys.exit(2)
    d = json.load(open(jpath, encoding="utf-8"))
    checks = validate(d)
    width = max(len(msg) for _, _, msg in checks)
    print("BUSINESS PLAN VALIDATION — %s" % jpath)
    for cid, ok, msg in checks:
        print("  %-8s %-4s %s" % (cid, "PASS" if ok else "FAIL", msg))
    npass = sum(1 for _, ok, _ in checks if ok)
    print("  %d of %d checks passed" % (npass, len(checks)))
    if "--check" in args:
        sys.exit(0 if npass == len(checks) else 1)
    if npass != len(checks):
        print("REFUSING TO GENERATE — a business plan that fails its own checks is not a plan.")
        sys.exit(1)
    open(opath, "w", encoding="utf-8", newline="\n").write(render(d, checks))
    print("wrote %s (%d KB)" % (opath, os.path.getsize(opath) // 1024))


if __name__ == "__main__":
    main()
