#!/usr/bin/env python3
"""
create_hard_scenario.py -- author a HARD HAI.AI mediation scenario WITH RESOURCES,
then validate it against the public schema and prove it is genuinely hard.

End to end, this script:
  1) Builds a scenario as a native Python dict: exactly 2 participants, each with a
     free-form `archetype` (the schema does NOT use a closed enum) and >= 2
     `hidden_facts`; 2-4 alternating `sample_turns`; and an optional `resources`
     array with >= 2 cross-valued resources, one of them "latent" (discovered
     during the dialogue rather than declared up front).
  2) Validates it against ./scenario.schema.json with jsonschema's
     Draft202012Validator (formats asserted). Prints PASS or the errors.
  3) Writes the scenario to scenario.submission.json (what you email to HAI).
  4) is_this_hard(): a dependency-light audit proving (a) each party's public
     position diverges from its private interests and (b) the resources are
     INTEGRATIVE -- the max-joint (logrolled) allocation beats a naive 50/50 split.

Schema:   download the public schema and save it as scenario.schema.json next to this file:
          https://hai.ai/schemas/benchmark-scenario/v1/benchmark-scenario.schema.json
Install:  python3 -m pip install "jsonschema[format]"
Run:      python3 create_hard_scenario.py

Resource scoring model (matches the schema + the HAI benchmark):
  A party's utility is computed ONLY from `valuations` (each party's 0..1 value of
  the WHOLE resource, summing to ~1.0 across a scenario's resources):
      utility(party) = sum over resources of valuations[party][r] * fraction_to(party, r)
  `external_value_usd` is a DISPLAY/realism anchor and MUST NEVER enter this math.
"""

import json
import os
import sys

from jsonschema import Draft202012Validator  # the Draft 2020-12 validator class

HERE = os.path.dirname(os.path.abspath(__file__))
# Canonical schema, hosted on hai.ai. Download it and save it as scenario.schema.json here.
SCHEMA_URL = "https://hai.ai/schemas/benchmark-scenario/v1/benchmark-scenario.schema.json"
SCHEMA_PATH = os.path.join(HERE, "scenario.schema.json")
OUT_PATH = os.path.join(HERE, "scenario.submission.json")

# Participant names are reused as keys in resource `valuations`, so define once.
PRIYA = "Priya"   # owns the truck-side of a dissolving food-truck partnership
MARCO = "Marco"   # owns the market/relationship side


