<?php

declare(strict_types=1);

// REVIEW DRAFT: no HTTP or DB access without approval of this exact SHA.
if ((string)getenv('CDXARTICLE2_EXECUTE') !== 'ROOT_APPROVED_EXACT_SHA'
    || ($argv[1] ?? '') !== '--execute-article2') {
    fwrite(STDERR, "Review draft; execution disabled\n");
    exit(2);
}

$phase = 'runtime-identity';
$reason = 'none';
set_exception_handler(static function (Throwable $error) use (&$phase, &$reason): void {
    $allowedPhases = [
        'runtime-identity', 'source-digest', 'isolation-boundary',
        'db-context', 'db-connect', 'snapshot-before',
        'http-get-cate', 'http-list', 'snapshot-after',
    ];
    $safePhase = in_array($phase, $allowedPhases, true) ? $phase : 'unknown';
    $allowedReasons = [
        'none', 'file', 'type', 'port', 'host', 'schema',
        'user', 'password', 'selected-field',
    ];
    $safeReason = in_array($reason, $allowedReasons, true)
        ? $reason : 'unknown';
    fwrite(STDERR, 'ARTICLE2_GATE_FAILED phase=' . $safePhase
        . ' reason=' . $safeReason
        . ' ' . get_class($error)
        . ' code=' . (string)$error->getCode() . PHP_EOL);
    exit(2);
});

$lane = (string)getenv('CDXARTICLE2_LANE');
$root = (string)getenv('POC_APP_ROOT');
$base = rtrim((string)getenv('POC_BASE_URL'), '/');
$allowed = [
    'A' => ['/tmp/zhct-http-store-a.l5omH8', 'http://127.0.0.1:18097', 'mysql'],
    'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 'mysql'],
    'C' => ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', 'dm'],
];
if (!isset($allowed[$lane])
    || $root !== $allowed[$lane][0]
    || $base !== $allowed[$lane][1]
    || (string)getenv('POC_DIALECT') !== $allowed[$lane][2]
    || is_link($root)) {
    throw new RuntimeException('Approved Store isolation lane required');
}
if ((string)getenv('CDXARTICLE2_TOKEN') !== ''
    || (string)getenv('HTTP_COOKIE') !== '') {
    throw new RuntimeException('Anonymous batch must have no token/cookie');
}
$pid = (string)getenv('POC_RUNTIME_PID');
if (!ctype_digit($pid) || !is_file('/proc/' . $pid . '/cmdline')) {
    throw new RuntimeException('Runtime PID gate failed');
}
$cmdline = (string)file_get_contents('/proc/' . $pid . '/cmdline');
$port = (string)parse_url($base, PHP_URL_PORT);
if (strpos($cmdline, '127.0.0.1:' . $port) === false
    || strpos($cmdline, $root . '/public') === false) {
    throw new RuntimeException('Runtime port/docroot mismatch');
}

$phase = 'source-digest';
$sources = [
    'application/route.php',
    'application/base/AdminController.php',
    'application/p/controller/Article.php',
    'application/p/logic/Article.php',
    'application/model/Article.php',
    'application/model/ArticleCate.php',
];
$hashes = [];
foreach ($sources as $source) {
    $path = $root . '/' . $source;
    if (!is_file($path) || is_link($path)) {
        throw new RuntimeException('Source file gate failed');
    }
    $hashes[] = hash_file('sha256', $path);
}
if ((string)getenv('CDXARTICLE2_SOURCE_DIGEST')
    !== hash('sha256', implode('', $hashes))) {
    throw new RuntimeException('Reviewed source digest mismatch');
}

$phase = 'isolation-boundary';
$boundaryBody = @file_get_contents($base . '/__poc_boundary');
$boundary = is_string($boundaryBody) ? json_decode($boundaryBody, true) : null;
if (!is_array($boundary)
    || empty($boundary['external_stubs'])
    || empty($boundary['redis_isolated'])
    || !empty($boundary['real_queue'])) {
    throw new RuntimeException('External stub/Redis/queue boundary failed');
}

