<?php

declare(strict_types=1);

namespace sammo;

if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit;
}

chdir(dirname(__DIR__));
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
error_reporting(E_ALL & ~E_DEPRECATED);
require_once 'lib.php';
require_once 'func.php';
require_once __DIR__ . '/turn_state_snapshot.php';

final class TurnComparisonTracingRNG implements RNG
{
    private int $sequence = 0;

    /** @var list<array<string, mixed>> */
    public array $calls = [];

    public function __construct(private readonly RNG $inner)
    {
    }

    public static function getMaxInt(): int
    {
        return LiteHashDRBG::getMaxInt();
    }

    public function nextBytes(int $bytes): string
    {
        $value = $this->inner->nextBytes($bytes);
        $this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
        return $value;
    }

    public function nextBits(int $bits): string
    {
        $value = $this->inner->nextBits($bits);
        $this->record('nextBits', ['bits' => $bits], bin2hex($value));
        return $value;
    }

    public function nextInt(?int $max = null): int
    {
        $value = $this->inner->nextInt($max);
        $this->record('nextInt', ['maxInclusive' => $max], $value);
        return $value;
    }

    public function nextFloat1(): float
    {
        $value = $this->inner->nextFloat1();
        $this->record('nextFloat1', [], $value);
        return $value;
    }

    private function record(string $operation, array $arguments, mixed $result): void
    {
        $this->calls[] = [
            'seq' => $this->sequence++,
            'operation' => $operation,
            'arguments' => $arguments === [] ? (object)[] : $arguments,
            'result' => $result,
        ];
    }
}

/** @return array<string, mixed> */
function comparisonMappedPatch(array $row, array $mapping): array
{
    $patch = [];
    foreach ($mapping as $canonical => $legacy) {
        if (array_key_exists($canonical, $row)) {
            $patch[$legacy] = $row[$canonical];
        }
    }
    return $patch;
}

