#!/usr/bin/env python3
"""
Build the booklet content JSON (for the vendored generator) from engine results.

Usage:
    python3 build_booklet_content.py results.json meta.json content.json

Produces a compact business case (target: cover + 3 content pages):

  1. The opportunity            - the current process and the value at stake, in plain terms
  2. The proposed approach      - what changes, in lay terms (NO coefficients)
  3. Estimated impact           - headline value-per-dollar callout, the Conservative /
                                  Expected / Strong scenario table, and a lay read-out
  4. Methodology and assumptions- ONE PAGE: short method note + compact assumptions table
                                  + per-segment validity table (coefficients allowed here)
  5. About Deeper Signals       - fixed call to action (chosen variant), same every time
  Disclaimer                    - the standard disclaimer, verbatim, always included

Rules enforced here:
  - No validity coefficient (r, R, rho) appears in the body (sections 1 to 3).
    Coefficients appear only in the methodology section.
  - Point estimates are never shown as the impact; always the three scenarios.
  - All money is USD ($). Figures are undiscounted (no NPV), stated in the methodology.
  - The headline is value per dollar invested (benefit-cost ratio), with the horizon
    stated precisely (value over the tenure of those selected, vs one year's cost).
  - The disclaimer is verbatim and always present.
"""

import json
import sys


# ---------------------------------------------------------------------------
# Call to action. This block is FIXED: the same wording appears in every
# document. Three approved variants are defined; CTA_DEFAULT selects which one
# is used. To switch the house version, change CTA_DEFAULT to "A", "B", or "C".
# Drawn from the Deeper Signals 2026 brochure voice. No em or en dashes.
# ---------------------------------------------------------------------------

CTA_CONTACT = "Learn more at deepersignals.com, or contact hello@deepersignals.com or +1 (732) 598 2272."

CTA_VARIANTS = {
    # A. Concise, mission-led.
    "A": (
        "Deeper Signals helps organizations find and grow their top talent faster. Our "
        "scientifically validated, AI powered assessments measure the soft skills, personality, "
        "values, and reasoning that predict performance, then turn those insights into action "
        "across hiring, development, team effectiveness, and succession planning. If better "
        "assessment could strengthen the decisions modeled in this document, we would welcome a "
        "conversation. " + CTA_CONTACT
    ),
    # B. Results-led.
    "B": (
        "Soft skills, hard results. Deeper Signals delivers an end to end soft skills "
        "intelligence platform that helps organizations make smarter, fairer, and more strategic "
        "talent decisions, from the first hire to the next leader. Our assessments are fast (5 to "
        "7 minutes), scientifically validated, and built to be fair, so you can identify the right "
        "talent and develop it at scale. To explore what validated assessment could do for the "
        "numbers in this document, we would be glad to help. " + CTA_CONTACT
    ),
    # C. Problem and solution.
    "C": (
        "In an age of AI and rapid change, soft skills increasingly define business success, yet "
        "most organizations still struggle to measure and develop them. Deeper Signals changes "
        "that. Founded by Professor Tomas Chamorro-Premuzic in 2020, we provide quick, "
        "scientifically validated assessments and AI powered insights that help organizations "
        "identify, hire, and grow top talent across the employee lifecycle. If the value modeled "
        "here is something you want to realize, we can help you take the next step. " + CTA_CONTACT
    ),
}

CTA_DEFAULT = "A"

CTA_HEADING = "About Deeper Signals"


# The standard disclaimer. USED VERBATIM. Do not summarize, shorten, or paraphrase.
STANDARD_DISCLAIMER = (
    "These figures are estimates, not guarantees. They are based on the workforce data "
    "provided, published meta-analytic research, and conservative modeling assumptions, and "
    "they are expressed in US dollars. Any selection process should be monitored for adverse "
    "impact, validated locally after implementation, and operated in alignment with local laws "
    "and guidelines. Deeper Signals makes no claim or guarantee of specific outcomes from the "
    "use of any assessment program."
)


def fmt_money(x, cur="$"):
    if x is None:
        return "n/a"
    ax = abs(x)
    if ax >= 1_000_000:
        return f"{cur}{x/1_000_000:.2f}m"
    if ax >= 1_000:
        return f"{cur}{x/1_000:.0f}k"
    return f"{cur}{x:,.0f}"