$phase = 'db-context';
$host = (string)getenv('POC_HOST');
$dbPort = (string)getenv('POC_PORT');
$schema = (string)getenv('POC_SCHEMA');
$dbUser = (string)getenv('POC_USER');
$dbPassword = (string)getenv('POC_PASSWORD');
$reason = 'host';
if ($host === '') throw new RuntimeException('DB context missing');
$reason = 'port';
if ($dbPort === '') throw new RuntimeException('DB context missing');
$reason = 'schema';
if ($schema === '') throw new RuntimeException('DB context missing');
$reason = 'user';
if ($dbUser === '') throw new RuntimeException('DB context missing');
$reason = 'password';
if ($dbPassword === '') throw new RuntimeException('DB context missing');
if ($lane === 'C') {
    $reason = 'host';
    if ($host !== '127.0.0.1') throw new RuntimeException('Nonproduction DM target mismatch');
    $reason = 'port';
    if ($dbPort !== '5237') throw new RuntimeException('Nonproduction DM target mismatch');
    $reason = 'schema';
    if (strtoupper($schema) !== 'ZHCT_APP') throw new RuntimeException('Nonproduction DM target mismatch');
    $reason = 'user';
    if (strtoupper($dbUser) !== 'ZHCT_APP') throw new RuntimeException('Nonproduction DM target mismatch');
    $dsn = 'dm:host=' . $host . ';port=' . $dbPort . ';schema=' . $schema;
} else {
    $reason = 'file';
    $configPath = '/workspace/zhct/zhctproject/store/application/config/local/database.php';
    $selectedPath = $root . '/application/database.php';
    if (!is_file($configPath) || is_link($configPath)
        || !is_file($selectedPath) || is_link($selectedPath)) {
        throw new RuntimeException('Code local/runtime MySQL config missing');
    }
    // Each require has its own scope: runtime database.php defines $config.
    $loadConfig = static function (string $file): array {
        $loaded = require $file;
        if (!is_array($loaded)) {
            throw new RuntimeException('Database config did not return array');
        }
        return $loaded;
    };
    $config = $loadConfig($configPath);
    $selected = $loadConfig($selectedPath);
    $reason = 'type';
    if (!is_array($config) || !is_array($selected)
        || ($selected['type'] ?? null) !== 'mysql') {
        throw new RuntimeException('Runtime DB differs from code local MySQL');
    }
    $reason = 'port';
    if ((int)($selected['hostport'] ?? 3306)
            !== (int)($config['hostport'] ?? 3306)
        || (string)($config['hostport'] ?? '3306') !== $dbPort) {
        throw new RuntimeException('Runtime DB differs from code local MySQL');
    }
    $reason = 'host';
    if ((string)($config['hostname'] ?? '') !== $host) {
        throw new RuntimeException('Runtime DB differs from code local MySQL');
    }
    $reason = 'schema';
    if ((string)($config['database'] ?? '') !== $schema) {
        throw new RuntimeException('Runtime DB differs from code local MySQL');
    }
    $reason = 'user';
    if ((string)($config['username'] ?? '') !== $dbUser) {
        throw new RuntimeException('Runtime DB differs from code local MySQL');
    }
    $reason = 'password';
    if ((string)($config['password'] ?? '') !== $dbPassword) {
        throw new RuntimeException('Runtime DB differs from code local MySQL');
    }
    $reason = 'selected-field';
    foreach (['hostname', 'database', 'username', 'password'] as $field) {
        if (!isset($config[$field], $selected[$field])
            || $config[$field] !== $selected[$field]) {
            throw new RuntimeException('Selected runtime DB differs from code local MySQL');
        }
    }
    $dsn = 'mysql:host=' . $host . ';port=' . $dbPort
        . ';dbname=' . $schema . ';charset=utf8mb4';
}
$reason = 'none';
$phase = 'db-connect';
$db = new PDO($dsn, $dbUser, $dbPassword, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
]);
$table = static function (string $name) use ($lane): string {
    return $lane === 'C' ? strtoupper('ydy_' . $name) : 'ydy_' . $name;
};
$rows = static function (string $sql) use ($db): array {
    $stmt = $db->query($sql);
    return array_map(static function (array $row): array {
        return array_change_key_case($row, CASE_LOWER);
    }, $stmt->fetchAll(PDO::FETCH_ASSOC));
};
$snapshot = static function () use ($rows, $table): array {
    $categories = $rows('SELECT UUID,PUUID,LEVEL FROM '
        . $table('article_cate') . ' ORDER BY ID');
    $articles = $rows('SELECT UUID,ARTICLE_CATE_UUID,STATUS FROM '
        . $table('article') . ' ORDER BY ID');
    $categoryIds = [];
    foreach ($categories as $category) {
        $categoryIds[(string)$category['uuid']] = true;
    }
    $activeRelated = 0;
    foreach ($articles as $article) {
        if ((int)$article['status'] === 1
            && isset($categoryIds[(string)$article['article_cate_uuid']])) {
            ++$activeRelated;
        }
    }
    return [
        'category_count' => count($categories),
        'article_count' => count($articles),
        'active_related_count' => $activeRelated,
        'category_fingerprint' => hash('sha256',
            json_encode($categories, JSON_UNESCAPED_UNICODE)),
        'article_fingerprint' => hash('sha256',
            json_encode($articles, JSON_UNESCAPED_UNICODE)),
    ];
};
$phase = 'snapshot-before';
$before = $snapshot();
if ($before['category_count'] < 1 || $before['active_related_count'] < 1) {
    throw new RuntimeException('Nonempty article/category relation missing');
}