function comparisonApplyTurnFixtureSetup(mixed $setup): void
{
    if ($setup === null) {
        return;
    }
    if (!is_array($setup)) {
        throw new \InvalidArgumentException('setup must be an object');
    }
    $db = DB::db();
    $world = $setup['world'] ?? null;
    if ($world !== null) {
        if (!is_array($world)) {
            throw new \InvalidArgumentException('setup.world must be an object');
        }
        $game = KVStorage::getStorage($db, 'game_env');
        foreach (['year', 'month', 'startyear'] as $key) {
            $fixtureKey = $key === 'startyear' ? 'startYear' : $key;
            if (!array_key_exists($fixtureKey, $world)) {
                continue;
            }
            $value = $world[$fixtureKey];
            if (!is_int($value) || $value < 1 || ($key === 'month' && $value > 12)) {
                throw new \InvalidArgumentException("setup.world.{$fixtureKey} is invalid");
            }
            $game->setValue($key, $value);
        }
        $game->resetCache();
        if (array_key_exists('hiddenSeed', $world)) {
            $hiddenSeed = $world['hiddenSeed'];
            if (!is_string($hiddenSeed) || $hiddenSeed === '' || strlen($hiddenSeed) > 256) {
                throw new \InvalidArgumentException('setup.world.hiddenSeed is invalid');
            }
            UniqueConst::$hiddenSeed = $hiddenSeed;
        }
    }

    foreach (['nations', 'cities', 'generals', 'diplomacy'] as $collection) {
        if (isset($setup[$collection]) && !is_array($setup[$collection])) {
            throw new \InvalidArgumentException("setup.{$collection} must be an array");
        }
    }

    if (($setup['isolateWorld'] ?? false) === true) {
        $generalIds = array_values(array_map(
            static fn(array $row): int => (int)$row['id'],
            $setup['generals'] ?? [],
        ));
        $nationIds = array_values(array_map(
            static fn(array $row): int => (int)$row['id'],
            $setup['nations'] ?? [],
        ));
        if ($generalIds === [] || $nationIds === []) {
            throw new \InvalidArgumentException('isolateWorld requires generals and nations');
        }
        $db->delete('general_turn', 'general_id NOT IN %li', $generalIds);
        $db->delete('nation_turn', 'nation_id NOT IN %li', $nationIds);
        $db->delete('troop', '1 = 1');
        $db->delete('diplomacy', '1 = 1');
        $db->delete('general', 'no NOT IN %li', $generalIds);
        $db->delete('nation', 'nation NOT IN %li', $nationIds);
        $db->update('city', [
            'nation' => 0,
            'supply' => 0,
            'front' => 0,
            'state' => 0,
            'term' => 0,
        ], '1 = 1');
    }

    foreach ($setup['nations'] ?? [] as $row) {
        if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
            throw new \InvalidArgumentException('setup.nations requires positive integer ids');
        }
        $data = comparisonMappedPatch($row, [
            'id' => 'nation',
            'name' => 'name',
            'color' => 'color',
            'capitalCityId' => 'capital',
            'gold' => 'gold',
            'rice' => 'rice',
            'tech' => 'tech',
            'level' => 'level',
            'typeCode' => 'type',
            'war' => 'war',
            'diplomacyLimit' => 'surlimit',
            'generalCount' => 'gennum',
            'power' => 'power',
        ]);
        if (isset($row['meta'])) {
            $data['aux'] = Json::encode($row['meta']);
        }
        $data += [
            'name' => "fixture-nation-{$row['id']}",
            'color' => '#777777',
            'capital' => 0,
            'gold' => 0,
            'rice' => 0,
            'level' => 1,
            'type' => 'che_중립',
            'aux' => '{}',
        ];
        $db->insertUpdate('nation', $data, $data);
    }

    foreach ($setup['cities'] ?? [] as $row) {
        if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
            throw new \InvalidArgumentException('setup.cities requires positive integer ids');
        }
        $patch = comparisonMappedPatch($row, [
            'nationId' => 'nation',
            'population' => 'pop',
            'agriculture' => 'agri',
            'commerce' => 'comm',
            'security' => 'secu',
            'supplyState' => 'supply',
            'frontState' => 'front',
            'defence' => 'def',
            'wall' => 'wall',
            'state' => 'state',
            'term' => 'term',
            'trust' => 'trust',
            'trade' => 'trade',
        ]);
        if ($patch !== []) {
            $db->update('city', $patch, 'city = %i', $row['id']);
        }
    }

    foreach ($setup['generals'] ?? [] as $row) {
        if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
            throw new \InvalidArgumentException('setup.generals requires positive integer ids');
        }
        $patch = comparisonMappedPatch($row, [
            'nationId' => 'nation',
            'cityId' => 'city',
            'troopId' => 'troop',
            'leadership' => 'leadership',
            'strength' => 'strength',
            'intelligence' => 'intel',
            'leadershipExp' => 'leadership_exp',
            'strengthExp' => 'strength_exp',
            'intelExp' => 'intel_exp',
            'experience' => 'experience',
            'dedication' => 'dedication',
            'expLevel' => 'explevel',
            'officerLevel' => 'officer_level',
            'officerCityId' => 'officer_city',
            'injury' => 'injury',
            'age' => 'age',
            'gold' => 'gold',
            'rice' => 'rice',
            'crew' => 'crew',
            'crewTypeId' => 'crewtype',
            'train' => 'train',
            'atmos' => 'atmos',
            'dex1' => 'dex1',
            'dex2' => 'dex2',
            'dex3' => 'dex3',
            'dex4' => 'dex4',
            'dex5' => 'dex5',
            'killTurn' => 'killturn',
            'npcState' => 'npc',
            'blockState' => 'block',
            'specialDomestic' => 'special',
            'specialWar' => 'special2',
            'personality' => 'personal',
            'itemHorse' => 'horse',
            'itemWeapon' => 'weapon',
            'itemBook' => 'book',
            'itemExtra' => 'item',
        ]);
        if (isset($row['meta'])) {
            $patch['aux'] = Json::encode($row['meta']);
        }
        if (isset($row['lastTurn']) && is_array($row['lastTurn'])) {
            $patch['last_turn'] = Json::encode($row['lastTurn']);
        }
        if ($patch !== []) {
            $db->update('general', $patch, 'no = %i', $row['id']);
        }
    }

    foreach ($setup['diplomacy'] ?? [] as $row) {
        if (
            !is_array($row) ||
            !is_int($row['fromNationId'] ?? null) ||
            !is_int($row['toNationId'] ?? null) ||
            $row['fromNationId'] < 1 ||
            $row['toNationId'] < 1
        ) {
            throw new \InvalidArgumentException('setup.diplomacy requires positive integer nation ids');
        }
        $data = comparisonMappedPatch($row, [
            'fromNationId' => 'me',
            'toNationId' => 'you',
            'state' => 'state',
            'term' => 'term',
            'dead' => 'dead',
        ]);
        $data += ['state' => 3, 'term' => 0, 'dead' => 0];
        $db->insertUpdate('diplomacy', $data, $data);
    }
}

