#!/usr/bin/env php
<?php
/**
 * 人狼戦績 スクレイピング＆解析バッチ（サーバB: 公開API用）
 *
 * zinro.net からゲームログを取得→解析→PostgreSQL に保存。
 * HTML ファイルは保存しない（容量節約）。
 * 10分ごとに cron で実行する想定。
 *
 * 使い方:
 *   php scrape_batch.php              # 通常（差分取得）
 *   php scrape_batch.php --from=12345 # 指定IDから開始
 *
 * Cron（root の crontab に追加）:
 *   0,10,20,30,40,50 * * * * cd /var/www/html/zinro/jinro-api && php scrape_batch.php >> /var/log/zinro-scrape.log 2>&1
 */
declare(strict_types=1);
mb_internal_encoding('UTF-8');

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

const ZINRO_LOG_URL      = 'https://zinro.net/m/log.php?id=';
const ZINRO_ROOMLIST_URL = 'https://zinro.net/m/room_list.php?scene=%E7%B5%82%E4%BA%86';
const ZINRO_UA           = 'SaPuRi - Kaiseki-Senseki';
const SLEEP_USEC         = 2000000; // 2秒

const PG_HOST = 'localhost';
const PG_PORT = '5432';
const PG_DB   = 'zinro_meta';
const PG_USER = 'zinro';

/**
 * Cron は Docker 用 .env を自動では読み込まないため、PG_PASSWORD が未設定なら
 * 同じディレクトリの DATABASE_URL からパスワードだけを取得する。
 */
function resolvePgPassword(): string
{
    $password = getenv('PG_PASSWORD');
    if (is_string($password) && $password !== '') {
        return $password;
    }

    $envPath = __DIR__ . '/.env';
    $lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if ($lines === false) {
        return '';
    }
    foreach ($lines as $line) {
        if (str_starts_with(ltrim($line), '#') || !str_starts_with($line, 'DATABASE_URL=')) {
            continue;
        }
        $url = trim(substr($line, strlen('DATABASE_URL=')), " \t\n\r\0\x0B\"'");
        $parts = parse_url($url);
        if (is_array($parts) && isset($parts['pass'])) {
            return rawurldecode((string)$parts['pass']);
        }
    }
    return '';
}

define('PG_PASS', resolvePgPassword());

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::']);
$fromOpt = isset($options['from']) ? max(1, (int)$options['from']) : null;

// ══════════════════════════════════════════════════
//  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);
}

// 実行ユーザーが異なる cron と手動実行の双方で共有できるDBロックを使う。
if (!$pdo->query("SELECT pg_try_advisory_lock(1443001)")->fetchColumn()) {
    echo '[' . date('Y-m-d H:i:s') . "] 別のスクレイピング処理が実行中のため終了\n";
    exit(0);
}

// ══════════════════════════════════════════════════
//  開始ID決定
// ══════════════════════════════════════════════════

if ($fromOpt !== null) {
    $fromId = $fromOpt;
} else {
    $stmt = $pdo->query("SELECT COALESCE(MAX(log_id), 0) FROM games");
    $fromId = (int)$stmt->fetchColumn() + 1;
}

// ══════════════════════════════════════════════════
//  終了ID取得（zinro.net の終了ページから）
// ══════════════════════════════════════════════════

$toId = fetchLatestLogIdFromRoomList();
if ($toId === null) {
    // 取得元を正常確認できなかった場合は「確認日時」を進めない。
    exit(0);
}

// 新規ログの有無とは独立して、取得元を正常確認できた時刻を保持する。
// サイトでは最新ゲーム日時（更新日時）とこの時刻（確認日時）を併記する。
$checkedAt = time();
$pdo->exec("CREATE TABLE IF NOT EXISTS batch_state (key TEXT PRIMARY KEY, value_int BIGINT NOT NULL DEFAULT 0, value_text TEXT NOT NULL DEFAULT '', updated_at BIGINT NOT NULL DEFAULT 0)");
$checkedStmt = $pdo->prepare("
    INSERT INTO batch_state (key, value_int, value_text, updated_at)
    VALUES ('last_crawl_checked_at', :latest_log_id, :detail, :checked_at)
    ON CONFLICT (key) DO UPDATE SET
        value_int = EXCLUDED.value_int,
        value_text = EXCLUDED.value_text,
        updated_at = EXCLUDED.updated_at
");
$checkedStmt->execute([
    ':latest_log_id' => $toId,
    ':detail' => 'zinro.net room_list latest_log_id=' . $toId,
    ':checked_at' => $checkedAt,
]);

if ($toId < $fromId) {
    // 新しいログなし
    exit(0);
}

echo "[" . date('Y-m-d H:i:s') . "] スクレイピング開始: {$fromId} ～ {$toId}\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
    )