$post = static function (string $path, array $form) use ($base): array {
    $context = stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
        'content' => http_build_query($form, '', '&', PHP_QUERY_RFC3986),
        'timeout' => 15,
        'ignore_errors' => true,
    ]]);
    $body = @file_get_contents($base . $path, false, $context);
    $responseHeaders = $http_response_header ?? [];
    if (!is_string($body)
        || strpos((string)($responseHeaders[0] ?? ''), ' 200 ') === false) {
        throw new RuntimeException('Anonymous HTTP transport failed');
    }
    $decoded = json_decode($body, true);
    if (!is_array($decoded)) {
        throw new RuntimeException('Anonymous HTTP JSON contract failed');
    }
    return $decoded;
};
$types = static function (array $value): array {
    $result = [];
    foreach ($value as $key => $item) {
        $result[(string)$key] = gettype($item);
    }
    ksort($result);
    return $result;
};
$result = [];
foreach ([
    ['/p/article/getCate', []],
    ['/p/article/lists', ['page' => 1]],
] as $case) {
    $phase = $case[0] === '/p/article/getCate'
        ? 'http-get-cate' : 'http-list';
    $response = $post($case[0], $case[1]);
    $data = $response['data'] ?? null;
    $positive = $case[0] === '/p/article/lists'
        ? is_array($data) && !empty($data['rows'])
        : is_array($data) && count($data) > 0;
    $result[] = [
        'path' => $case[0],
        'http' => 200,
        'code' => $response['code'] ?? null,
        'top_level_types' => $types($response),
        'data_types' => is_array($data) ? $types($data) : [],
        'positive' => $positive,
        'body_sha256' => hash('sha256', json_encode(
            $response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
    ];
    if (!array_key_exists('code', $response)
        || (int)$response['code'] !== 0 || !$positive) {
        throw new RuntimeException('Anonymous business positive gate failed');
    }
}
$phase = 'snapshot-after';
$after = $snapshot();
if ($before !== $after) {
    throw new RuntimeException('Article/category source changed');
}
echo json_encode([
    'lane' => $lane,
    'auth' => 'anonymous-no-cookie-no-token',
    'source_relation_nonempty' => true,
    'source_unchanged' => true,
    'results' => $result,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