function comparisonRunTurnCommand(array $request): array
{
    if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
        throw new \RuntimeException('TURN_DIFFERENTIAL_ENABLED=1 is required');
    }
    $kind = $request['kind'] ?? null;
    if ($kind !== 'general' && $kind !== 'nation') {
        throw new \InvalidArgumentException('kind must be general or nation');
    }
    $actorGeneralId = $request['actorGeneralId'] ?? null;
    $action = $request['action'] ?? null;
    $args = $request['args'] ?? null;
    if (!is_int($actorGeneralId) || $actorGeneralId < 1) {
        throw new \InvalidArgumentException('actorGeneralId must be a positive integer');
    }
    if (!is_string($action) || $action === '') {
        throw new \InvalidArgumentException('action must be a non-empty string');
    }

    comparisonApplyTurnFixtureSetup($request['setup'] ?? null);
    $snapshotRequest = ['observe' => $request['observe'] ?? []];
    $before = comparisonTurnStateSnapshot($snapshotRequest);
    $db = DB::db();
    $gameStorage = KVStorage::getStorage($db, 'game_env');
    $gameStorage->resetCache();
    $environment = $gameStorage->getAll();
    $general = General::createObjFromDB($actorGeneralId);
    $turn = new TurnExecutionHelper($general);
    $general->increaseInheritancePoint(Enums\InheritanceKey::lived_month, 1);
    $preprocessRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
        UniqueConst::$hiddenSeed,
        'preprocess',
        (int)$environment['year'],
        (int)$environment['month'],
        $actorGeneralId,
    )));
    $turn->preprocessCommand($preprocessRng, $environment);
    $seedDomain = $kind === 'general' ? 'generalCommand' : 'nationCommand';
    $seed = Util::simpleSerialize(
        UniqueConst::$hiddenSeed,
        $seedDomain,
        (int)$environment['year'],
        (int)$environment['month'],
        $actorGeneralId,
        $action,
    );
    $tracingRng = new TurnComparisonTracingRNG(new LiteHashDRBG($seed));
    $rng = new RandUtil($tracingRng);
    if ($kind === 'general' && $action === 'che_견문') {
        \sammo\TextDecoration\SightseeingMessage::setComparisonRng($rng);
    }

    $previousLastTurnRaw = $general->getLastTurn()->toRaw();
    if ($kind === 'general') {
        $command = buildGeneralCommandClass($action, $general, $environment, $args);
        $resultTurn = $turn->processCommand($rng, $command, false);
    } else {
        $command = buildNationCommandClass($action, $general, $environment, $general->getLastTurn(), $args);
        $resultTurn = $turn->processNationCommand($rng, $command);
    }

    $general->getLogger()->flush();
    $turn->applyDB();
    unset($turn);
    $after = comparisonTurnStateSnapshot($snapshotRequest);
    $resultTurnRaw = $resultTurn->toRaw();
    $resultTerm = (int)($resultTurnRaw['term'] ?? 0);
    $preReqTurn = $command->getPreReqTurn();
    $completed = ($resultTurnRaw['command'] ?? null) === $command->getName()
        && (
            ($preReqTurn === 0 && $resultTerm === 0)
            || (
                $preReqTurn > 0
                && ($previousLastTurnRaw['command'] ?? null) === $command->getName()
                && (int)($previousLastTurnRaw['term'] ?? 0) === $preReqTurn
            )
        );

    return [
        'schemaVersion' => 1,
        'engine' => 'ref',
        'execution' => [
            'kind' => $kind,
            'actorGeneralId' => $actorGeneralId,
            'action' => $action,
            'args' => $args,
            'seedDomain' => $seedDomain,
            'outcome' => [
                'lastTurn' => $resultTurnRaw,
                'commandName' => $command->getName(),
                'completed' => $completed,
            ],
        ],
        'before' => $before,
        'after' => $after,
        'rng' => $tracingRng->calls,
    ];
}

try {
    $input = stream_get_contents(STDIN);
    $request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
    if (!is_array($request)) {
        throw new \InvalidArgumentException('request must be an object');
    }
    echo json_encode(
        comparisonRunTurnCommand($request),
        JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
    ), PHP_EOL;
} catch (\Throwable $throwable) {
    fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
    exit(1);
}