def fmt_full(x, cur="$"):
    if x is None:
        return "n/a"
    return f"{cur}{x:,.0f}"


def fmt_x(x):
    return f"{x:.1f}x" if x is not None else "n/a"


# Plain-language labels for the body. The methodology may use technical method names; the body
# never should. Each canonical method maps to a phrase a layperson understands.
PLAIN_LABELS = {
    "Structured interviews": "a structured interview",
    "Unstructured interviews": "an unstructured interview",
    "Cognitive ability tests": "a cognitive ability test",
    "Job knowledge tests": "a job knowledge test",
    "Work sample tests": "a work sample exercise",
    "Integrity tests": "an integrity assessment",
    "Assessment centers": "an assessment centre",
    "SJT (knowledge)": "a situational judgement test",
    "SJT (behavioral tendency)": "a situational judgement test",
    "Conscientiousness (contextualized)": "a work-based personality assessment",
    "Conscientiousness (overall)": "a personality assessment",
    "Emotional stability (contextualized)": "a work-based personality assessment",
    "Emotional stability (overall)": "a personality assessment",
    "Extraversion (contextualized)": "a work-based personality assessment",
    "Extraversion (overall)": "a personality assessment",
    "Agreeableness (contextualized)": "a work-based personality assessment",
    "Agreeableness (overall)": "a personality assessment",
    "Openness (contextualized)": "a work-based personality assessment",
    "Openness (overall)": "a personality assessment",
    "Personality-based EI": "an emotional intelligence assessment",
    "Ability-based EI": "an emotional intelligence assessment",
    "Empirically keyed biodata": "a background and biodata questionnaire",
    "Rationally keyed biodata": "a background and biodata questionnaire",
    "Interests": "a values and interests assessment",
    "Job experience (years)": "years of relevant experience",
}


def plain_label(method):
    return PLAIN_LABELS.get(method, method.lower())


def join_methods(methods):
    """Join methods into a plain-language phrase, de-duplicating repeated plain labels."""
    labels = []
    for m in (methods or []):
        lbl = plain_label(m)
        if lbl not in labels:
            labels.append(lbl)
    if not labels:
        return "a validated assessment approach"
    if len(labels) == 1:
        return labels[0]
    return ", ".join(labels[:-1]) + ", and " + labels[-1]


def sr_phrase(sr):
    """Render a selection ratio as a plain '1 in N' phrase where it reads cleanly."""
    if not sr or sr <= 0:
        return "n/a"
    inv = 1.0 / sr
    near = round(inv)
    if abs(inv - near) < 0.05 and near >= 2:
        return f"1 in {near} ({sr:.2f})"
    return f"{sr:.2f}"