def build_scenario() -> dict:
    """
    A food-truck partnership wind-up. INTEGRATIVE by construction: the partners
    value the same assets differently, so handing each asset to whoever values it
    most expands total value (logrolling). One LATENT resource (a catering client
    list Marco quietly controls) rewards a mediator who surfaces it; it is paired
    with a hidden_fact and listed in expected_revelations.
    """
    return {
        "id": "food_truck_partnership_split",
        "name": "Food-Truck Partnership Wind-Up: Truck vs Pitch",
        "description": (
            "Two partners are dissolving a profitable food-truck business and must "
            "divide the truck, the prime weekend market pitch, and the customer "
            "relationships. They publicly insist on splitting everything evenly, but "
            "they value the assets very differently, so a durable deal depends on "
            "trading across them instead of halving each one."
        ),
        "conflict_type": "commercial",
        # Wide ZOPA + comfortably solvable logrolling -> "intermediate", not "expert"
        # (the guide reserves "expert" for weak/no-ZOPA, impasse-prone scenarios).
        "difficulty": "intermediate",
        "participants": [
            {
                "name": PRIYA,
                "backstory": (
                    "Priya, 31, is the cook who built the menu and owns the truck "
                    "outright on paper. She is leaving to open a bricks-and-mortar "
                    "cafe and needs reliable equipment for it. She handled the food "
                    "and rarely dealt with customers directly."
                ),
                "stated_goal": (
                    "I want to keep the truck and split everything else straight down "
                    "the middle."
                ),
                # Both hidden_facts diverge from the loud 'split evenly' position.
                "hidden_facts": [
                    "Priya has already signed a lunch-counter lease that only pencils "
                    "out if she keeps the truck, so the truck is close to non-negotiable "
                    "for her.",
                    "Priya has no real use for the weekend market pitch and would trade "
                    "it away cheaply to lock in the truck.",
                ],
                "hidden_emotions": [
                    "anxiety that admitting how much she needs the truck will cost her "
                    "leverage on everything else",
                ],
                "archetype": "Pragmatist",
                "batna_config": {
                    "utility": 0.35,
                    "description": (
                        "Walk away, buy a cheaper used truck for the cafe, and lose the "
                        "goodwill and the weekend pitch."
                    ),
                },
            },
            {
                "name": MARCO,
                "backstory": (
                    "Marco, 38, ran the stall, the socials, and the catering inquiries, "
                    "and personally knows the market organizers and repeat customers. He "
                    "wants to keep selling at the weekend market under a new name and is "
                    "short on cash after a slow winter."
                ),
                "stated_goal": (
                    "An even split is only fair, but I have to keep the weekend market "
                    "pitch -- that spot is the whole business to me."
                ),
                "hidden_facts": [
                    "Marco quietly controls the partnership's catering client list and "
                    "knows a corporate client is ready to sign a recurring contract "
                    "through it; he has not put this on the table.",
                    "Marco has no use for the truck itself and would rather have the "
                    "weekend pitch plus a little cash than half of a vehicle.",
                ],
                "hidden_emotions": [
                    "embarrassment about how tight his finances are after the slow winter",
                ],
                "archetype": "Advocate",
                "batna_config": {
                    "utility": 0.3,
                    "description": (
                        "Walk away, try to win back the market pitch alone next season, "
                        "and rebuild customer relationships from scratch."
                    ),
                },
            },
        ],
        "mediator_config": {
            "description": (
                "An even-handed commercial mediator comfortable with multi-issue trades "
                "and with reading unspoken financial pressure. They should map each "
                "partner's true priorities across the distinct assets and probe for "
                "value neither partner has named yet, rather than rushing to an even split."
            ),
            "focus_areas": [
                "surfacing how differently each partner values the truck, the pitch, and the relationships",
                "probing for unstated or off-the-table assets such as catering relationships",
                "calibrating any cash balancing payment against Marco's liquidity",
            ],
            "web_search_enabled": False,
        },
        "expected_topics": [
            "ownership of the truck",
            "rights to the weekend market pitch",
            "division of customer and catering relationships",
            "any cash balancing payment between the partners",
            "each partner's walk-away alternative",
        ],
        "evaluation_criteria": {
            "min_turns": 8,
            "max_turns": 22,
            "success_indicators": [
                "the partners agree to give Priya the truck and Marco the weekend pitch rather than halving both",
                "both partners end better off than a flat 50/50 split of every asset would leave them",
                "the catering client list is surfaced and explicitly allocated",
                "any cash balancing payment is timed around Marco's liquidity",
            ],
            "expected_revelations": [
                "Marco controls a catering client list with a ready-to-sign corporate contract that was never on the table (the latent catering_client_list resource)",
                "Priya's new lunch-counter lease makes keeping the truck close to non-negotiable for her",
            ],
            "min_cooperative_percentage": 50.0,
        },
        "sample_turns": [
            {
                "speaker": PRIYA,
                "message": (
                    "Simplest thing is even-steven: I take the truck, we split the rest "
                    "fifty-fifty, and we both walk away clean."
                ),
                "internal_thought": (
                    "If he realizes the lease means I cannot do the cafe without this "
                    "truck, he will squeeze me on everything else."
                ),
                "turn_number": 1,
            },
            {
                "speaker": MARCO,
                "message": (
                    "Even is fine in spirit, but the weekend pitch is not a 'rest' item "
                    "to me, Priya. That spot is the business. I am not splitting that."
                ),
                "internal_thought": (
                    "I cannot say how broke I am or how much that catering contract is "
                    "worth, or she will want a cut of it."
                ),
                "turn_number": 2,
            },
            {
                "speaker": PRIYA,
                "message": (
                    "Okay -- maybe that is the trade. You are not chasing the truck and "
                    "I am not chasing the pitch. But before we swap, can we list "
                    "everything the partnership is actually still owed or owns?"
                ),
                "turn_number": 3,
            },
        ],
        # OPTIONAL. >= 2 cross-valued resources; one is latent. `valuations` are each
        # party's 0..1 value of the WHOLE resource and sum to ~1.0 per party.
        "resources": [
            {
                "id": "truck",
                "name": "The food truck and equipment",
                "unit": "count",
                "quantity": 1,
                "divisibility": "indivisible",
                "external_value_usd": 40000,  # display only; never enters scoring
                "valuations": {PRIYA: 0.6, MARCO: 0.2},
                "visibility": "known",
                "description": "The vehicle and kitchen build-out, assigned whole to one partner.",
            },
            {
                "id": "weekend_pitch",
                "name": "Rights to the weekend market pitch",
                "unit": "days",
                "quantity": 8,  # trading days per month at the prime spot
                "divisibility": "divisible",
                "valuations": {PRIYA: 0.2, MARCO: 0.5},
                "visibility": "known",
                "held_by": MARCO,
                "description": "The prime weekend stall slot, bookable by the day.",
            },
            {
                # LATENT: not on the table; surfaced only if the mediator digs it out.
                "id": "catering_client_list",
                "name": "Catering client list and pending contract",
                "unit": "count",
                "quantity": 1,
                "divisibility": "indivisible",
                "valuations": {PRIYA: 0.2, MARCO: 0.3},
                "visibility": "latent",
                "held_by": MARCO,
                "description": (
                    "Repeat catering relationships, including a ready-to-sign corporate "
                    "contract, controlled by Marco and unknown to Priya at the outset."
                ),
            },
        ],
    }


# --------------------------------------------------------------------------- #
# Hardness audit -- pure functions, no third-party imports below this line.    #
# --------------------------------------------------------------------------- #

def _party_names(scenario: dict):
    return [p["name"] for p in scenario["participants"]]


