<?php
/**
 * wiki_api.php
 * 人狼戦績 — ログイン / Wiki / コメント / プロフィール PHP直結 API
 *
 * Nginx が /api.php を Docker にプロキシしている環境でも、
 * このファイルは直接 PHP で動作します。
 *
 * DB: PostgreSQL（接続情報は環境変数で指定）
 *
 * ── エンドポイント (?mode=xxx) ──────────────────────────────────────────────
 *  login          POST  トリップキー → トリップ生成 → DB確認 → JWT発行
 *  logout         POST  サーバ側は何もしない（クライアントがトークン破棄）
 *  me             GET   JWT検証 → trip / name を返す
 *  wiki_get       GET   Wiki本文取得 (key_type + key_value)
 *  wiki_save      POST  Wiki保存 (要JWT)
 *  comment_list   GET   コメント一覧 (key_type + key_value)
 *  comment_add    POST  コメント追加 (要JWT)
 *  profile_get    GET   プロフィール取得 (trip)
 *  profile_update POST  プロフィール更新 (要JWT・自分のtripのみ)
 * ────────────────────────────────────────────────────────────────────────────
 *
 * セットアップ:
 *   1. このファイルを index.php と同じディレクトリに配置
 *   2. 必要なら下記の JWT_SECRET を環境変数 SESSION_SECRET で上書き
 *      export SESSION_SECRET="ランダムな32文字以上の文字列"
 *   3. php-pgsql / php-mbstring が有効であること
 *   4. Nginx で Authorization ヘッダーを PHP に渡す場合は
 *      fastcgi_param HTTP_AUTHORIZATION $http_authorization; を追記
 */

declare(strict_types=1);
// DOKASEN_ANALYTICS_V1
if (is_file(__DIR__ . '/../analytics/binding.php')) {
    require_once __DIR__ . '/../analytics/binding.php';
    require_once __DIR__ . '/../analytics/auth-audit.php';
}

// PHP 警告・エラーが JSON レスポンスに混入しないようバッファリング
ob_start();
// 画面へのエラー出力を抑制（ログには残る）
ini_set('display_errors', '0');

// ── CORS & Content-Type ──────────────────────────────────────────────────────
header('Content-Type: application/json; charset=UTF-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }

// ── 設定 ─────────────────────────────────────────────────────────────────────
// PHP-FPMはDocker用の.envを自動では読み込まないため、未設定値だけ補完する。
function loadLocalEnv(string $path): void
{
    $allowed = ['DB_DSN', 'DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASS', 'DB_SSLMODE', 'SESSION_SECRET'];
    $lines = @file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if ($lines === false) return;
    foreach ($lines as $line) {
        $line = trim($line);
        if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) continue;
        [$key, $value] = explode('=', $line, 2);
        $key = trim($key);
        if (!in_array($key, $allowed, true)) continue;
        if (($current = getenv($key)) !== false && $current !== '') continue;
        $value = trim($value);
        if (strlen($value) >= 2 && (($value[0] === '"' && $value[-1] === '"') || ($value[0] === "'" && $value[-1] === "'"))) {
            $value = substr($value, 1, -1);
        }
        putenv($key . '=' . $value);
        $_ENV[$key] = $value;
    }
}
loadLocalEnv(__DIR__ . '/.env');

$dbHost = getenv('DB_HOST') ?: 'localhost';
$dbPort = getenv('DB_PORT') ?: '5432';
$dbName = getenv('DB_NAME') ?: 'zinro_meta';
$dbSslMode = getenv('DB_SSLMODE') ?: '';
$dbDsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $dbHost, $dbPort, $dbName);
if ($dbSslMode !== '') $dbDsn .= ';sslmode=' . $dbSslMode;

define('DB_DSN',     getenv('DB_DSN') ?: $dbDsn);
define('DB_USER',    getenv('DB_USER') ?: 'zinro');
define('DB_PASS',    getenv('DB_PASS') ?: '');
// SESSION_SECRET 環境変数があれば使用（docker-compose の .env から読む等）
define('JWT_SECRET', getenv('SESSION_SECRET') ?: 'CHANGE-ME-IN-PRODUCTION');
define('JWT_EXPIRE', 86400 * 30); // 30日

