#!/usr/bin/env php
<?php
/**
 * 人狼戦績 HTMLログ解析バッチ（サーバA: 非公開・解析用）
 *
 * zhtml/ の HTML を解析 → PostgreSQL に保存。
 * 初回は全件、以降は未処理分のみ差分処理。
 *
 * 使い方:
 *   php parse_logs.php                  # 差分処理
 *   php parse_logs.php --from=1         # 指定IDから開始
 *   php parse_logs.php --reset          # games/players を TRUNCATE して全件再処理
 *   php parse_logs.php --dry-run        # DB書き込みなし
 *   php parse_logs.php --limit=1000     # 最大1000件だけ処理
 *
 * Cron:
 *   every 10 minutes: cd /var/www/html/zinro && php parse_logs.php >> /var/log/zinro-parse.log 2>&1
 */
declare(strict_types=1);
mb_internal_encoding('UTF-8');

// ══════════════════════════════════════════════════
//  設定
// ══════════════════════════════════════════════════

const ZHTML_DIR = __DIR__ . '/zhtml';
const PG_HOST   = 'localhost';
const PG_PORT   = '5432';
const PG_DB     = 'zinro_meta';
const PG_USER   = 'zinro';
define('PG_PASS', getenv('PG_PASSWORD') ?: '');

// 通常配役一覧（これ以外は「カオス」）
const KNOWN_HAIYAKU = [
    '夏至配役','ルアー配役','4A','4A占','6A','6B','6C','メチ配役',
    '8CFO','8CNO（おはよーぐる配役）','9D','10A狩','10C猫','11C猫',
    '11A','12A猫','12B','12B背徳猫（ゆきうさ配役）','12W',
    '12R猫FO（ライス配役）','13Z','14D猫','16A','16D猫','17A',
    '18A猫','19D猫',
];

// ══════════════════════════════════════════════════
//  CLI
// ══════════════════════════════════════════════════

$options  = getopt('', ['from::','reset','dry-run','limit::']);
$fromOpt  = isset($options['from'])  ? max(1,(int)$options['from']) : null;
$resetFlg = isset($options['reset']);
$dryRun   = isset($options['dry-run']);
$limitOpt = isset($options['limit']) ? (int)$options['limit'] : 0;

// ══════════════════════════════════════════════════
//  DB接続
// ══════════════════════════════════════════════════

