#!/usr/bin/env node
/*
 * create_hard_scenario.mjs -- 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 object: 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 Ajv (Draft 2020-12).
 *   3) Writes the scenario to scenario.submission.json (what you email to HAI).
 *   4) isThisHard(): proves (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://mediationbench.com/benchmarks/benchmark-scenario.schema.json
 * Install:  npm install ajv ajv-formats
 * Run:      node create_hard_scenario.mjs
 *
 * IMPORTANT: import Ajv from 'ajv/dist/2020' for Draft 2020-12. Plain 'ajv'
 * defaults to draft-07 and will not understand this schema's $defs/format usage.
 *
 * Resource scoring model (matches the schema + the HAI benchmark):
 *   utility(party) = sum over resources of valuations[party][r] * fractionTo(party, r)
 *   `external_value_usd` is a DISPLAY anchor and MUST NEVER enter this math.
 */

import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv2020 from "ajv/dist/2020.js"; // Draft 2020-12 build of Ajv
import addFormats from "ajv-formats";

const HERE = dirname(fileURLToPath(import.meta.url));
// Published schema mirror. Download it and save it as scenario.schema.json here.
const SCHEMA_URL = "https://mediationbench.com/benchmarks/benchmark-scenario.schema.json";
const SCHEMA_PATH = join(HERE, "scenario.schema.json");
const OUT_PATH = join(HERE, "scenario.submission.json");

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

function buildScenario() {
  // A food-truck partnership wind-up. INTEGRATIVE by construction: partners value
  // the same assets differently, so handing each asset to its higher-valuer expands
  // total value (logrolling). One LATENT resource (a catering client list Marco
  // controls) rewards a mediator who surfaces it; it pairs with a hidden_fact and an
  // expected_revelation.
  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".
    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,
      // Per-scenario, not a harness-wide cap; 24 is the recommended default.
      max_turns: 24,
      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 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.                                            //
// --------------------------------------------------------------------------- //

const partyNames = (s) => s.participants.map((p) => p.name);

// utility(party) = sum_r valuations[party][r] * fractionTo(party, r). [0,1].
// external_value_usd is deliberately NOT read here -- it is display only.
function partyUtilities(scenario, allocation) {
  const names = partyNames(scenario);
  const u = Object.fromEntries(names.map((n) => [n, 0]));
  for (const r of scenario.resources ?? []) {
    const frac = allocation[r.id];
    for (const n of names) u[n] += r.valuations[n] * (frac[n] ?? 0);
  }
  return u;
}

const fmt = (u) =>
  "{" + Object.entries(u).map(([k, v]) => `${k}=${v.toFixed(2)}`).join(", ") + "}";

const tokens = (s) => new Set(s.toLowerCase().split(/\s+/));

function isThisHard(scenario, verbose = true) {
  const names = partyNames(scenario);
  const [a, b] = names;

  // (a) public positions diverge from private interests
  for (const p of scenario.participants) {
    if (p.hidden_facts.length < 2) throw new Error(`${p.name} needs >= 2 hidden_facts`);
    const stated = tokens(p.stated_goal);
    const divergent = p.hidden_facts.some((hf) => {
      const t = tokens(hf);
      const overlap = [...stated].filter((w) => t.has(w)).length / Math.max(1, t.size);
      return overlap < 0.5;
    });
    if (!divergent) throw new Error(`${p.name} hidden_facts merely restate the stated_goal`);
  }
  if (verbose) console.log("[hard:a] PASS  public positions diverge from private interests for both parties");

  // (b) integrative resources beat a naive 50/50
  const resources = scenario.resources ?? [];
  if (resources.length < 2) throw new Error("a HARD resource scenario needs >= 2 resources");
  for (const r of resources)
    for (const n of names)
      if (!(n in r.valuations)) throw new Error(`${r.id} missing a valuation for ${n}`);

  const half = Object.fromEntries(resources.map((r) => [r.id, { [a]: 0.5, [b]: 0.5 }]));
  const best = Object.fromEntries(
    resources.map((r) => {
      const winner = r.valuations[a] >= r.valuations[b] ? a : b;
      return [r.id, { [winner]: 1.0 }];
    })
  );

  const uHalf = partyUtilities(scenario, half);
  const uBest = partyUtilities(scenario, best);
  const jointHalf = Object.values(uHalf).reduce((x, y) => x + y, 0);
  const jointBest = Object.values(uBest).reduce((x, y) => x + y, 0);
  const surplus = jointBest - jointHalf;

  if (verbose) {
    console.log(`[hard:b] naive 50/50 split      -> ${fmt(uHalf)}  joint ${jointHalf.toFixed(2)}`);
    console.log(`[hard:b] max-joint (logrolled)  -> ${fmt(uBest)}  joint ${jointBest.toFixed(2)}`);
    console.log(`[hard:b] integrative surplus     = ${surplus.toFixed(2)}`);
  }

  if (surplus <= 1e-9) throw new Error("resources are not integrative (fixed pie): trading adds no value");
  const latent = resources.filter((r) => r.visibility === "latent");
  if (latent.length === 0) throw new Error("a HARD scenario needs >= 1 latent resource to reward discovery");
  if (verbose)
    console.log(`[hard:b] PASS  resources are INTEGRATIVE; ${latent.length} latent resource(s) to surface`);
  return true;
}

function validate(scenario) {
  if (!existsSync(SCHEMA_PATH)) {
    console.log(`VALIDATION: SKIP -- ${SCHEMA_PATH} not found.`);
    console.log(`  Download ${SCHEMA_URL} and save it as scenario.schema.json next to this script.`);
    return false;
  }
  const schema = JSON.parse(readFileSync(SCHEMA_PATH, "utf-8"));
  const ajv = new Ajv2020({ allErrors: true, strict: false });
  addFormats(ajv);
  const validateFn = ajv.compile(schema);
  if (validateFn(scenario)) {
    console.log("VALIDATION: PASS  (validates against scenario.schema.json)");
    return true;
  }
  console.log("VALIDATION: FAIL");
  for (const e of validateFn.errors) console.log(`  - at ${e.instancePath || "$"}: ${e.message}`);
  return false;
}

function main() {
  const scenario = buildScenario();
  const schemaOk = validate(scenario);
  let hardOk = false;
  try {
    hardOk = isThisHard(scenario);
  } catch (err) {
    console.log(`[hard] FAIL  ${err.message}`);
  }

  writeFileSync(OUT_PATH, JSON.stringify(scenario, null, 2) + "\n");
  console.log(`WROTE: ${OUT_PATH}  (email this file to hello@hai.io, subject 'Scenario contribution')`);

  if (schemaOk && hardOk) {
    console.log("ALL CHECKS PASSED");
    process.exit(0);
  }
  console.log("CHECKS FAILED");
  process.exit(1);
}

main();