// ── DB接続（遅延シングルトン）────────────────────────────────────────────────
function getDB(): PDO
{
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO(DB_DSN, DB_USER, DB_PASS, [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    }
    return $pdo;
}

// ── レスポンスヘルパー ────────────────────────────────────────────────────────
function ok(array $data): void
{
    ob_end_clean(); // PHP 警告などをバッファから破棄
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}
function err(string $msg, int $code = 400): void
{
    if (($_GET['mode'] ?? '') === 'login' && function_exists('dok_auth_audit')) {
        dok_auth_audit('AUTH_LOGIN_FAILURE', $GLOBALS['dok_attempt_account'] ?? null, null, 'invalid_credentials');
    }
    ob_end_clean();
    http_response_code($code);
    echo json_encode(['error' => $msg], JSON_UNESCAPED_UNICODE);
    exit;
}

// ── JWT ───────────────────────────────────────────────────────────────────────
function b64u_enc(string $data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function b64u_dec(string $data): string
{
    return base64_decode(strtr($data, '-_', '+/'));
}
function jwt_sign(array $payload): string
{
    $h = b64u_enc((string)json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
    $p = b64u_enc((string)json_encode($payload));
    $s = b64u_enc(hash_hmac('sha256', "$h.$p", JWT_SECRET, true));
    return "$h.$p.$s";
}
function jwt_verify(string $token): ?array
{
    $parts = explode('.', $token);
    if (count($parts) !== 3) return null;
    [$h, $p, $s] = $parts;
    $expected = b64u_enc(hash_hmac('sha256', "$h.$p", JWT_SECRET, true));
    if (!hash_equals($expected, $s)) return null;
    $payload = json_decode(b64u_dec($p), true);
    if (!$payload) return null;
    if (isset($payload['exp']) && $payload['exp'] < time()) return null;
    return $payload;
}
function get_auth(): ?array
{
    $h = '';

    // 1. getallheaders() — Apache / PHP built-in server
    if (function_exists('getallheaders')) {
        foreach (getallheaders() as $k => $v) {
            if (strtolower($k) === 'authorization') { $h = $v; break; }
        }
    }

    // 2. $_SERVER['HTTP_AUTHORIZATION'] — 標準 CGI/FPM
    if (!$h) $h = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

    // 3. $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] — mod_rewrite 経由
    if (!$h) $h = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';

    // 4. Apache の場合 Authorization が HTTP_ で来ないことがある
    if (!$h) $h = $_SERVER['Authorization'] ?? '';

    if (!$h || !preg_match('/^Bearer\s+(.+)$/i', $h, $m)) return null;
    return jwt_verify(trim($m[1]));
}

// ── トリップ生成（2ch / 人狼オンライン方式）─────────────────────────────────────
/**
 * $raw_key: ユーザーが入力した生キー（# なし、または #付き、または ##付き）
 * 戻り値: ◆xxxxx 形式のトリップ文字列
 */
function generate_trip(string $raw_key): string
{
    // api.php の calc_trip_from_key() と同一ロジック
    // 12文字以上のキー → SHA1 base64方式 → ◆+12桁（12桁トリップ対応）
    // 12文字未満のキー → DES crypt方式  → ◆+10桁（通常トリップ）

    $key = trim(str_replace('＃', '#', $raw_key));
    if ($key === '') return '';

    // key / #key / name#key / ##secure / name##secure を同じ規則で解釈
    if (str_starts_with($key, '##')) {
        $key = substr($key, 1);
    } elseif (str_starts_with($key, '#')) {
        $key = substr($key, 1);
    } elseif (($pos = strpos($key, '##')) !== false) {
        $key = '#' . substr($key, $pos + 2);
    } elseif (($pos = strpos($key, '#')) !== false) {
        $key = substr($key, $pos + 1);
    }
    if ($key === '') return '';

    $sjis = @mb_convert_encoding($key, 'SJIS', 'UTF-8') ?: $key;

    if (strlen($key) >= 12) {
        // 12桁トリップ: SHA1 → base64 → 先頭12文字
        $trip = substr(base64_encode(sha1($sjis, true)), 0, 12);
        $trip = str_replace(['+', '/'], ['.', '_'], $trip);
    } else {
        // 通常トリップ: DES crypt方式
        $salt_raw = substr($sjis . 'H.', 1, 2);
        $salt = '';
        for ($i = 0; $i < 2; $i++) {
            $c = isset($salt_raw[$i]) ? ord($salt_raw[$i]) : ord('.');
            if ($c < ord('.') || $c > ord('z'))        $c = ord('.');
            elseif ($c >= ord(':') && $c <= ord('@'))   $c = $c - ord(':') + ord('A');
            elseif ($c >= ord('[') && $c <= ord('`'))   $c = $c - ord('[') + ord('a');
            $salt .= chr($c);
        }
        $trip = substr(crypt($sjis, $salt), -10);
    }

    return '◆' . $trip;
}

// ── リクエストボディ取得 ──────────────────────────────────────────────────────
function get_body(): array
{
    $ct = $_SERVER['CONTENT_TYPE'] ?? '';
    if (str_contains($ct, 'application/json')) {
        return (array)(json_decode((string)file_get_contents('php://input'), true) ?? []);
    }
    return $_POST;
}

// ── ルーティング ──────────────────────────────────────────────────────────────
$mode = $_GET['mode'] ?? get_body()['mode'] ?? '';

switch ($mode) {
    case 'login':          handle_login();          break;
    case 'logout':
        $dokAccount = get_auth();
        if ($dokAccount && function_exists('dok_auth_audit')) dok_auth_audit('AUTH_LOGOUT', $dokAccount['trip'] ?? null, 'logout');
        ok(['ok' => true]); break;
    case 'me':             handle_me();              break;
    case 'wiki_get':       handle_wiki_get();        break;
    case 'wiki_save':      handle_wiki_save();       break;
    case 'comment_list':   handle_comment_list();    break;
    case 'comment_add':    handle_comment_add();     break;
    case 'profile_get':    handle_profile_get();     break;
    case 'profile_update': handle_profile_update();  break;
    default:               err('unknown mode', 404);
}

// ─────────────────────────────────────────────────────────────────────────────
// ハンドラー
// ─────────────────────────────────────────────────────────────────────────────

/**
 * login
 * POST { trip_key: string, name: string }
 * → { token, trip, name }
 */
function handle_login(): void
{
    $body     = get_body();
    $trip_key = trim((string)($body['trip_key'] ?? ''));
    $name     = trim((string)($body['name']     ?? ''));

    if ($name === '') err('名前を入力してください');
    if ($trip_key === '') err('トリップキーを入力してください');

    $trip = generate_trip($trip_key);
    if ($trip === '' || $trip === '◆') err('トリップを生成できませんでした');
    $GLOBALS['dok_attempt_account'] = $trip;

    $pdo  = getDB();

    // DB にトリップが存在するか確認
    $stmt = $pdo->prepare(
        'SELECT COUNT(*) FROM players WHERE trip = ? AND is_spectator = 0'
    );
    $stmt->execute([$trip]);
    if ((int)$stmt->fetchColumn() === 0) {
        err(
            'このトリップはデータベースに存在しません。' .
            '戦績が1件以上あるトリップのみログイン可能です。' .
            '（生成されたトリップ: ' . $trip . '）',
            401
        );
    }

    $token = jwt_sign([
        'trip' => $trip,
        'name' => $name,
        'iat'  => time(),
        'exp'  => time() + JWT_EXPIRE,
    ]);

    if (function_exists('dok_auth_audit')) {
        dok_auth_audit('AUTH_LOGIN_SUCCESS', $trip, 'login_success');
        dok_auth_audit('AUTH_SESSION_CREATED', $trip);
    }

    ok(['token' => $token, 'trip' => $trip, 'name' => $name]);
}

/**
 * me
 * GET (Authorization: Bearer <token>)
 * → { trip, name }
 */
function handle_me(): void
{
    $auth = get_auth();
    if (!$auth) {
        // デバッグ用: Authorization ヘッダーが届いているか確認
        // （本番で不要なら削除してOK。ブラウザの Network タブで確認できます）
        $dbg_auth_present = !empty($_SERVER['HTTP_AUTHORIZATION'])
            || !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])
            || (function_exists('getallheaders') && isset(getallheaders()['Authorization']));
        err('未ログイン（Authorization ヘッダー ' . ($dbg_auth_present ? '検出済み→JWT検証失敗' : '未検出→Nginx設定要確認') . '）', 401);
    }
    ok(['trip' => $auth['trip'], 'name' => $auth['name']]);
}

/**
 * wiki_get
 * GET ?key_type=trip&key_value=◆xxxx
 * → { body, updated_at_text }
 */
function handle_wiki_get(): void
{
    $key_type  = trim($_GET['key_type']  ?? '');
    $key_value = trim($_GET['key_value'] ?? '');
    if ($key_type === '' || $key_value === '') err('key_type / key_value が必要です');

    $pdo  = getDB();
    $stmt = $pdo->prepare(
        'SELECT body, updated_at_text FROM wiki WHERE key_type = ? AND key_value = ?'
    );
    $stmt->execute([$key_type, $key_value]);
    $row = $stmt->fetch();

    ok($row ?: ['body' => '', 'updated_at_text' => '']);
}

/**
 * wiki_save
 * POST { key_type, key_value, body }  (要JWT)
 * → { ok: true, updated_at_text }
 */
function handle_wiki_save(): void
{
    $auth = get_auth();
    if (!$auth) err('ログインが必要です', 401);

    $body      = get_body();
    $key_type  = trim((string)($body['key_type']  ?? ''));
    $key_value = trim((string)($body['key_value'] ?? ''));
    $wiki_body = (string)($body['body'] ?? '');

    if ($key_type === '' || $key_value === '') err('key_type / key_value が必要です');
    if (mb_strlen($wiki_body) > 10000) err('Wiki本文は10,000文字以内にしてください');

    $key      = $key_type . ':' . $key_value;
    $now      = time();
    $now_text = date('Y/m/d H:i', $now);

    $pdo = getDB();
    $pdo->prepare(
        'INSERT INTO wiki (key, key_type, key_value, body, updated_at, updated_at_text)
         VALUES (?, ?, ?, ?, ?, ?)
         ON CONFLICT (key_type, key_value) DO UPDATE
           SET body           = EXCLUDED.body,
               updated_at     = EXCLUDED.updated_at,
               updated_at_text= EXCLUDED.updated_at_text'
    )->execute([$key, $key_type, $key_value, $wiki_body, $now, $now_text]);

    // 編集履歴を保存
    $pdo->prepare(
        'INSERT INTO wiki_hist (key_type, key_value, body, editor_name, editor_trip, created_at)
         VALUES (?, ?, ?, ?, ?, ?)'
    )->execute([$key_type, $key_value, $wiki_body, $auth['name'], $auth['trip'], $now]);

    ok(['ok' => true, 'updated_at_text' => $now_text]);
}

/**
 * comment_list
 * GET ?key_type=trip&key_value=◆xxxx
 * → { comments: [ { id, body, author_name, author_trip, created_at_text }, ... ] }
 */
function handle_comment_list(): void
{
    $key_type  = trim($_GET['key_type']  ?? '');
    $key_value = trim($_GET['key_value'] ?? '');
    if ($key_type === '' || $key_value === '') err('key_type / key_value が必要です');

    $pdo  = getDB();
    $stmt = $pdo->prepare(
        'SELECT id, body, author_name, author_trip, created_at
           FROM comments
          WHERE key_type = ? AND key_value = ? AND deleted = 0
          ORDER BY created_at ASC
          LIMIT 200'
    );
    $stmt->execute([$key_type, $key_value]);
    $rows = $stmt->fetchAll();

    foreach ($rows as &$r) {
        $r['created_at_text'] = date('Y/m/d H:i', (int)$r['created_at']);
    }
    unset($r);

    ok(['comments' => $rows]);
}

/**
 * comment_add
 * POST { key_type, key_value, body }  (要JWT)
 * → { ok: true }
 */
function handle_comment_add(): void
{
    $auth = get_auth();
    if (!$auth) err('ログインが必要です', 401);

    $body      = get_body();
    $key_type  = trim((string)($body['key_type']  ?? ''));
    $key_value = trim((string)($body['key_value'] ?? ''));
    $text      = trim((string)($body['body']      ?? ''));

    if ($key_type === '' || $key_value === '') err('key_type / key_value が必要です');
    if ($text === '') err('コメント本文が必要です');
    if (mb_strlen($text) > 1000) err('コメントは1,000文字以内にしてください');

    $pdo = getDB();
    $pdo->prepare(
        'INSERT INTO comments
           (key_type, key_value, body, author_name, author_trip, created_at, deleted)
         VALUES (?, ?, ?, ?, ?, ?, 0)'
    )->execute([$key_type, $key_value, $text, $auth['name'], $auth['trip'], time()]);

    ok(['ok' => true]);
}

/**
 * profile_get
 * GET ?trip=◆xxxx
 * → { trip, self_intro, x_id, x_display }
 */
function handle_profile_get(): void
{
    $trip = trim($_GET['trip'] ?? '');
    if ($trip === '') err('trip が必要です');

    $pdo  = getDB();
    $stmt = $pdo->prepare('SELECT * FROM player_profile WHERE trip = ?');
    $stmt->execute([$trip]);
    $row = $stmt->fetch();

    ok($row ?: ['trip' => $trip, 'self_intro' => '', 'x_id' => null, 'x_display' => null]);
}

/**
 * profile_update
 * POST { self_intro?, x_id?, x_display? }  (要JWT)
 * → { ok: true }
 */
function handle_profile_update(): void
{
    $auth = get_auth();
    if (!$auth) err('ログインが必要です', 401);

    $body       = get_body();
    $self_intro = trim((string)($body['self_intro'] ?? ''));
    $x_id       = trim((string)($body['x_id']       ?? '')) ?: null;
    $x_display  = trim((string)($body['x_display']  ?? '')) ?: null;

    if (mb_strlen($self_intro) > 500) err('自己紹介は500文字以内にしてください');

    // @ 除去
    if ($x_id !== null) $x_id = ltrim($x_id, '@');

    $pdo = getDB();
    $pdo->prepare(
        'INSERT INTO player_profile (trip, self_intro, x_id, x_display, updated_at)
         VALUES (?, ?, ?, ?, ?)
         ON CONFLICT (trip) DO UPDATE
           SET self_intro = EXCLUDED.self_intro,
               x_id       = EXCLUDED.x_id,
               x_display  = EXCLUDED.x_display,
               updated_at = EXCLUDED.updated_at'
    )->execute([$auth['trip'], $self_intro, $x_id, $x_display, time()]);

    ok(['ok' => true]);
}
