#!/usr/bin/env python3
"""site_team.py — サイトの入口のページを、3社の AI の「制作チーム」で作る（合議はしない）.

合議（全員の批判を通る案を残す）にすると、残るのは価値のある案ではなく、誰も反論できない無難な案になる。
そこで、ひとつにまとめず、作者ごとの案を磨いて並べる。

流れ（1つの依頼につき）:
  1. 3社（Claude・Gemini・codex）が、互いに見せずに企画書を1本ずつ書く（v1）
  2. ほかの2社が評者として攻める。指摘は必ず「弱い所」と「こう直せばもっと強くなる」の組。
     案を捨てる・題材を変えさせる権限は誰にもない。事実の誤りは、ログにある範囲でいちばん強い言い方に直して示す
  3. 作者が書き直す。指摘を取り込むかは作者が決め、尖りは守る。2〜3を 回数.txt の回数（既定2）くり返す
  4. 3本を並べたまま終える（並べた.md）。選ぶのはエイイチ
  5. 話し合いで良くなったかを確かめる（比べる.md）。各作者の最初の版と最後の版を、
     どちらがどちらか伏せて並べる。答えは _答え（見ないで）.json
  評者にも作者の名前は伏せる（案A・案B・案C と呼ぶ）。どの社がどの案かは _対応（見ないで）.json

置き場所: PixelstoMotors/サイト改善/制作/{依頼の名前}/
  依頼.md      Claude が書く。何を作るか・守ること・材料（数字・ログ一覧）
  回数.txt     書き直しの回数（無ければ2）
  かける.txt   印。終わると かけた.txt に名前を変える
  止める.txt   置くと、いまの段が終わったところで止まる（消せば続きから。できたファイルは作り直さない）

使い方（Mac のターミナルで）:
  python3 site_team.py --check     3社が答えるかを確かめるだけ
  python3 site_team.py --once      印のついた依頼を1つ回す
  python3 site_team.py --install   5分ごとに見回るように登録する（YouTube の会議とは別に回る）
"""
import argparse, json, os, random, re, subprocess, sys, tempfile, time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime

HERE = os.path.dirname(os.path.abspath(__file__))                 # PixelstoMotors/サイト改善/_仕組み
P2M = os.path.dirname(os.path.dirname(HERE))                      # PixelstoMotors
BASE = os.path.join(P2M, "デジタルツイン学習R2R2R", "動画", "_制作ベース")
sys.path.insert(0, BASE)
import debate_runner as dr                                        # Gemini・codex の呼び方とキーを使い回す
cr = dr.cr