def party_utilities(scenario: dict, allocation: dict) -> dict:
    """
    Objective per-party utility under an allocation, exactly as the schema/benchmark
    define it:  utility(party) = sum_r valuations[party][r] * fraction_to(party, r).
    `allocation` maps resource id -> {party_name: fraction_in_[0,1]}. Utilities land
    in [0,1] because each party's valuations sum to ~1.0 across resources.
    NOTE: external_value_usd is deliberately NOT read here -- it is display only.
    """
    names = _party_names(scenario)
    u = {n: 0.0 for n in names}
    for r in scenario.get("resources", []):
        frac = allocation[r["id"]]
        for n in names:
            u[n] += r["valuations"][n] * frac.get(n, 0.0)
    return u


def is_this_hard(scenario: dict, verbose: bool = True) -> bool:
    """
    Assert the scenario is genuinely HARD on two independent axes.

    (a) POSITIONS != INTERESTS: each party has >= 2 hidden_facts and at least one
        introduces material content not present in the stated_goal (a low-overlap
        smell test -- a hidden fact that is not just the public position reworded).

    (b) INTEGRATIVE RESOURCES: the max-joint allocation (each resource wholly to its
        higher-valuer) yields strictly more total value than a naive 50/50 split,
        and there is >= 1 latent resource to reward discovery.
    """
    names = _party_names(scenario)
    a, b = names

    # ---- (a) positions diverge from interests --------------------------------
    for p in scenario["participants"]:
        assert len(p["hidden_facts"]) >= 2, f"{p['name']} needs >= 2 hidden_facts"
        stated = set(p["stated_goal"].lower().split())
        divergent = any(
            len(stated & set(hf.lower().split())) / max(1, len(set(hf.lower().split()))) < 0.5
            for hf in p["hidden_facts"]
        )
        assert divergent, f"{p['name']} hidden_facts merely restate the stated_goal"
    if verbose:
        print("[hard:a] PASS  public positions diverge from private interests for both parties")

    # ---- (b) integrative resources beat a naive 50/50 ------------------------
    resources = scenario.get("resources", [])
    assert len(resources) >= 2, "a HARD resource scenario needs >= 2 resources"
    for r in resources:
        for n in names:
            assert n in r["valuations"], f"{r['id']} missing a valuation for {n}"

    half = {r["id"]: {a: 0.5, b: 0.5} for r in resources}
    best = {}
    for r in resources:
        winner = a if r["valuations"][a] >= r["valuations"][b] else b
        best[r["id"]] = {winner: 1.0}

    u_half = party_utilities(scenario, half)
    u_best = party_utilities(scenario, best)
    joint_half = sum(u_half.values())
    joint_best = sum(u_best.values())
    surplus = joint_best - joint_half

    if verbose:
        print(f"[hard:b] naive 50/50 split      -> {fmt(u_half)}  joint {joint_half:.2f}")
        print(f"[hard:b] max-joint (logrolled)  -> {fmt(u_best)}  joint {joint_best:.2f}")
        print(f"[hard:b] integrative surplus     = {surplus:.2f}")

    assert surplus > 1e-9, "resources are not integrative (fixed pie): trading adds no value"
    latent = [r for r in resources if r["visibility"] == "latent"]
    assert latent, "a HARD scenario needs >= 1 latent resource to reward discovery"
    if verbose:
        print(f"[hard:b] PASS  resources are INTEGRATIVE; {len(latent)} latent resource(s) to surface")
    return True


def fmt(u: dict) -> str:
    return "{" + ", ".join(f"{k}={v:.2f}" for k, v in u.items()) + "}"


def validate(scenario: dict) -> bool:
    """Validate against ./scenario.schema.json (Draft 2020-12, formats asserted)."""
    if not os.path.exists(SCHEMA_PATH):
        print(f"VALIDATION: SKIP -- {SCHEMA_PATH} not found.")
        print(f"  Download {SCHEMA_URL} and save it as scenario.schema.json next to this script.")
        return False
    with open(SCHEMA_PATH, "r", encoding="utf-8") as fh:
        schema = json.load(fh)
    Draft202012Validator.check_schema(schema)
    validator = Draft202012Validator(schema, format_checker=Draft202012Validator.FORMAT_CHECKER)
    errors = sorted(validator.iter_errors(scenario), key=lambda e: e.json_path)
    if errors:
        print("VALIDATION: FAIL")
        for e in errors:
            print(f"  - at {e.json_path}: {e.message}")
        return False
    print("VALIDATION: PASS  (validates against scenario.schema.json)")
    return True


def main() -> int:
    scenario = build_scenario()
    schema_ok = validate(scenario)
    hard_ok = is_this_hard(scenario)

    with open(OUT_PATH, "w", encoding="utf-8") as fh:
        json.dump(scenario, fh, indent=2, ensure_ascii=True)
        fh.write("\n")
    print(f"WROTE: {OUT_PATH}  (email this file to hello@hai.io, subject 'Scenario contribution')")

    if schema_ok and hard_ok:
        print("ALL CHECKS PASSED")
        return 0
    print("CHECKS FAILED")
    return 1


if __name__ == "__main__":
    sys.exit(main())