');

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

$processed = 0;
$inserted = 0;

for ($id = $fromId; $id <= $toId; $id++) {

    // 既に処理済みか確認
    $chk = $pdo->prepare("SELECT 1 FROM games WHERE log_id = :id LIMIT 1");
    $chk->execute([':id' => $id]);
    if ($chk->fetchColumn()) { continue; }

    $html = fetchLogHtml($id);
    if ($html === null) { usleep(SLEEP_USEC); continue; }
    if (mb_strpos($html, '指定されたログは存在しません') !== false) { usleep(SLEEP_USEC); continue; }

    $parsed = parseZinroLog($id, $html);
    if ($parsed === null || empty($parsed['games'])) { usleep(SLEEP_USEC); 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'],
                ]);
            }
        }
        $pdo->commit();
        $inserted += count($parsed['games']);
        $processed++;
        echo "  log_id={$id} OK (games=" . count($parsed['games']) . ")\n";
    } catch (Throwable $e) {
        $pdo->rollBack();
        echo "  log_id={$id} ERR: {$e->getMessage()}\n";
    }

    usleep(SLEEP_USEC);
}

echo "[" . date('Y-m-d H:i:s') . "] 完了: 処理={$processed} games={$inserted}\n";

// ══════════════════════════════════════════════════
//  関数群
// ══════════════════════════════════════════════════

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

function fetchLatestLogIdFromRoomList(): ?int {
    $ctx = stream_context_create(['http' => [
        'method' => 'GET',
        'header' => "User-Agent: " . ZINRO_UA,
        'timeout' => 10,
    ]]);
    $lines = @file(ZINRO_ROOMLIST_URL, 0, $ctx);
    if (!$lines) return null;
    foreach ($lines as $line) {
        if (strpos($line, 'confirmDialog') !== false && preg_match('/id=(\d+)/', $line, $m)) {
            return (int)$m[1];
        }
    }
    return null;
}

function fetchLogHtml(int $id): ?string {
    $ch = curl_init(ZINRO_LOG_URL . $id);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 15,
        CURLOPT_USERAGENT => ZINRO_UA,
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    return ($body !== false && $code < 400) ? $body : null;
}

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

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=[];
    $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=[];

    // zinro.net 側のHTMLテンプレート変更で行番号は変動するため、
    // ログ本体の JavaScript 変数をHTML全体から検索する。
    if (preg_match('/^\s*var\s+message\s*=\s*(\[.*\]);\s*$/m', $html, $messageMatch)) {
        $comment=json_decode($messageMatch[1], true);
        if (is_array($comment) && isset($comment[0]["from_user"], $comment[0]["message"])
            && $comment[0]["from_user"] === "鯖") {
            if (!str_contains($comment[0]["message"], "村を作成しました")) krsort($comment);
            $flagreg=1;
        }
    }
    foreach ($data as $x) {
        if ($flagreg>0) {
            $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;$chuya="夜";$datestart=0;$datesakusei=0;

    foreach ($comment as $com) {
        $c++;
        if (!isset($com["from_user"])||$com["from_user"]!=="鯖") {
            $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++;
            $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="夜";
        }

        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="夜";
        }

        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]; }

        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);$t=$x[0]??'';foreach($jikkouuranai[$game]??[] as $kk=>$xx){if($kk===$t)$uranaifox[$game][$kk]=$xx;} }
            if(str_contains($message,"さんは人狼です")) { $x=explode("さんは人狼です",$message);$t=$x[0]??'';foreach($jikkouuranai[$game]??[] as $kk=>$xx){if($kk===$t)$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=[];
        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}φ";
        }

        $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;

        $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'=>''];

        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];
}