JOBS = os.path.join(os.path.dirname(HERE), "制作")
MARK, DONE, STOP, RUNNING = "かける.txt", "かけた.txt", "止める.txt", "実行中.txt"
AUTHORS = ["Claude", "Gemini", "codex"]
CLAUDE = os.path.expanduser("~/.npm-global/bin/claude")
CLAUDE_TIMEOUT = 25 * 60
PATH = ":".join([os.path.expanduser("~/.npm-global/bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])
LABEL = "jp.adaptdesign.pixelstomotors.site-team"
PLIST = os.path.expanduser(f"~/Library/LaunchAgents/{LABEL}.plist")
LOGFILE = os.path.join(cr.CONF, "site_team.log")

GROUND = """\
# 前提（全員に共通）

あなたは、Webサイト「Pixels to Motors」（pixelstomotors.com）の、人の入口になるページを作る制作チームの一員です。
チームは3つの AI で、最後に人間（エイイチ）が選びます。
目指すのは、届けたい相手が「これは保存しておきたい」と思う、価値のあるページです。誰にも怒られない無難なページではありません。

## 守ること（エイイチが決めたこと）
- 個々のログ（/record/…）の形式は変えない。入口のページから送るだけ
- 題材を広げない。入口のページは、依頼の「ログ一覧」にある中身だけで作る。ログにない事実・数字を足さない
- トップや SNS に相談の窓口を出さない。新しいキャッチコピー・肩書きを足さない
- 中身より言い過ぎた見出し・煽り・他社を悪く言う演出はしない。ただし、ログにある事実の範囲でいちばん強く言うのはよい
- 日本語で書く。英語の文を混ぜない。数字には根拠（依頼の実際の数字・ログ・推測）を添え、推測は推測と書く
"""

PLAN = """\
## 企画書の形（3,000字以内。この見出しで書く）
### ページの名前と URL の案
### 誰に（1人に絞る。その人がどこから来て、何に困っているか）
### この人がこのページで得るもの（1文）
### 最初の画面（見出しと、その下の3行。実際の文面で書く）
### 見せ場（このページにしかないもの。ログのどの事実か）
### 構成（4〜6節。節ごとに、何を書くかと、使うログ /record/… とその中の事実）
### 束ねるログ（/record/… の一覧）
### 今あるページとのつなぎ方（トップ・シリーズ・今ある LP）
### 公開後に見る数字（30件たまるまでは判断しない）
### ゴールにどう効くか（依頼にゴールの表があれば、その行ごとに。効く理由と、効かないかもしれない理由の両方）
"""

AUTHOR_V1 = """\
# あなたの役：作者
入口のページを1本、作者として企画してください。ほかの作者の案は見ていません。見る必要もありません。
- 依頼に「達成したいこと」と「ゴール」があれば、それに効くことを最優先にする
- 自分がいちばん価値があると思う題材と切り口を、ログ一覧から選ぶ。ほかの人も選びそうな無難な切り口より、あなたにしか出せない切り口を選ぶ
- 届けたい相手を1人に絞り、その人に深く刺さるように尖らせる
- 迷ったら、平均的な案ではなく、強い案を選ぶ。言い過ぎは後で直せるが、弱さは直せない
""" + PLAN

CRITIC = """\
# あなたの役：評者（{me}）
作者が書いた「{target}」の企画書を、もっと強くするために攻めてください。ほかの案とは比べません。
- 「強い」とは、依頼の「達成したいこと」と「ゴール」に効くこと。ゴールに効かない所をいちばん重く見る
- 指摘は必ず「弱い所」と「こう直せばもっと強くなる（具体の書き換え案・文面）」の組で書く。弱い所だけの指摘は書かない
- 案を捨てる・題材や相手を変えるよう求めない。作者が選んだ題材と相手のまま、強くする
- 事実の誤り（ログ一覧にないログ・ログにない中身・言い過ぎ）は、ログにある範囲でいちばん強い言い方に書き直して示す
- 指摘には重さを付ける：【高】直さないと伝わらない・誤り ／【中】直すと強くなる ／【低】好み。全部で5〜8個まで
- 最後に「守るべき尖り」として、この案のいちばん良い所を1つ書く（作者が書き直しで消さないように）
"""

REWRITE = """\
# あなたの役：作者（書き直し）
あなたが書いた企画書（{target}）に、2人の評者から指摘が届きました。
- 取り込むかどうかは、作者のあなたが決める。評者の言いなりにならない。案の尖り（いちばん良い所）は守る
- 事実の誤りの指摘だけは、必ず直す（ログにある範囲でいちばん強い言い方に）
- 出す形：まず【採否】として、指摘ごとに「採る／一部採る／採らない」と理由を1行ずつ。次に【企画書】として、書き直した企画書の全文
""" + PLAN


VALUE_CRITIC = """\
# あなたの役：評者（{me}）
作者が書いた「{target}」のページを、もっと価値のあるものにするために攻めてください。ほかの案とは比べません。
問うのは1つだけ：**読む人（ロボット導入を判断する経営者・任された PM）が、このページで初めて得るものは何か。**
- ほかのサイトやカタログでも手に入ること、読む人の判断に効かないことは「削る」か「この現場の数字で強くする」かを、書き換えの文面つきで示す
- いちばん刺さる項目と、いちばん弱い項目を1つずつ挙げ、弱い方の直し方を書く
- 案を捨てさせない。作者の切り口のまま強くする。指摘は5〜8個まで
- 事実の確かめは「事実の照合」の材料と照らして行う。合わない所は、ログにある範囲でいちばん強い言い方に直して示す
"""

REWRITE_PAGE = """\
# あなたの役：作者（書き直し）
あなたが書いたページ（{target}）に、2人の評者から指摘が届きました。
- 取り込むかどうかは、作者のあなたが決める。いちばん刺さる所は守る
- 事実の誤りの指摘だけは、必ず直す
- 出す形：まず【採否】として、指摘ごとに「採る／一部採る／採らない」と理由を1行ずつ。次に【企画書】として、書き直したページの全文
"""

AUTHOR_PAGE = """\
# あなたの役：作者
依頼の「ページの形」に沿って、ページそのものを書いてください。ほかの作者のページは見ていません。
- 依頼の後ろにある「持ち寄った材料」（ログからまとめた項目・外の文脈・事実の照合）を全部使う
- 読む人が「入れる前に知りたかった」と思うことを、いちばん前に出す。無難にまとめない
"""


def now():
    return datetime.now(dr.JST).strftime("%Y-%m-%d %H:%M")


def claude(prompt, cwd=None, tools=None):
    env = dict(os.environ)
    for k in ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"):
        env.pop(k, None)
    env.update({"ENABLE_CLAUDEAI_MCP_SERVERS": "false", "PATH": PATH, "LANG": "ja_JP.UTF-8"})
    with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryFile("w+", encoding="utf-8") as fo, \
            tempfile.TemporaryFile("w+", encoding="utf-8") as fe:
        cmd = [CLAUDE, "-p", prompt, "--output-format", "text", "--permission-mode", "default"]
        if tools:
            cmd += ["--allowedTools", ",".join(tools)]
        p = subprocess.Popen(cmd, cwd=cwd or tmp, stdout=fo, stderr=fe, stdin=subprocess.DEVNULL, env=env, start_new_session=True)
        try:
            p.wait(timeout=CLAUDE_TIMEOUT)
        except subprocess.TimeoutExpired:
            p.kill()
            raise RuntimeError("Claude が時間内に答えませんでした")
        fo.seek(0); fe.seek(0)
        out, err = fo.read().strip(), fe.read().strip()
    if p.returncode != 0 or not out:
        raise RuntimeError(f"Claude が失敗しました（終了コード {p.returncode}）: {err[-600:]}")
    return out


class Caller:
    def __init__(self):
        self.key = dr.gemini_key()
        self.model, _ = dr.pick_model(self.key)

    def ask(self, who, system, user, wdir, search=False, files=False):
        for attempt in range(3):
            try:
                if who == "Gemini":
                    ans, _ = dr.gemini(self.key, self.model, system, user, search=search)
                elif who == "codex":
                    ans, _ = dr.codex(wdir, system + "\n\n" + user, web=False)
                else:
                    tools = (["Read", "Glob", "Grep"] if files else []) + (["WebSearch", "WebFetch"] if search else [])
                    ans = claude(system + "\n\n" + user, cwd=wdir if files else None, tools=tools or None)
                return ans.strip()
            except Exception as e:
                dr.log(f"  {who} 失敗（{attempt + 1}回目）: {str(e)[:200]}")
                if attempt == 2:
                    raise
                time.sleep(30)


def rd(p):
    return open(p, encoding="utf-8").read()


def wr(p, text):
    os.makedirs(os.path.dirname(p), exist_ok=True)
    dr.write(p, text)


def plan_part(ans):
    m = re.search(r"【企画書】\s*(.*)", ans, re.S)
    return (m.group(1) if m else ans).strip()


def progress(d, lines):
    wr(os.path.join(d, "進み具合.md"), f"# 進み具合（{os.path.basename(d)}）\n\n- 最後に書いた時刻 {now()}\n" + "".join(f"- {x}\n" for x in lines))


def run(d, dry=False):
    name = os.path.basename(d)
    req = rd(os.path.join(d, "依頼.md"))
    rounds = 2
    if os.path.exists(os.path.join(d, "回数.txt")):
        try:
            rounds = max(1, min(4, int(rd(os.path.join(d, "回数.txt")).strip())))
        except ValueError:
            pass
    mp = os.path.join(d, "_対応（見ないで）.json")
    if os.path.exists(mp):
        who_of = json.load(open(mp, encoding="utf-8"))
    else:
        labels = ["案A", "案B", "案C"]
        sh = AUTHORS[:]
        random.shuffle(sh)
        who_of = dict(zip(labels, sh))
        if not dry:
            wr(mp, json.dumps(who_of, ensure_ascii=False, indent=1))
    labels = sorted(who_of)
    pack_tasks = None
    if os.path.exists(os.path.join(d, "持ち寄り.json")):
        pack_tasks = json.load(open(os.path.join(d, "持ち寄り.json"), encoding="utf-8"))
    if dry:
        dr.log(f"［試し］{name}: 書き直し {rounds} 回・{who_of}")
        return
    call = Caller()
    wr(os.path.join(d, RUNNING), f"開始 {now()}\nPID {os.getpid()}\n")
    done = []
    pool = ThreadPoolExecutor(max_workers=3)

    def stopped():
        if os.path.exists(os.path.join(d, STOP)):
            progress(d, done + ["止める.txt があるので止まっています（消せば続きから）"])
            return True
        return False

    def step(jobs):
        """jobs: [(出力パス, 社, system, user[, {search, files}])]。まだ無いものだけを同時に"""
        todo = [j for j in jobs if not os.path.exists(j[0])]
        futs = [(j[0], pool.submit(call.ask, j[1], GROUND + "\n" + j[2], j[3], d, **(j[4] if len(j) > 4 else {}))) for j in todo]
        err = None
        for path, f in futs:                             # できた分は残す（続きから再開したときに作り直さない）
            try:
                wr(path, f.result() + "\n")
            except Exception as e:
                err = err or e
        if err:
            raise err

    try:
        dr.log(f"▶ 制作チーム {name}: 書き直し {rounds} 回")
        author_v1, critic, rewrite = AUTHOR_V1, CRITIC, REWRITE
        if pack_tasks:
            # 0. 持ち寄り：3社がそれぞれにしか取れない材料を作る（Claude と codex は 材料/ のログ全文を読む。Gemini は Web 検索）
            opt = {"Claude": {"files": True}, "codex": {}, "Gemini": {"search": True}}
            step([(os.path.join(d, "持ち寄り", f"{w}.md"), w, pack_tasks[w]["役"], req, opt[w]) for w in AUTHORS])
            packs = "\n\n".join(f"## {pack_tasks[w]['名前']}\n\n" + rd(os.path.join(d, "持ち寄り", f"{w}.md")) for w in AUTHORS)
            req_raw, req = req, req + "\n\n# 持ち寄った材料\n\n" + packs
            author_v1, critic, rewrite = AUTHOR_PAGE, VALUE_CRITIC, REWRITE_PAGE
            done.append("0. 持ち寄りの材料（3社）ができました")
            # 比べる相手：Claude 1本だけで、同じ生の材料から同じページを書く（持ち寄りも話し合いもなし）
            step([(os.path.join(d, "1本", "v1.md"), "Claude", AUTHOR_PAGE + "\n- 材料/ のログ全文を自分で読み、必要なら Web 検索もしてよい。持ち寄った材料は無い",
                   req_raw, {"files": True, "search": True})])
            done.append("0. 比べる相手（Claude 1本の版）ができました")
            progress(d, done)
        # 1. 互いに見せずに書く
        step([(os.path.join(d, L, "v1.md"), who_of[L], author_v1, req) for L in labels])
        done.append("1. 3本の企画書（v1）ができました")
        progress(d, done)
        for r in range(1, rounds + 1):
            if stopped():
                return
            # 2. ほかの2社が攻める（名前は伏せる）
            jobs = []
            for L in labels:
                cur = rd(os.path.join(d, L, f"v{r}.md"))
                for C in labels:
                    if C == L:
                        continue
                    sysm = critic.format(me=f"評者（{C}の作者）", target=L)
                    jobs.append((os.path.join(d, L, f"批評{r}_{C}の作者から.md"), who_of[C], sysm,
                                 f"{req}\n\n# 作者の企画書（{L}・第{r}版）\n\n{cur}"))
            step(jobs)
            done.append(f"2. {r}回目の批評ができました")
            progress(d, done)
            if stopped():
                return
            # 3. 作者が書き直す
            jobs = []
            for L in labels:
                cur = rd(os.path.join(d, L, f"v{r}.md"))
                crit = "\n\n".join(f"## 評者（{C}の作者）から\n\n" + rd(os.path.join(d, L, f"批評{r}_{C}の作者から.md"))
                                   for C in labels if C != L)
                jobs.append((os.path.join(d, L, f"書き直し{r}.md"), who_of[L], rewrite.format(target=L),
                             f"{req}\n\n# あなたの企画書（{L}・第{r}版）\n\n{cur}\n\n# 届いた指摘\n\n{crit}"))
            step(jobs)
            for L in labels:
                v = os.path.join(d, L, f"v{r + 1}.md")
                if not os.path.exists(v):
                    wr(v, plan_part(rd(os.path.join(d, L, f"書き直し{r}.md"))) + "\n")
            done.append(f"3. {r}回目の書き直し（v{r + 1}）ができました")
            progress(d, done)
        last = rounds + 1
        # 4. 3本を並べる
        wr(os.path.join(d, "並べた.md"), f"# 並べた（{name}・最後の版）\n\nどの社がどの案かは伏せています（_対応（見ないで）.json）。選ぶのはエイイチ。\n\n"
           + "\n\n---\n\n".join(f"# {L}\n\n" + rd(os.path.join(d, L, f"v{last}.md")) for L in labels))
        # 5. 最初と最後を伏せて比べる
        if pack_tasks:
            cands = [("1本（Claude だけ）", rd(os.path.join(d, "1本", "v1.md")))] + [(f"{L}（話し合いのあと）", rd(os.path.join(d, L, f"v{last}.md"))) for L in labels]
            random.shuffle(cands)
            marks = ["①", "②", "③", "④"]
            wr(os.path.join(d, "_答え（見ないで）.json"), json.dumps({m: c[0] for m, c in zip(marks, cands)}, ensure_ascii=False, indent=1))
            wr(os.path.join(d, "比べる.md"), "# 比べる（1本のAIより、話し合いの方が良いか）\n\n4本のうち1本は Claude だけで書いた版、3本は3社の持ち寄りと話し合いのあとの版です。どれがどれかは伏せています。"
               "いちばん良いものと、いちばん悪いものを選んでから、_答え（見ないで）.json を開いてください。\n\n"
               + "\n\n---\n\n".join(f"# {m}\n\n" + c[1] for m, c in zip(marks, cands)))
            ans, body = None, None
        else:
            ans, body = {}, []
        for L in (labels if ans is not None else []):
            pair = [("v1", rd(os.path.join(d, L, "v1.md"))), (f"v{last}", rd(os.path.join(d, L, f"v{last}.md")))]
            random.shuffle(pair)
            ans[L] = {"甲": pair[0][0], "乙": pair[1][0]}
            body.append(f"# {L}\n\n## 甲\n\n{pair[0][1]}\n\n## 乙\n\n{pair[1][1]}")
        if ans is not None:
          wr(os.path.join(d, "_答え（見ないで）.json"), json.dumps(ans, ensure_ascii=False, indent=1))
          wr(os.path.join(d, "比べる.md"), "# 比べる（話し合いで良くなったか）\n\n各案の最初の版と最後の版を、どちらがどちらか伏せて並べています。"
           "案ごとに「甲と乙のどちらが良いか」を決めてから、_答え（見ないで）.json を開いてください。\n\n" + "\n\n---\n\n".join(body))
        os.rename(os.path.join(d, MARK), os.path.join(d, DONE))
        done.append("4. 並べた.md と 比べる.md ができました。終わりです")
        progress(d, done)
        dr.log(f"✓ 制作チーム {name}: 終わりました")
        dr.notify(f"サイトの制作チームが終わりました：{name}")
    except Exception as e:
        wr(os.path.join(d, "エラー.txt"), f"{now()}\n\n{e}\n")
        progress(d, done + [f"エラーで止まりました（エラー.txt）。次の見回りで続きから試します"])
        dr.log(f"× 制作チーム {name}: {e}")
        dr.notify(f"サイトの制作チームが止まりました：{name}")
    finally:
        pool.shutdown(wait=False)
        p = os.path.join(d, RUNNING)
        if os.path.exists(p):
            os.remove(p)


def alive(d):
    p = os.path.join(d, RUNNING)
    if not os.path.exists(p):
        return False
    m = re.search(r"PID (\d+)", rd(p))
    if m:
        try:
            os.kill(int(m.group(1)), 0)
            return True
        except ProcessLookupError:
            os.remove(p)
            return False
        except PermissionError:
            return True
    return False


def once(dry):
    if not os.path.isdir(JOBS):
        dr.log("制作/ がありません")
        return
    for name in sorted(os.listdir(JOBS)):
        d = os.path.join(JOBS, name)
        if not os.path.isdir(d):
            continue
        f = set(os.listdir(d))
        if alive(d):
            dr.log(f"制作チーム {name} が動いています")
            return
        if MARK in f and STOP not in f and "依頼.md" in f:
            run(d, dry)
            return
    dr.log("印（制作/{名前}/かける.txt）のついたものはありません")


def check():
    call = Caller()
    dr.log(f"Gemini のモデル: {call.model}")
    for who in AUTHORS:
        t0 = time.time()
        a = call.ask(who, "短く答えてください。", "「準備できました」とだけ答えてください。", HERE)
        dr.log(f"{who}: {a[:40]}（{int(time.time() - t0)}秒）")


def install():
    os.makedirs(os.path.dirname(PLIST), exist_ok=True)
    os.makedirs(cr.CONF, exist_ok=True)
    cal = "".join(f"<dict><key>Minute</key><integer>{m}</integer></dict>" for m in range(4, 60, 5))
    plist = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>{LABEL}</string>
  <key>ProgramArguments</key><array><string>{sys.executable}</string><string>{os.path.abspath(__file__)}</string><string>--once</string></array>
  <key>StartCalendarInterval</key><array>{cal}</array>
  <key>RunAtLoad</key><true/>
  <key>AbandonProcessGroup</key><true/>
  <key>WorkingDirectory</key><string>{HERE}</string>
  <key>EnvironmentVariables</key><dict>
    <key>PATH</key><string>{PATH}</string>
    <key>CODEX_HOME</key><string>{cr.CODEX_HOME}</string>
    <key>LANG</key><string>ja_JP.UTF-8</string>
  </dict>
  <key>StandardOutPath</key><string>{LOGFILE}</string>
  <key>StandardErrorPath</key><string>{LOGFILE}</string>
</dict></plist>
"""
    open(PLIST, "w").write(plist)
    subprocess.run(["launchctl", "unload", PLIST], capture_output=True)
    r = subprocess.run(["launchctl", "load", PLIST], capture_output=True, text=True)
    if r.returncode != 0:
        raise RuntimeError(f"登録できませんでした: {r.stderr.strip()}")
    print(f"登録しました。5分ごとに見回ります（いまも1回動きます）。記録: {LOGFILE}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="サイトの入口のページを、3社の AI の制作チームで作る")
    ap.add_argument("--check", action="store_true")
    ap.add_argument("--once", action="store_true")
    ap.add_argument("--dry-run", action="store_true")
    ap.add_argument("--install", action="store_true")
    a = ap.parse_args()
    try:
        if a.install:
            install()
        elif a.check:
            check()
        else:
            once(a.dry_run)
    except Exception as e:
        dr.log("×", e)
        sys.exit(1)
