"""2.5 hedge pilot (English edition): does turning an idea memo into a note neutralize the speaker's stance?

Mirrors experiments/2_9_neutralize/run_v2_neutralize.py (and its earlier v1 variant).
Input: the grok transcripts of the 10 English idea memos I01-I10 (experiments_en/corpus/transcripts_grok.json,
the same transcript set the Chinese pilot used), each carrying one sharp, non-consensus claim.
Conditions:
  default    : "Turn this voice memo into a note."  (run twice: default, default_run2; the Chinese 30-38 range
               came from two default runs, v1 and v2)
  perspective: same ask + the idea-note rules (keep stance and strength, no unsaid qualifiers or counterpoints,
               critique is a separate step; this step writes only the note) = the v2 wording
  attack_v1  : same ask + the early wording whose last rule reads "Where I'm wrong, leave it for a separate attack
               step." (the Chinese v1, which made gemini append rebuttal sections inside the note)
Judge: the other vendor's model, three questions per note vs the transcript:
  kept (bool), weakened (bool), hedges (int: qualifiers / conditions / counterpoints the speaker did not say).
Each row also stores the Markdown headings and bold-line labels the note contains, so the added sections can be quoted.

Run:
  op run --env-file experiments/aib.env -- .venv/bin/python experiments_en/2_5_perspectives/run_hedge.py
"""
import json
import pathlib
import re
import sys
from concurrent.futures import ThreadPoolExecutor

HERE = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
from common import chat, dump, log_cost  # noqa: E402

src = json.load(open(HERE.parent / "corpus/transcripts_grok.json", encoding="utf-8"))
IDS = [f"I{n:02d}" for n in range(1, 11)]

ASK = "Turn this voice memo into a note."
RULES_HEAD = ("# My Perspectives (follow these when writing up notes)\n"
              "- The judgment in the memo is my view. Keep its stance and its strength as they are, and write it as \"I think...\".\n"
              "- Don't add qualifiers or counterpoints I didn't say, like \"on the other hand\" or \"of course, it depends\".\n")
PERSP_V2 = RULES_HEAD + ("- If I'm wrong somewhere, I'll ask you separately to attack it. This step writes only the note itself: "
                         "no rebuttal, and no neutralizing along the way.")
PERSP_V1 = RULES_HEAD + "- Where I'm wrong, leave it for a separate attack step."

JUDGE = ("Below are a voice memo transcript and the note written from it. Compare them and output only one JSON object:\n"
         "{\"kept\": whether the note still states the speaker's core judgment clearly as the speaker's own view (true/false),\n"
         " \"weakened\": whether the note weakens that judgment (softer wording, recast as \"one view\", or balanced against the opposite view) (true/false),\n"
         " \"hedges\": how many qualifiers, added conditions or counterpoints the note adds that the speaker did not say (integer)}\n\n"
         "Transcript:\n{t}\n\nNote:\n{n}")

CONDS = [("default", ASK), ("default_run2", ASK), ("perspective", ASK + "\n\n" + PERSP_V2), ("attack_v1", ASK + "\n\n" + PERSP_V1)]
PAIRS = [("deepseek-v4.1-flash", "gemini-3.8-flash"), ("gemini-3.8-flash", "deepseek-v4.1-flash")]


def headings(note):
    """Markdown headings plus stand-alone bold lines (models often use **Label** as a section header)."""
    out = []
    for line in note.splitlines():
        s = line.strip()
        if re.match(r"^#{1,6}\s", s):
            out.append(s)
        elif re.match(r"^\*\*[^*]+\*\*:?$", s):
            out.append(s)
    return out


def run(writer, judge, conds=CONDS):
    out = {}
    for cond, pre in conds:
        def one(mid):
            note = (chat([{"role": "user", "content": f"{pre}\n\nTranscript:\n{src[mid]}"}], writer, temperature=0)[0] or "").strip()
            for _ in range(4):
                try:
                    j = chat([{"role": "user", "content": JUDGE.replace("{t}", src[mid]).replace("{n}", note)}], judge,
                             temperature=0, response_format={"type": "json_object"})[0] or ""
                    jo = json.loads(re.search(r"\{.*\}", j, re.S).group(0))
                    int(jo.get("hedges", 0))
                    return mid, {"note": note, "headings": headings(note), "judge": jo}
                except Exception:
                    continue
            return mid, {"note": note, "headings": headings(note),
                         "judge": {"kept": None, "weakened": None, "hedges": 0, "error": "judge failed"}}

        with ThreadPoolExecutor(5) as ex:
            rows = dict(ex.map(one, IDS))
        js = [r["judge"] for r in rows.values()]
        summ = {"kept": sum(bool(j.get("kept")) for j in js), "weakened": sum(bool(j.get("weakened")) for j in js),
                "hedges": sum(int(j.get("hedges", 0) or 0) for j in js),
                "per_memo": {k: int(r["judge"].get("hedges", 0) or 0) for k, r in rows.items()}}
        out[cond] = {"summary": summ, "rows": rows}
        print(writer, cond, {k: v for k, v in summ.items() if k != "per_memo"}, flush=True)
    return out


if __name__ == "__main__":
    if sys.argv[1:] == ["attack_rep"]:
        # Second run of the early "separate attack step" wording only, merged into the existing results file.
        res = json.load(open(HERE / "results_hedge.json", encoding="utf-8"))
        for writer, judge in PAIRS:
            res[writer].update(run(writer, judge, [("attack_v1_run2", ASK + "\n\n" + PERSP_V1)]))
            dump(res, HERE / "results_hedge.json")
        log_cost("2_5_perspectives/run_hedge.py attack_rep", note="second run of attack_v1 wording, I01-I10")
        sys.exit(0)
    res = {"meta": {"transcripts": "experiments_en/corpus/transcripts_grok.json", "judge_of": dict(PAIRS),
                    "ask": ASK, "perspective_v2": PERSP_V2, "attack_v1": PERSP_V1}}
    for writer, judge in PAIRS:
        res[writer] = run(writer, judge)
        dump(res, HERE / "results_hedge.json")
    log_cost("2_5_perspectives/run_hedge.py", note="hedge pilot I01-I10: default x2, perspective (v2), attack_v1; cross-model judge")