def build(results, meta):
    cur = "$"
    segments = results.get("segments", [])
    scenarios = results.get("scenarios", {})
    budget = results.get("budget")
    any_ret = results.get("any_retention", False)
    decision_label = results.get("decision_label", "talent decisions")
    client = meta.get("client_name") or results.get("client_name") or "your organization"

    low = scenarios.get("low", {})
    mid = scenarios.get("mid", {})
    high = scenarios.get("high", {})
    mid_bcr = mid.get("benefit_cost_ratio")

    multi = len(segments) > 1
    body = []

    # ===================== 1. THE OPPORTUNITY =====================
    body.append({"type": "h1", "text": "The opportunity"})

    # Volume + current process summary, in lay terms.
    total_n = sum(int(s["n"]) for s in segments)
    org_size = results.get("org_size")
    org_clause = f" across a workforce of about {int(org_size):,}" if org_size else ""
    if multi:
        vol = (f"Across the populations in scope, {client} makes about {total_n:,} "
               f"{decision_label} a year{org_clause}.")
    else:
        s0 = segments[0]
        vol = (f"{client} makes roughly {int(s0['n']):,} {decision_label} a year{org_clause} "
               f"for roles paying around {fmt_full(s0['salary'], cur)} on average.")

    body.append({
        "type": "paragraph",
        "text": (
            f"{vol} Each of these is a high value decision, yet they are often made with limited, "
            f"inconsistent information. When the process that informs them is only weakly connected "
            f"to who actually performs, the difference between a strong decision and an average one "
            f"is left on the table, decision after decision, and it compounds over the time each "
            f"person stays."
        ),
    })
    body.append({
        "type": "paragraph",
        "text": (
            "This is the largest and least visible cost in talent decisions. It does not appear on "
            "any invoice, but it shows up in the performance, retention, and momentum of the teams "
            "these decisions build."
        ),
    })

    # ===================== 2. THE PROPOSED APPROACH =====================
    body.append({"type": "h1", "text": "The proposed approach"})

    # Methods label drawn from the first (or only) segment's proposed methods, in lay terms.
    methods_label = meta.get("methods_label") or join_methods(segments[0].get("proposed_methods"))
    body.append({
        "type": "paragraph",
        "text": (
            f"The proposal is to inform each decision with {methods_label}: a consistent, "
            f"evidence-based way to understand how someone is likely to perform, applied the same "
            f"way to everyone. Rather than relying on unstructured judgment, each person is "
            f"considered against the same validated criteria, which lifts both the quality and the "
            f"fairness of the decision."
        ),
    })

    # Lay benefits (NO coefficients). Use the first segment's lay translations.
    lay = segments[0].get("lay", {})
    top_mult = lay.get("top_performer_multiplier")
    items = []
    if top_mult and top_mult > 1.0:
        items.append({
            "label": "Find more of your top performers",
            "text": (f"the approach is expected to surface strong performers at roughly "
                     f"{top_mult:.1f} times the rate of the current process."),
        })
    items.append({
        "label": "Make fewer costly mistakes",
        "text": ("a stronger signal at the point of decision means fewer regretted hires and "
                 "promotions, which is where most of the avoidable cost sits."),
    })
    if any_ret:
        items.append({
            "label": "Keep more of the people worth keeping",
            "text": ("better fit and motivation insight helps retain a share of the people the "
                     "organization would otherwise lose to avoidable, voluntary turnover."),
        })
    if items:
        body.append({"type": "emphasis_list", "items": items})

    # ===================== 3. ESTIMATED IMPACT =====================
    body.append({"type": "h1", "text": "Estimated impact"})

    # Headline callout: value per dollar, horizon stated precisely.
    if mid_bcr is not None:
        callout_runs = [
            {"text": f"For every {cur}1 invested in better {decision_label}, the model estimates ",
             "font": "Manrope", "size": 21, "bold": True},
            {"text": f"{cur}{mid_bcr:.2f} in value", "font": "Manrope", "size": 21, "bold": True,
             "color": "FF005C"},
            {"text": (f" over the time those selected stay with the organization "
                      f"(Expected scenario). "),
             "font": "Manrope", "size": 21, "bold": True},
            {"text": (f"Across scenarios the estimate ranges from {cur}{low.get('benefit_cost_ratio'):.2f} "
                      f"to {cur}{high.get('benefit_cost_ratio'):.2f} per {cur}1."),
             "font": "Manrope Medium", "size": 21, "bold": False},
        ]
        body.append({"type": "callout1", "runs": callout_runs})

    # Scenario table.
    columns = ["", "Conservative", "Expected", "Strong"]
    rows = []
    rows.append([
        "Stronger workforce performance",
        fmt_money(low.get("performance_value"), cur),
        fmt_money(mid.get("performance_value"), cur),
        fmt_money(high.get("performance_value"), cur),
    ])
    emphasis_rows = []
    if any_ret:
        rows.append([
            "Lower avoidable turnover",
            fmt_money(low.get("retention_value"), cur),
            fmt_money(mid.get("retention_value"), cur),
            fmt_money(high.get("retention_value"), cur),
        ])
    # Total row (emphasis)
    rows.append([
        "Total estimated value",
        fmt_money(low.get("value_total"), cur),
        fmt_money(mid.get("value_total"), cur),
        fmt_money(high.get("value_total"), cur),
    ])
    emphasis_rows.append(len(rows) - 1)
    # Value per dollar row (emphasis), only if budget present
    if budget:
        rows.append([
            f"Value per {cur}1 invested",
            fmt_x(low.get("benefit_cost_ratio")),
            fmt_x(mid.get("benefit_cost_ratio")),
            fmt_x(high.get("benefit_cost_ratio")),
        ])
        emphasis_rows.append(len(rows) - 1)

    body.append({
        "type": "table",
        "columns": columns,
        "col_widths": [0.40, 0.20, 0.20, 0.20],
        "emphasis_rows": emphasis_rows,
        "rows": rows,
    })

    # Lay read-out sentence.
    readout = (
        f"In plain terms, the performance value is the additional output a stronger process is "
        f"expected to produce from the people selected, valued over their time in role."
    )
    if any_ret:
        readout += (
            " Lower turnover is shown as a separate, secondary figure: the replacement cost avoided "
            "when better selection keeps more of the right people. It is additive to the performance "
            "value, not a different view of it."
        )
    readout += (
        ""
    )
    body.append({"type": "paragraph", "text": readout})

    # Per-segment totals table, only when there is more than one segment.
    if multi:
        body.append({"type": "h3", "text": "By decision population"})
        seg_cols = ["Population", "Conservative", "Expected", "Strong"]
        seg_rows = []
        for s in segments:
            seg_rows.append([
                s["name"],
                fmt_money(s["total"]["low"], cur),
                fmt_money(s["total"]["mid"], cur),
                fmt_money(s["total"]["high"], cur),
            ])
        body.append({
            "type": "table",
            "columns": seg_cols,
            "col_widths": [0.40, 0.20, 0.20, 0.20],
            "rows": seg_rows,
        })

    # ===================== 4. METHODOLOGY AND ASSUMPTIONS (ONE PAGE) =====================
    body.append({"type": "h1", "text": "Methodology and assumptions"})

    method_note = (
        "The estimates use a standard personnel-selection utility model. Value flows from the gain "
        "in predictive validity, the degree to which a process actually predicts performance, drawn "
        "from the current meta-analytic reference (Sackett et al. 2022). That gain is converted to "
        "dollars using hiring volume, the value of a standard deviation of performance (set "
        "conservatively at 40% of salary), the selectivity of the process, and expected tenure. "
        "Figures are undiscounted (no net present value adjustment is applied) and are framed as the "
        "value from one year of decisions over the tenure of those selected, set against the "
        "annual amount invested in the assessments. Retention value, where shown, is the separate "
        "cost avoided when better selection reduces voluntary departures."
    )
    body.append({"type": "paragraph", "text": method_note})

    def retention_rows():
        out = []
        out.append(["Modeled reduction in voluntary turnover",
                    "5%, 10%, and 15% (Conservative, Expected, Strong)"])
        ret_seg = next((s for s in segments if s.get("retention_included")), None)
        if ret_seg:
            if ret_seg.get("cost_is_default"):
                out.append(["Cost per voluntary departure", "50% of salary (conservative default)"])
            else:
                out.append(["Cost per voluntary departure",
                            fmt_full(ret_seg.get("cost_per_departure"), cur) + " (provided)"])
        return out

    if multi:
        # Global assumptions table + a per-segment validity table.
        # Selection ratio and tenure vary by population, so they live per segment, not here.
        g_rows = [
            ["Value of one standard deviation of performance", "40% of salary (held flat across scenarios)"],
            ["Scenario range (performance)", "proposed validity scaled to 85%, 100%, and 110%"],
            ["Figures discounted (NPV)", "No"],
            ["Currency", "US dollars"],
        ]
        if any_ret:
            g_rows.extend(retention_rows())
        if budget:
            g_rows.append(["Annual cost of the assessments", fmt_full(budget, cur)])
        body.append({
            "type": "table",
            "columns": ["Assumption", "Value used"],
            "col_widths": [0.55, 0.45],
            "rows": g_rows,
        })
        v_rows = []
        for s in segments:
            v_rows.append([
                s["name"], f"{int(s['n']):,}", f"{s.get('selection_ratio'):.2f}",
                f"{s.get('tenure'):.0f}y", f"{s['R_current']:.2f}", f"{s['R_proposed']:.2f}",
                f"+{s['delta_r']:.2f}",
            ])
        body.append({
            "type": "table",
            "columns": ["Population", "Decisions/yr", "Sel. ratio", "Tenure", "Current (r)", "Proposed (R)", "Added"],
            "col_widths": [0.26, 0.15, 0.13, 0.11, 0.12, 0.13, 0.10],
            "rows": v_rows,
        })
    else:
        # One merged inputs-and-assumptions table for a single decision population.
        s = segments[0]
        m_rows = [
            ["Decisions modeled per year", f"{int(s['n']):,}"],
            ["Average salary", fmt_full(s["salary"], cur)],
            ["Selection ratio", sr_phrase(s.get("selection_ratio"))],
            ["Expected tenure", f"{s.get('tenure'):.0f} years"],
            ["Value of one standard deviation of performance", "40% of salary (held flat)"],
            ["Current process validity (r)", f"{s['R_current']:.2f}"],
            ["Proposed battery validity (R)", f"{s['R_proposed']:.2f}"],
            ["Added validity (basis for value)", f"+{s['delta_r']:.2f}"],
            ["Scenario range (performance)", "proposed validity scaled to 85%, 100%, 110%"],
        ]
        if any_ret:
            m_rows.extend(retention_rows())
        m_rows.append(["Figures discounted (NPV)", "No"])
        m_rows.append(["Currency", "US dollars"])
        if budget:
            m_rows.append(["Annual cost of the assessments", fmt_full(budget, cur)])
        body.append({
            "type": "table",
            "columns": ["Input or assumption", "Value used"],
            "col_widths": [0.55, 0.45],
            "rows": m_rows,
        })

    # Defensive notes, kept to one or two short sentences.
    notes = (
        "Predictor validities are operational values from Sackett et al. (2022); multiple methods "
        "are combined with an adjustment for overlap rather than summed, and the combined value is "
        "capped at 0.60. "
    )
    if any(s.get("current_is_default") for s in segments):
        notes += ("The current process is assumed to be an unstructured interview where it was not "
                  "specified; a more rigorous current process would lower the estimated gain. ")
    if results.get("currency_note"):
        notes += results["currency_note"] + " "
    body.append({"type": "paragraph", "text": notes.strip()})

    # ===================== 5. ABOUT DEEPER SIGNALS (CALL TO ACTION) =====================
    body.append({"type": "h1", "text": CTA_HEADING})
    body.append({"type": "paragraph", "text": CTA_VARIANTS[CTA_DEFAULT]})

    # ===================== DISCLAIMER (verbatim, always) =====================
    body.append({"type": "h2", "text": "Important note on these estimates"})
    body.append({
        "type": "callout2",
        "items": [{"label": "These are estimates, not guarantees", "text": STANDARD_DISCLAIMER}],
    })

    # ---- Cover meta (tagline carries the headline value-per-dollar) ----
    if mid_bcr is not None:
        tagline1 = f"For every {cur}1 invested in better decisions,"
        tagline2 = f"the model estimates {cur}{mid_bcr:.2f} in talent value."
    else:
        tagline1 = meta.get("taglineLine1", "Finding and growing top talent is hard.")
        tagline2 = meta.get("taglineLine2", "Deeper Signals makes it measurable.")

    content = {
        "meta": {
            "title": meta.get("title", "Assessment ROI"),
            "subheading": meta.get("subheading", "Building the business case"),
            "taglineLine1": tagline1,
            "taglineLine2": tagline2,
            "date": meta.get("date", ""),
        },
        "body": body,
    }
    return content


def main():
    if len(sys.argv) < 4:
        print("Usage: build_booklet_content.py results.json meta.json content.json", file=sys.stderr)
        sys.exit(1)
    with open(sys.argv[1]) as f:
        results = json.load(f)
    with open(sys.argv[2]) as f:
        meta = json.load(f)
    content = build(results, meta)
    with open(sys.argv[3], "w") as f:
        json.dump(content, f, indent=2)
    print(f"Wrote {sys.argv[3]} ({len(content['body'])} body blocks)")


if __name__ == "__main__":
    main()
