<?php

declare(strict_types=1);

// Review-only library. Construction is disabled unless a reviewed runner
// explicitly supplies the approval flag.
if (isset($_SERVER['SCRIPT_FILENAME'])
    && realpath((string)$_SERVER['SCRIPT_FILENAME']) === __FILE__) {
    fwrite(STDERR, "Review library; direct execution disabled\n");
    exit(2);
}
final class StoreReadOnlyBatchBase
{
    private $lane;
    private $root;
    private $baseUrl;
    private $allowedPaths;
    private $db;

    public function __construct(string $lane, array $allowedPaths, array $sourceFiles, string $expectedDigest)
    {
        if ((string)getenv('CDX_STORE_READ_BATCH_EXECUTE') !== 'ROOT_APPROVED_EXACT_SHA') {
            throw new RuntimeException('Read-only batch execution disabled');
        }
        $lanes = [
            'A' => ['/tmp/zhct-http-store-a.l5omH8', 'http://127.0.0.1:18097', '21023', 'mysql'],
            'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', '10001', 'mysql'],
            'C' => ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', '112175', 'dm'],
        ];
        if (!isset($lanes[$lane]) || !$allowedPaths || !$sourceFiles
            || count($sourceFiles) > 20 || !preg_match('/^[a-f0-9]{64}$/', $expectedDigest)) {
            throw new RuntimeException('Unreviewed read-only batch scope');
        }
        $this->lane = $lane;
        $this->root = $lanes[$lane][0];
        $this->baseUrl = $lanes[$lane][1];
        $this->allowedPaths = array_values(array_unique($allowedPaths));
        foreach ($this->allowedPaths as $path) {
            if (!is_string($path)
                || !preg_match('#^/[A-Za-z0-9/_-]+$#', $path)
                || strpos($path, '/p/') !== 0) {
                throw new RuntimeException('Non-PC or dynamic path prohibited');
            }
        }
        if (is_link($this->root)
            || (string)getenv('POC_APP_ROOT') !== $this->root
            || rtrim((string)getenv('POC_BASE_URL'), '/') !== $this->baseUrl
            || (string)getenv('POC_RUNTIME_PID') !== $lanes[$lane][2]
            || (string)getenv('POC_DIALECT') !== $lanes[$lane][3]
            || (string)getenv('CDX_STORE_READ_TOKEN') !== ''
            || (string)getenv('HTTP_COOKIE') !== '') {
            throw new RuntimeException('Isolated anonymous runtime required');
        }
        $cmdlinePath = '/proc/' . $lanes[$lane][2] . '/cmdline';
        $cmdline = is_file($cmdlinePath) ? (string)file_get_contents($cmdlinePath) : '';
        if (strpos($cmdline, '127.0.0.1:' . (string)parse_url($this->baseUrl, PHP_URL_PORT)) === false
            || strpos($cmdline, $this->root . '/public') === false) {
            throw new RuntimeException('Runtime PID/port/docroot mismatch');
        }
        $hashes = [];
        foreach ($sourceFiles as $source) {
            if (!is_string($source)
                || !preg_match('#^application/[A-Za-z0-9_./-]+\.php$#', $source)
                || strpos($source, '..') !== false) {
                throw new RuntimeException('Invalid source path');
            }
            $sourcePath = $this->root . '/' . $source;
            if (!is_file($sourcePath) || is_link($sourcePath)) {
                throw new RuntimeException('Source file missing');
            }
            $hashes[] = hash_file('sha256', $sourcePath);
        }
        if (hash('sha256', implode('', $hashes)) !== $expectedDigest) {
            throw new RuntimeException('Reviewed source digest mismatch');
        }
        $boundaryBody = @file_get_contents($this->baseUrl . '/__poc_boundary');
        $headers = $http_response_header ?? [];
        $boundary = is_string($boundaryBody) ? json_decode($boundaryBody, true) : null;
        if (strpos((string)($headers[0] ?? ''), ' 200 ') === false
            || !is_array($boundary)
            || empty($boundary['external_stubs'])
            || empty($boundary['redis_isolated'])
            || !empty($boundary['real_queue'])) {
            throw new RuntimeException('External stub/Redis/queue isolation failed');
        }
        $this->db = $this->connect();
    }

    private function connect(): PDO
    {
        $host = (string)getenv('POC_HOST');
        $port = (string)getenv('POC_PORT');
        $schema = (string)getenv('POC_SCHEMA');
        $user = (string)getenv('POC_USER');
        $password = (string)getenv('POC_PASSWORD');
        if ($host === '' || $port === '' || $schema === ''
            || $user === '' || $password === '') {
            throw new RuntimeException('Database context incomplete');
        }
        if ($this->lane === 'C') {
            if ($host !== '127.0.0.1' || $port !== '5237'
                || strtoupper($schema) !== 'ZHCT_APP'
                || strtoupper($user) !== 'ZHCT_APP') {
                throw new RuntimeException('Nonproduction DM target mismatch');
            }
            $dsn = 'dm:host=' . $host . ';port=' . $port . ';schema=' . $schema;
        } else {
            $load = static function (string $file): array {
                $value = require $file;
                if (!is_array($value)) {
                    throw new RuntimeException('Invalid MySQL configuration');
                }
                return $value;
            };
            $codeLocal = '/workspace/zhct/zhctproject/store/application/config/local/database.php';
            $selectedPath = $this->root . '/application/database.php';
            if (!is_file($codeLocal) || is_link($codeLocal)
                || !is_file($selectedPath) || is_link($selectedPath)) {
                throw new RuntimeException('Code-local/runtime configuration missing');
            }
            $local = $load($codeLocal);
            $selected = $load($selectedPath);
            if (($selected['type'] ?? null) !== 'mysql'
                || (int)($selected['hostport'] ?? 3306)
                    !== (int)($local['hostport'] ?? 3306)
                || (string)($local['hostport'] ?? '3306') !== $port) {
                throw new RuntimeException('MySQL dialect/port differs from code local');
            }
            foreach ([
                'hostname' => $host,
                'database' => $schema,
                'username' => $user,
                'password' => $password,
            ] as $field => $actual) {
                if (!isset($local[$field], $selected[$field])
                    || $local[$field] !== $selected[$field]
                    || (string)$local[$field] !== $actual) {
                    throw new RuntimeException('MySQL context differs from code local');
                }
            }
            $dsn = 'mysql:host=' . $host . ';port=' . $port
                . ';dbname=' . $schema . ';charset=utf8mb4';
        }
        return new PDO($dsn, $user, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]);
    }

    public function table(string $name): string
    {
        if (!preg_match('/^[a-z_]+$/', $name)) {
            throw new RuntimeException('Invalid read table name');
        }
        return $this->lane === 'C' ? strtoupper('ydy_' . $name) : 'ydy_' . $name;
    }

    public function selectRows(string $sql, array $params = []): array
    {
        if (!preg_match('/^\s*SELECT\b/i', $sql) || strpos($sql, ';') !== false) {
            throw new RuntimeException('SELECT-only batch');
        }
        $statement = $this->db->prepare($sql);
        $statement->execute($params);
        return array_map(static function (array $row): array {
            return array_change_key_case($row, CASE_LOWER);
        }, $statement->fetchAll(PDO::FETCH_ASSOC));
    }

    public function snapshot(array $queries): array
    {
        $result = [];
        foreach ($queries as $name => $sql) {
            if (!is_string($name) || !preg_match('/^[a-z_]+$/', $name)) {
                throw new RuntimeException('Invalid snapshot label');
            }
            $rows = $this->selectRows($sql);
            $encoded = json_encode($rows, JSON_UNESCAPED_UNICODE
                | JSON_INVALID_UTF8_SUBSTITUTE);
            if (!is_string($encoded)) {
                throw new RuntimeException('Read source cannot be fingerprinted');
            }
            $result[$name] = [
                'count' => count($rows),
                'sha256' => hash('sha256', $encoded),
            ];
        }
        ksort($result);
        return $result;
    }

    public function post(string $path, array $form): array
    {
        if (!in_array($path, $this->allowedPaths, true)) {
            throw new RuntimeException('HTTP path not in reviewed whitelist');
        }
        $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($this->baseUrl . $path, false, $context);
        $headers = $http_response_header ?? [];
        if (!is_string($body)
            || strpos((string)($headers[0] ?? ''), ' 200 ') === false) {
            throw new RuntimeException('Isolated read HTTP transport failed');
        }
        $response = json_decode($body, true);
        if (!is_array($response)) {
            throw new RuntimeException('Read HTTP JSON failed');
        }
        return $response;
    }

    public static function safeResponse(array $response): array
    {
        $types = static function (array $value): array {
            $out = [];
            foreach ($value as $key => $item) {
                $out[(string)$key] = gettype($item);
            }
            ksort($out);
            return $out;
        };
        $data = $response['data'] ?? null;
        $topTypes = $types($response);
        $dataTypes = is_array($data) ? $types($data) : [];
        $topEncoded = json_encode($topTypes, JSON_UNESCAPED_UNICODE
            | JSON_INVALID_UTF8_SUBSTITUTE);
        $dataEncoded = json_encode($dataTypes, JSON_UNESCAPED_UNICODE
            | JSON_INVALID_UTF8_SUBSTITUTE);
        $responseEncoded = json_encode($response, JSON_UNESCAPED_UNICODE
            | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
        if (!is_string($topEncoded) || !is_string($dataEncoded)
            || !is_string($responseEncoded)) {
            throw new RuntimeException('Read response cannot be fingerprinted');
        }
        $typeCounts = array_count_values(array_values($dataTypes));
        ksort($typeCounts);
        return [
            'code' => $response['code'] ?? null,
            'top_level_key_types_sha256' => hash('sha256', $topEncoded),
            'data_key_types_sha256' => hash('sha256', $dataEncoded),
            'data_key_count' => count($dataTypes),
            'data_type_counts' => $typeCounts,
            'response_sha256' => hash('sha256', $responseEncoded),
        ];
    }
}