$dsn = sprintf("pgsql:host=%s;port=%s;dbname=%s", PG_HOST, PG_PORT, PG_DB);
try {
    $pdo = new PDO($dsn, PG_USER, PG_PASS, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
} catch (PDOException $e) {
    fwrite(STDERR, "DB接続失敗: {$e->getMessage()}\n");
    exit(1);
}

echo "=== 人狼戦績 ログ解析バッチ ===\n";
echo "日時: " . date('Y-m-d H:i:s') . "\n";

if ($resetFlg) {
    echo "★ RESET: TRUNCATE games/players\n";
    $pdo->exec("TRUNCATE players, games RESTART IDENTITY CASCADE");
}

// ══════════════════════════════════════════════════
//  処理範囲の決定
// ══════════════════════════════════════════════════

$existing = [];
$st = $pdo->query("SELECT DISTINCT log_id FROM games");
while ($r = $st->fetch()) $existing[(int)$r['log_id']] = true;
echo "DB内の既存log_id数: " . count($existing) . "\n";

if (!is_dir(ZHTML_DIR)) { fwrite(STDERR, "ERROR: zhtml/ なし\n"); exit(1); }

$files = glob(ZHTML_DIR . '/*.html');
$todo  = [];
foreach ($files as $f) {
    $base = basename($f, '.html');
    if (!is_numeric($base)) continue;
    $id = (int)$base;
    if ($fromOpt !== null && $id < $fromOpt) continue;
    if (isset($existing[$id])) continue;
    $todo[] = $id;
}
sort($todo);
if ($limitOpt > 0) $todo = array_slice($todo, 0, $limitOpt);

$total = count($todo);
echo "未処理: {$total}\n";
if ($total === 0) { echo "処理対象なし。\n"; exit(0); }
echo "範囲: {$todo[0]} ～ " . end($todo) . "\n\n";

// ══════════════════════════════════════════════════
//  prepared statements
// ══════════════════════════════════════════════════

$stmtGame = $pdo->prepare('
    INSERT INTO games (
        log_id, game_index, created_at, started_at, finished_at,
        village_name, haiyaku, haiyaku_class, rm_name,
        game_time, wait_time, review_time, tsuiho_json, raw_line
    ) VALUES (
        :log_id,:game_index,:created_at,:started_at,:finished_at,
        :village_name,:haiyaku,:haiyaku_class,:rm_name,
        :game_time,:wait_time,:review_time,:tsuiho_json,:raw_line
    ) ON CONFLICT (log_id, game_index) DO NOTHING
      RETURNING id
');

$stmtPlayer = $pdo->prepare('
    INSERT INTO players (
        game_id, log_id, name, trip, role, win,
        talk_count, live_days, is_spectator, tsuiho_flag, created_at
    ) VALUES (
        :game_id,:log_id,:name,:trip,:role,:win,
        :talk_count,:live_days,:is_spectator,:tsuiho_flag,:created_at
    )
');

$pdo->exec("
CREATE TABLE IF NOT EXISTS player_talks (
    id          BIGSERIAL PRIMARY KEY,
    game_id     INTEGER REFERENCES games(id) ON DELETE CASCADE,
    log_id      INTEGER NOT NULL,
    game_index  INTEGER NOT NULL DEFAULT 0,
    seq         INTEGER NOT NULL,
    created_at  BIGINT DEFAULT NULL,
    day_label   TEXT NOT NULL DEFAULT '',
    phase       TEXT NOT NULL DEFAULT '',
    from_user   TEXT NOT NULL DEFAULT '',
    to_user     TEXT NOT NULL DEFAULT '',
    name        TEXT NOT NULL DEFAULT '',
    trip        TEXT NOT NULL DEFAULT '',
    message     TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_player_talks_log_game_seq
    ON player_talks (log_id, game_index, seq);
CREATE INDEX IF NOT EXISTS idx_player_talks_trip_created
    ON player_talks (trip, created_at DESC) WHERE trip <> '';
CREATE INDEX IF NOT EXISTS idx_player_talks_name_created
    ON player_talks (name, created_at DESC) WHERE name <> '';
CREATE INDEX IF NOT EXISTS idx_player_talks_game
    ON player_talks (game_id, seq);
");

$stmtTalk = $pdo->prepare('
    INSERT INTO player_talks (
        game_id, log_id, game_index, seq, created_at,
        day_label, phase, from_user, to_user, name, trip, message
    ) VALUES (
        :game_id, :log_id, :game_index, :seq, :created_at,
        :day_label, :phase, :from_user, :to_user, :name, :trip, :message
    )
    ON CONFLICT (log_id, game_index, seq) DO NOTHING
');

// ══════════════════════════════════════════════════
//  メインループ
// ══════════════════════════════════════════════════

$processed = $inserted = $skipped = $errors = 0;
$t0 = microtime(true);

foreach ($todo as $idx => $logId) {
    $html = @file_get_contents(ZHTML_DIR . "/{$logId}.html");
    if ($html === false) { $errors++; continue; }
    if (mb_strpos($html, '指定されたログは存在しません') !== false) { $skipped++; continue; }

    $parsed = parseZinroLog($logId, $html);
    if ($parsed === null || empty($parsed['games'])) { $skipped++; continue; }

    if ($dryRun) {
        echo "[DRY] log_id={$logId} games=" . count($parsed['games']) . "\n";
        $processed++;
        continue;
    }

    try {
        $pdo->beginTransaction();
        foreach ($parsed['games'] as $g) {
            $stmtGame->execute([
                ':log_id'        => $g['log_id'],
                ':game_index'    => $g['game_index'],
                ':created_at'    => $g['created_at'],
                ':started_at'    => $g['started_at'],
                ':finished_at'   => $g['finished_at'],
                ':village_name'  => $g['village_name'],
                ':haiyaku'       => $g['haiyaku'],
                ':haiyaku_class' => $g['haiyaku_class'],
                ':rm_name'       => $g['rm_name'],
                ':game_time'     => $g['game_time'],
                ':wait_time'     => $g['wait_time'],
                ':review_time'   => $g['review_time'],
                ':tsuiho_json'   => $g['tsuiho_json'],
                ':raw_line'      => $g['raw_line'],
            ]);
            $gameIdValue = $stmtGame->fetchColumn();
            if ($gameIdValue === false) continue; // 既に同じゲームが取り込まれている
            $gameId = (int)$gameIdValue;

            foreach ($parsed['players'] as $p) {
                if ($p['log_id'] !== $g['log_id'] || $p['game_index'] !== $g['game_index']) continue;
                $stmtPlayer->execute([
                    ':game_id'      => $gameId,
                    ':log_id'       => $p['log_id'],
                    ':name'         => $p['name'],
                    ':trip'         => $p['trip'],
                    ':role'         => $p['role'],
                    ':win'          => $p['win'],
                    ':talk_count'   => $p['talk_count'],
                    ':live_days'    => $p['live_days'],
                    ':is_spectator' => $p['is_spectator'],
                    ':tsuiho_flag'  => $p['tsuiho_flag'],
                    ':created_at'   => $p['created_at'],
                ]);
            }
            foreach (($parsed['talks'] ?? []) as $t) {
                if ($t['log_id'] !== $g['log_id'] || $t['game_index'] !== $g['game_index']) continue;
                $stmtTalk->execute([
                    ':game_id'     => $gameId,
                    ':log_id'      => $t['log_id'],
                    ':game_index'  => $t['game_index'],
                    ':seq'         => $t['seq'],
                    ':created_at'  => $t['created_at'],
                    ':day_label'   => $t['day_label'],
                    ':phase'       => $t['phase'],
                    ':from_user'   => $t['from_user'],
                    ':to_user'     => $t['to_user'],
                    ':name'        => $t['name'],
                    ':trip'        => $t['trip'],
                    ':message'     => $t['message'],
                ]);
            }
        }
        $pdo->commit();
        $inserted += count($parsed['games']);
        $processed++;
    } catch (Throwable $e) {
        $pdo->rollBack();
        echo "[ERR] log_id={$logId}: {$e->getMessage()}\n";
        $errors++;
    }

    if (($idx+1) % 500 === 0) {
        $el = microtime(true) - $t0;
        echo sprintf("  ... %d/%d (%.1f件/秒, games=%d)\n", $idx+1, $total, ($idx+1)/max($el,0.1), $inserted);
    }
}

$el = microtime(true) - $t0;
echo sprintf("\n=== 完了: 処理=%d games=%d skip=%d err=%d (%.1f秒) ===\n",
    $processed, $inserted, $skipped, $errors, $el);

// ══════════════════════════════════════════════════════════
//  配役分類ヘルパ
// ══════════════════════════════════════════════════════════

function classifyHaiyaku(string $haiyaku): string
{
    if ($haiyaku === '') return 'カオス';
    return in_array($haiyaku, KNOWN_HAIYAKU, true) ? '通常' : 'カオス';
}

// ══════════════════════════════════════════════════════════
//  HTML解析（既存PHP解析ロジックベース）
// ══════════════════════════════════════════════════════════

function parseZinroLog(int $logId, string $html): ?array
{
    $gametime=$totsushi=$uranaiwolf=$uranaifox=$karyudogj=$kami
        =$shohai=$gamemachijikan=$datekansousen=$namestats=$sansens
        =$kansens=$nametrip=$jinei=$haiyakus=$arashi=$tsuiho
        =$hayagami=$miuchigami=$hatsugen=$seizonnissuu
        =$data=$comment=$jikkouuranai=$match=$jikkoukaryudo
        =$seizon=$com=$chuyacount=[];
    $talks=[];

    $chudan=0; $datemake=0;
    $muramake=$roomname=$chuya=$date=$datesakusei=$jikkoudatepre
        =$datestart=$datestop=$tmp=$jikkoudatenow=$jikkoutime=$dateend="";

    $data = explode("\n", $html);
    $j=1; $flagreg=0; $nametrip=[];

    foreach ($data as $x) {
        if ($j===77) {
            $flagreg = (trim($x)==="公開されていない村です<br />") ? 0 : 1;
        }
        if ($flagreg>0) {
            if ($j===160) {
                $line=str_replace(["\r","\n"],"",$x);
                $line=trim($line);
                $line=str_replace("var message = ","",$line);
                $line=rtrim($line,";");
                $comment=json_decode($line,true);
                if (is_array($comment) && isset($comment[0]["from_user"],$comment[0]["message"])
                    && $comment[0]["from_user"]==="鯖") {
                    if (!mb_strpos($comment[0]["message"],"村を作成しました")) {
                        krsort($comment);
                    }
                }
            }
            $flag=0;
            if ($j>210) {
                $a=explode('"',$x); $b=explode("'",$x);
                if (($a[1]??'')==="modal") { $trip=str_replace("トリップ：","",$b[1]??''); }
                if (mb_ereg("</span>",$x)) { $x="<".$x; $name=trim(strip_tags($x)); $flag=1; }
                if ($flag>0 && !empty($name)) { $nametrip[$name]=$trip??''; }
            }
        }
        $j++;
    }

    if (!$flagreg || !is_array($comment) || count($comment)===0) return null;

    // ── コメント解析 ──
    $game=-1; $c=0; $cc=0; $chudan=0; $precom=""; $anata=0; $inGameWindow=false;
    $chuya="夜"; $datestart=0; $datesakusei=0;

    foreach ($comment as $com) {
        $c++;
        if (!isset($com["from_user"]) || $com["from_user"]!=="鯖") {
            if (!$inGameWindow) {
                $fromUser=(string)($com["from_user"]??'');
                $message=(string)($com["message"]??'');
                if ($fromUser !== '' && $message !== '') {
                    try {
                        $dtTalk=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
                        $talkTs=(int)$dtTalk->format('U');
                    } catch (Throwable $e) {
                        $talkTs=null;
                    }
                    $talkGameIndex = $game >= 0 ? $game : 0;
                    $talks[]=[
                        'log_id'=>$logId,
                        'game_index'=>(int)$talkGameIndex,
                        'seq'=>(int)$c,
                        'created_at'=>$talkTs,
                        'day_label'=>'',
                        'phase'=>$game >= 0 ? 'after_game' : 'before_game',
                        'from_user'=>$fromUser,
                        'to_user'=>(string)($com["to_user"]??''),
                        'name'=>$fromUser,
                        'trip'=>(string)($nametrip[$fromUser]??''),
                        'message'=>mb_substr($message,0,2000),
                    ];
                }
            }
            // 昼＆生存中の発言カウント
            $fromUser=$com["from_user"]??'';
            if ($chuya==="昼" && ($seizon[$game][$fromUser]??0)>0) {
                $hatsugen[$game][$fromUser]=($hatsugen[$game][$fromUser]??0)+1;
            }
            // 夜の役職実行
            if ($chuya==="夜") {
                $msg=$com["message"]??'';
                if (str_contains($msg,"実行@")) {
                    $x=explode("→",str_replace("実行@","",$msg));
                    if (($namestats[$game][$x[0]]??'')==="占い師")
                        $jikkouuranai[$game][$x[1]]=$x[0];
                    if (($namestats[$game][$x[0]]??'')==="狩人")
                        $jikkoukaryudo[$game][$x[1]]=$x[0];
                    if (($namestats[$game][$x[0]]??'')==="人狼") {
                        $dt=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
                        $jikkoudatenow=$dt->format('U');
                        $jikkoutime=$jikkoudatenow-$jikkoudatepre;
                        if ($jikkoutime<50) $hayagami[$x[0]]=$jikkoutime;
                        if (($namestats[$game][$x[1]]??'')==="人狼")
                            $miuchigami[$game][$x[0]]="a";
                    }
                }
            }
            // 荒らし判定（省略 — 元コードと同等）
            continue;
        }

        // ── 鯖発言 ──
        $cc++;
        $curKey=($com["message"]??'').($com["to_user"]??'');
        if ($curKey===$precom) continue;
        $precom=$curKey;

        if ($cc===1) {
            $roomname=mb_convert_kana($com["room_name"]??'',"n");
            $datemake=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
            $datesakusei=$datemake->format('U');
        }
        if ($roomname==='') {
            $roomname=mb_convert_kana($com["room_name"]??'',"n");
            $datemake=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
            $datesakusei=$datemake->format('U');
        }

        $message=$com["message"]??'';
        $toUser=$com["to_user"]??'';

        // あなたは【 → ゲーム開始
        if (mb_ereg("あなたは【",$message)) {
            $anata++;
            $dt=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
            $jikkoudatepre=$dt->format('U');
        }
        if ($anata===1) {
            $dt=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
            $datestart=$dt->format('U');
            $datekansousen[$game]=$datestart-$datesakusei;
            $game++;
            $inGameWindow=true;
            $totsushi[$game]=0;
            for ($i=1;$i<=19;$i++) ${"y$i"}=0;
            $all=0; $allsu=0; $haiyaku=""; $chuyacount[$game]=0;
            unset($uranaifox[$game],$uranaiwolf[$game],$jikkouuranai[$game],$jikkoukaryudo[$game]);
            if (isset($seizon[$game]) && is_array($seizon[$game])) {
                foreach ($seizon[$game] as $k=>$x) {
                    $hatsugen[$game][$k]=0; $seizonnissuu[$game][$k]=0;
                }
            }
        }

        // 村作成
        if (str_contains($message,"村を作成しました")) {
            $x=explode("さんが村を作成し",$message);
            $muramake=$x[0]??'';
        }

        // 役職割り当て
        if (preg_match('/あなたは【/',$message)) {
            $yaku="---"; $jin=0;
            $roles=[
                ['人狼','人狼',1,'y1'],['狂人','狂人',1,'y2'],['狂信者','狂信者',1,'y3'],
                ['占い師','占い師',2,'y4'],['狩人','狩人',2,'y5'],['霊能者','霊能者',2,'y6'],
                ['猫又','猫又',2,'y7'],['共有者','共有者',2,'y8'],['役人','役人',2,'y9'],
                ['怪盗','怪盗',2,'y10'],['狼憑き','狼憑き',2,'y11'],['ものまね','ものまね',2,'y12'],
                ['村人','村人',2,'y13'],['妖狐','妖狐',3,'y14'],['背徳者','背徳者',3,'y15'],
                ['てるてる','てるてる',4,'y16'],['恋人','恋人',5,'y17'],
                ['パン屋','パン屋',2,'y18'],['黒猫','黒猫',1,'y19'],
            ];
            foreach ($roles as [$kw,$rn,$tid,$ctr]) {
                if (str_contains($message,"あなたは【{$kw}")) {
                    $yaku=$rn; $jin=$tid;
                    if (!isset($$ctr)) $$ctr=0;
                    $$ctr++; $all++; break;
                }
            }
            $namestats[$game][$toUser]=$yaku;
            $jinei[$game][$toUser]=$jin;
            $seizon[$game][$toUser]=1;
        }

        // ゲーム開始 → 配役確定
        if (preg_match('/ゲームを開始します/',$message)) {
            $haiyaku='';
            if (($y1??0)==1&&($y2??0)==1&&($y13??0)==2&&$all==4) $haiyaku="夏至配役";
            elseif (($y1??0)==1&&($y16??0)==16&&($y13??0)==2&&$all==4) $haiyaku="ルアー配役";
            elseif (($y1??0)==1&&($y13??0)==3&&$all==4) $haiyaku="4A";
            elseif (($y1??0)==1&&($y4??0)==1&&($y13??0)==2&&$all==4) $haiyaku="4A占";
            elseif (($y1??0)==1&&($y4??0)==1&&($y13??0)==4&&$all==6) $haiyaku="6A";
            elseif (($y1??0)==1&&($y4??0)==1&&($y13??0)==3&&($y14??0)==1&&$all==6) $haiyaku="6B";
            elseif (($y1??0)==1&&($y2??0)==3&&($y4??0)==1&&($y5??0)==1&&$all==6) $haiyaku="6C";
            elseif (($y1??0)==1&&($y2??0)==2&&($y7??0)==1&&($y14??0)==1&&($y13??0)==1&&$all==6) $haiyaku="メチ配役";
            elseif (($y1??0)==2&&($y2??0)==1&&($y4??0)==1&&($y6??0)==1&&($y8??0)==2&&($y13??0)==1&&$all==8) $haiyaku="8CFO";
            elseif (($y1??0)==2&&($y2??0)==2&&($y4??0)==2&&($y6??0)==2&&$all==8) $haiyaku="8CNO（おはよーぐる配役）";
            elseif (($y1??0)==1&&($y4??0)==1&&($y14??0)==1&&($y15??0)==2&&($y13??0)==4&&$all==9) $haiyaku="9D";
            elseif (($y1??0)==2&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y13??0)==4&&$all==10) $haiyaku="10A狩";
            elseif (($y1??0)==3&&($y2??0)==1&&($y4??0)==2&&($y5??0)==1&&($y6??0)==1&&($y14??0)==1&&($y7??0)==1&&$all==10) $haiyaku="10C猫";
            elseif (($y1??0)==3&&($y2??0)==1&&($y4??0)==2&&($y5??0)==1&&($y6??0)==1&&($y14??0)==1&&($y7??0)==1&&($y13??0)==1&&$all==11) $haiyaku="11C猫";
            elseif (($y1??0)==2&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y13??0)==5&&$all==11) $haiyaku="11A";
            elseif (($y1??0)==3&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y7??0)==1&&($y13??0)==4&&$all==12) $haiyaku="12A猫";
            elseif (($y1??0)==2&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y13??0)==5&&($y14??0)==1&&$all==12) $haiyaku="12B";
            elseif (($y1??0)==3&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y13??0)==2&&($y14??0)==1&&($y15??0)==1&&($y7??0)==1&&$all==12) $haiyaku="12B背徳猫（ゆきうさ配役）";
            elseif (($y1??0)==3&&($y3??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y7??0)==1&&($y8??0)==2&&($y14??0)==1&&($y15??0)==1&&$all==12) $haiyaku="12W";
            elseif (($y1??0)==2&&($y3??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y7??0)==1&&($y8??0)==2&&($y14??0)==1&&($y15??0)==1&&($y13??0)==1&&$all==12) $haiyaku="12R猫FO（ライス配役）";
            elseif (($y1??0)==2&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y16??0)==1&&$all==13) $haiyaku="13Z";
            elseif (($y1??0)==3&&($y3??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y7??0)==1&&($y8??0)==2&&($y14??0)==1&&($y15??0)==1&&$all==14) $haiyaku="14D猫";
            elseif (($y1??0)==3&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y8??0)==2&&($y13??0)==6&&($y14??0)==1&&$all==16) $haiyaku="16A";
            elseif (($y1??0)==4&&($y3??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y7??0)==1&&($y8??0)==2&&($y13??0)==3&&($y14??0)==1&&($y15??0)==1&&$all==16) $haiyaku="16D猫";
            elseif (($y1??0)==3&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y8??0)==2&&($y13??0)==7&&($y14??0)==1&&$all==17) $haiyaku="17A";
            elseif (($y1??0)==4&&($y2??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y7??0)==1&&($y8??0)==2&&($y13??0)==6&&($y14??0)==1&&$all==18) $haiyaku="18A猫";
            elseif (($y1??0)==4&&($y3??0)==1&&($y4??0)==1&&($y5??0)==1&&($y6??0)==1&&($y7??0)==1&&($y8??0)==2&&($y13??0)==6&&($y14??0)==1&&($y15??0)==1&&$all==19) $haiyaku="19D猫";

            $haiyakus[$game]=$haiyaku;
        }

        // 死亡
        if (str_contains($message,"が死亡しました")) {
            $x=explode("さんが死亡しました",$message);
            if (isset($x[0])) $seizon[$game][$x[0]]=0;
        }

        // 昼夜判定
        if (str_contains($message,"投票時間になりました")) $chuya="夕";
        if (str_contains($message,"朝になりました")) {
            $chuya="昼";
            $chuyacount[$game]=($chuyacount[$game]??0)+1;
            if (isset($seizon[$game])) {
                foreach ($seizon[$game] as $k=>$x) {
                    if ($x>0) $seizonnissuu[$game][$k]=($seizonnissuu[$game][$k]??0)+1;
                }
            }
        }
        if (str_contains($message,"夜になりました")) $chuya="夜";

        // 突然死
        if (str_contains($message,"突然死")) $totsushi[$game]=1;

        // 中断
        if (str_contains($message,"ゲームを中断しました")) {
            $namestats[$game]=$jinei[$game]=$seizonnissuu[$game]
                =$hatsugen[$game]=$shohai[$game]=$seizon[$game]=[];
            unset($shohai[$game]);
            $game--; $chudan++; $anata=0; $chuya="夜"; $inGameWindow=false;
        }

        // 勝敗
        if (isset($jinei[$game]) && count($jinei[$game])>3) {
            preg_match("/【.+?】/",$message,$match);
            $target=$match[0]??'';
            if (str_contains($message,"【全員】勝利")) $shohai[$game][-1]=1;
            if (str_contains($message,"全員敗北"))     $shohai[$game][0]=1;
            if (str_contains($target,"人狼")     && str_contains($message,"勝利")) $shohai[$game][1]=1;
            if (str_contains($target,"村人")     && str_contains($message,"勝利")) $shohai[$game][2]=1;
            if (str_contains($target,"妖狐")     && str_contains($message,"勝利")) $shohai[$game][3]=1;
            if (str_contains($target,"てるてる") && str_contains($message,"勝利")) $shohai[$game][4]=1;
            if (str_contains($target,"恋人")     && str_contains($message,"勝利")) $shohai[$game][5]=1;
        }

        // 勝敗決定時
        if (str_contains($message,"勝利") || str_contains($message,"敗北")) {
            $dt=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
            $datestop=$dt->format('U');
            $gametime[$game]=$datestop-$datestart;
            $gamemachijikan[$game]=$datestart-$datesakusei;
            $datesakusei=$datestop; $dateend=$datestop;
            $anata=0; $chuya="夜"; $inGameWindow=false;
        }

        // ゲーム終了
        if (str_contains($message,"ゲームを終了しました")) {
            $dt=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
            $dateend=$dt->format('U');
            $datekansousen[$game]=$dateend-$datesakusei;
            $game++;
        }

        // コメント末尾
        if ($c===count($comment)) {
            $dt=new DateTime($com["created"]??'now',new DateTimeZone('Asia/Tokyo'));
            $dateend=$dt->format('U');
            $datekansousen[$game]=$dateend-$datesakusei;
        }

        // 追放
        if (str_contains($message,"追放しました")) {
            $x=explode("さんを村から",$message);
            if (isset($x[0])) $tsuiho[$x[0]]=$x[0];
        }

        // 夜: 鯖の噛み・GJ
        if ($chuya==="夜" && $com["from_user"]==="鯖") {
            if (str_contains($message,"さんを噛みます")) {
                $x=explode("さんを噛み",$message);
                if (!isset($chuyacount[$game])||$chuyacount[$game]<0) $chuyacount[$game]=0;
                $kami[$game][$chuyacount[$game]]=$x[0]??'';
            }
            if (str_contains($message,"狩人が人狼から守った")) {
                foreach ($jikkoukaryudo[$game]??[] as $k=>$x) {
                    if (($kami[$game][$chuyacount[$game]]??'')===$k) {
                        $karyudogj[$game][$x][]=$k;
                        $kami[$game][$chuyacount[$game]]="";
                    }
                }
            }
            if (str_contains($message,"妖狐のため占わ")) {
                $x=explode("さんは妖狐のた",$message);
                $target=$x[0]??'';
                foreach ($jikkouuranai[$game]??[] as $kk=>$xx) {
                    if ($kk===$target) $uranaifox[$game][$kk]=$xx;
                }
            }
            if (str_contains($message,"さんは人狼です")) {
                $x=explode("さんは人狼です",$message);
                $target=$x[0]??'';
                foreach ($jikkouuranai[$game]??[] as $kk=>$xx) {
                    if ($kk===$target) $uranaiwolf[$game][$kk]=$xx;
                }
            }
        }

        // 村名正規化
        $roomParts=explode("-",$roomname);
        if (count($roomParts)>1) { unset($roomParts[count($roomParts)-1]); $roomname=implode("-",$roomParts); }
        $roomname=mb_convert_kana($roomname,"a");
        $roomname=preg_replace([
            "/その[0-9]+$/u","/Part[0-9]+$/u","/part[0-9]+$/u",
            "/ー[0-9]+$/u","/-[0-9]+$/u","/ｰ[0-9]+$/u",
            "/【[0-9]+】$/u","/（[0-9]+）$/u","/\([0-9]+\)$/u","/[0-9]+$/u",
        ],"",$roomname);
        $roomname=preg_replace("/【[0-9]+こめ】$/u","【Nこめ】",$roomname);
        $roomname=preg_replace("/【act\.[0-9]+】$/u","【act.N】",$roomname);
        $repls=["蛸ロード"=>"蛸ロード","unity"=>"UNITY","UNITY"=>"UNITY",
            "マターリ"=>"マターリマターリ村","深夜食堂"=>"深夜食堂",
            "闇に隠れし世界★"=>"闇に隠れし世界★Nの巻",
            "チョコレート村"=>"チョコレート村N枚目"];
        foreach ($repls as $kw=>$sn) {
            if (mb_stripos($roomname,$kw)!==false) { $roomname=$sn; break; }
        }
    }

    // ── 結果組み立て ──
    $games=[]; $players=[];
    $createdTs=($datemake instanceof DateTime)?$datemake->getTimestamp():(int)$datesakusei;

    foreach ($shohai as $k=>$x) {
        if (!isset($x)) continue;
        if (($totsushi[$k]??0)>0) continue;
        // 配役が空のときは「カオス」として保存する（以前はスキップしていた）
        $haiyakuStr=(string)($haiyakus[$k]??'');
        $haiyakuClass=classifyHaiyaku($haiyakuStr);
        if ($haiyakuStr==='') $haiyakuStr='不明';

        $regname=[]; $regtrip=[];
        foreach ($jinei[$k]??[] as $kk=>$xx) {
            if ($kk==="初日犠牲者") continue;
            $role=$namestats[$k][$kk]??'';
            $trip=$nametrip[$kk]??'';
            $talk=$hatsugen[$k][$kk]??0;
            $survived=$seizonnissuu[$k][$kk]??0;
            $winflag=(isset($x[$xx])&&$x[$xx]==1)?1:0;
            $regname[$kk]="{$winflag}φ{$role}φ{$trip}φ{$talk}φ{$survived}φ";
            $regtrip[$trip]="{$winflag}φ{$role}φ{$kk}φ{$talk}φ{$survived}φ";
        }

        $tsuihoJson=json_encode($tsuiho,JSON_UNESCAPED_UNICODE);
        $gameTime=(int)($gametime[$k]??0);
        $waitTime=(int)($gamemachijikan[$k]??0);
        $reviewTime=(int)($datekansousen[$k]??0);
        $startedAt=$createdTs+$waitTime;
        $finishedAt=$startedAt+$gameTime;

        $rawLine=implode("<>",[(string)$logId,(string)$roomname,(string)$createdTs,
            $haiyakuStr,json_encode($regtrip,JSON_UNESCAPED_UNICODE),
            json_encode($regname,JSON_UNESCAPED_UNICODE),(string)$k,(string)$gameTime,
            (string)$waitTime,(string)$reviewTime,(string)$muramake,$tsuihoJson])."<>\n";

        $games[]=[
            'log_id'=>$logId,'game_index'=>(int)$k,'created_at'=>$createdTs,
            'started_at'=>$startedAt,'finished_at'=>$finishedAt,
            'village_name'=>$roomname,'haiyaku'=>$haiyakuStr,'haiyaku_class'=>$haiyakuClass,
            'rm_name'=>(string)$muramake,'game_time'=>$gameTime,'wait_time'=>$waitTime,
            'review_time'=>$reviewTime,'tsuiho_json'=>$tsuihoJson,'raw_line'=>$rawLine,
        ];

        foreach ($regname as $name=>$payload) {
            $parts=explode('φ',$payload);
            if (count($parts)<5) continue;
            $players[]=[
                'log_id'=>$logId,'game_index'=>(int)$k,
                'name'=>$name,'trip'=>(string)($parts[2]??''),
                'role'=>(string)($parts[1]??''),'win'=>(int)($parts[0]??0),
                'talk_count'=>(int)($parts[3]??0),'live_days'=>(int)($parts[4]??0),
                'is_spectator'=>0,'tsuiho_flag'=>isset($tsuiho[$name])?1:0,
                'created_at'=>$createdTs,
            ];
        }
    }

    return empty($games) ? null : ['games'=>$games,'players'=>$players,'talks'=>$talks];
}
