<?php

declare(strict_types=1);

namespace sammo;

if (PHP_SAPI !== 'cli' || getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
    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';

/** @param array<string, mixed> $values */
function comparisonMonthlyPatch(string $table, int $id, array $values): void
{
    $definitions = [
        'city' => [
            'idColumn' => 'city',
            'allowed' => [
                'name', 'level', 'nation', 'supply', 'front', 'pop', 'pop_max',
                'agri', 'agri_max', 'comm', 'comm_max', 'secu', 'secu_max',
                'trust', 'trade', 'def', 'def_max', 'wall', 'wall_max',
                'officer_set', 'state', 'region', 'term', 'conflict', 'dead',
            ],
        ],
        'nation' => [
            'idColumn' => 'nation',
            'allowed' => [
                'name', 'color', 'capital', 'gennum', 'gold', 'rice', 'tech',
                'level', 'type', 'rate_tmp', 'aux',
            ],
        ],
        'general' => [
            'idColumn' => 'no',
            'allowed' => [
                'name', 'nation', 'city', 'officer_level', 'officer_city',
                'crew', 'train', 'atmos', 'owner', 'npc', 'killturn', 'belong',
                'gold', 'rice', 'horse', 'weapon', 'book', 'item', 'aux',
            ],
        ],
    ];
    $definition = $definitions[$table] ?? null;
    if ($definition === null) {
        throw new \InvalidArgumentException("unsupported setup table: {$table}");
    }
    $patch = [];
    foreach ($values as $key => $value) {
        if (!is_string($key) || !in_array($key, $definition['allowed'], true)) {
            throw new \InvalidArgumentException("unsupported {$table} setup field");
        }
        if (in_array($key, ['conflict', 'aux'], true) && is_array($value)) {
            $value = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
        }
        $patch[$key] = $value;
    }
    if ($table === 'nation') {
        $patch += [
            'nation' => $id,
            'name' => "fixture-nation-{$id}",
            'color' => '#777777',
            'capital' => 0,
            'gold' => 0,
            'rice' => 0,
            'level' => 1,
            'type' => 'che_중립',
            'aux' => '{}',
        ];
        DB::db()->insertUpdate('nation', $patch, $patch);
    } elseif ($patch !== []) {
        DB::db()->update($table, $patch, "`{$definition['idColumn']}` = %i", $id);
    }
}

/** @param array<string, mixed> $observe */
function comparisonMonthlyDetails(array $observe, int $worldHistoryAfterId): array
{
    $db = DB::db();
    $generals = array_map(
        static fn(array $row): array => comparisonPickRow(
            $row,
            [
                'id' => 'no',
                'nationId' => 'nation',
                'cityId' => 'city',
                'officerLevel' => 'officer_level',
                'officerCityId' => 'officer_city',
                'crew' => 'crew',
                'train' => 'train',
                'atmos' => 'atmos',
                'gold' => 'gold',
                'rice' => 'rice',
            ],
        ),
        comparisonRowsById('general', 'no', comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds')),
    );
    $cities = array_map(
        static fn(array $row): array => comparisonPickRow(
            $row,
            [
                'id' => 'city',
                'nationId' => 'nation',
                'supplyState' => 'supply',
                'frontState' => 'front',
                'officerSet' => 'officer_set',
                'term' => 'term',
                'dead' => 'dead',
                'population' => 'pop',
                'agriculture' => 'agri',
                'commerce' => 'comm',
                'security' => 'secu',
                'trust' => 'trust',
                'defence' => 'def',
                'wall' => 'wall',
                'conflict' => 'conflict',
            ],
            ['conflict'],
        ),
        comparisonRowsById('city', 'city', comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds')),
    );
    $nations = array_map(
        static fn(array $row): array => comparisonPickRow(
            $row,
            [
                'id' => 'nation',
                'gold' => 'gold',
                'rice' => 'rice',
                'rate' => 'rate_tmp',
                'typeCode' => 'type',
            ],
        ),
        comparisonRowsById('nation', 'nation', comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds')),
    );
    $worldHistory = array_map(
        static fn(array $row): array => comparisonPickRow(
            $row,
            [
                'id' => 'id',
                'nationId' => 'nation_id',
                'year' => 'year',
                'month' => 'month',
                'text' => 'text',
            ],
        ),
        $db->query(
            'SELECT id, nation_id, year, month, text FROM world_history WHERE id > %i ORDER BY id',
            $worldHistoryAfterId,
        ),
    );
    $inheritancePoints = [];
    foreach (comparisonIntegerList($observe['ownerIds'] ?? [], 'ownerIds') as $ownerId) {
        $value = $db->queryFirstField(
            'SELECT value FROM storage WHERE namespace = %s AND `key` = %s',
            "inheritance_{$ownerId}",
            'unifier',
        );
        $inheritancePoints[] = [
            'ownerId' => $ownerId,
            'unifier' => $value === null ? 0 : (comparisonJsonValue($value)[0] ?? 0),
        ];
    }
    return [
        'generals' => $generals,
        'cities' => $cities,
        'nations' => $nations,
        'worldHistory' => $worldHistory,
        'inheritancePoints' => $inheritancePoints,
    ];
}

function comparisonMonthlyEventTraceMain(): void
{
    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');
        }
        $actionName = $request['action'] ?? null;
        $supportedActions = [
            'UpdateCitySupply',
            'UpdateNationLevel',
            'ProcessSemiAnnual',
            'ProcessWarIncome',
            'CreateAdminNPC',
            'CreateManyNPC',
            'RegNPC',
            'RegNeutralNPC',
            'RaiseNPCNation',
        ];
        if (!in_array($actionName, $supportedActions, true)) {
            throw new \InvalidArgumentException('unsupported monthly action');
        }
        $setup = $request['setup'] ?? [];
        if (!is_array($setup)) {
            throw new \InvalidArgumentException('setup must be an object');
        }
        foreach (['city', 'nation', 'general'] as $table) {
            $rows = $setup[$table] ?? [];
            if (!is_array($rows)) {
                throw new \InvalidArgumentException("setup.{$table} must be an array");
            }
            foreach ($rows as $row) {
                if (!is_array($row) || !is_int($row['id'] ?? null) || !is_array($row['values'] ?? null)) {
                    throw new \InvalidArgumentException("invalid setup.{$table} row");
                }
                comparisonMonthlyPatch($table, $row['id'], $row['values']);
            }
        }
        if (($setup['resetCities'] ?? false) === true) {
            DB::db()->query('UPDATE city SET nation = 0, level = 1');
            foreach ($setup['city'] ?? [] as $row) {
                comparisonMonthlyPatch('city', $row['id'], $row['values']);
            }
        }
        if (($setup['resetGenerals'] ?? false) === true) {
            DB::db()->query(
                'UPDATE general SET nation = 0, officer_level = 1, npc = 2, killturn = 0, horse = %s, weapon = %s, book = %s, item = %s',
                'None',
                'None',
                'None',
                'None',
            );
            foreach ($setup['general'] ?? [] as $row) {
                comparisonMonthlyPatch('general', $row['id'], $row['values']);
            }
        }
        if (($setup['resetNations'] ?? false) === true) {
            DB::db()->query('UPDATE nation SET level = 0, gennum = 0, tech = 0');
            foreach ($setup['nation'] ?? [] as $row) {
                comparisonMonthlyPatch('nation', $row['id'], $row['values']);
            }
        }
        if (($setup['resetUniqueOccupancy'] ?? false) === true) {
            DB::db()->delete('ng_auction', '`type` = %s AND finished = 0', 'uniqueItem');
            DB::db()->query('DELETE FROM storage WHERE namespace LIKE %s', 'ut\\_%');
        }
        foreach (comparisonIntegerList($setup['clearNationTurnIds'] ?? [], 'clearNationTurnIds') as $nationId) {
            DB::db()->delete('nation_turn', 'nation_id = %i', $nationId);
        }
        foreach (comparisonIntegerList($setup['clearInheritanceOwnerIds'] ?? [], 'clearInheritanceOwnerIds') as $ownerId) {
            DB::db()->delete('storage', 'namespace = %s', "inheritance_{$ownerId}");
        }

        $db = DB::db();
        $generalIdBeforeAction =
            (int)($db->queryFirstField('SELECT COALESCE(MAX(no), 0) FROM general') ?? 0);
        $nationIdBeforeAction =
            (int)($db->queryFirstField('SELECT COALESCE(MAX(nation), 0) FROM nation') ?? 0);
        $snapshotRequest = ['observe' => $request['observe'] ?? []];
        $snapshotRequest['observe']['logAfterId'] =
            (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0);
        $worldHistoryAfterId =
            (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM world_history') ?? 0);
        $before = comparisonTurnStateSnapshot($snapshotRequest);
        $beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);

        $environment = $request['environment'] ?? [];
        if (!is_array($environment)) {
            throw new \InvalidArgumentException('environment must be an object');
        }
        $year = $environment['year'] ?? null;
        $month = $environment['month'] ?? null;
        $startYear = $environment['startyear'] ?? null;
        if (!is_int($year) || !is_int($month) || !is_int($startYear)) {
            throw new \InvalidArgumentException('environment year, month, and startyear must be integers');
        }
        if (($setup['syncEnvironment'] ?? false) === true) {
            $gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
            foreach ([
                'year' => $year,
                'month' => $month,
                'startyear' => $startYear,
                'killturn' => $environment['killturn'] ?? $gameStorage->killturn,
                'turnterm' => $environment['turnterm'] ?? $gameStorage->turnterm,
            ] as $key => $value) {
                $gameStorage->setValue($key, $value);
            }
        }

        $actionClass = "\\sammo\\Event\\Action\\{$actionName}";
        if ($actionName === 'ProcessSemiAnnual') {
            $resource = $request['resource'] ?? null;
            if (!is_string($resource)) {
                throw new \InvalidArgumentException('ProcessSemiAnnual requires resource');
            }
            $action = new $actionClass($resource);
        } elseif (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC'], true)) {
            $args = $request['args'] ?? [];
            if (!is_array($args)) {
                throw new \InvalidArgumentException("{$actionName} args must be an array");
            }
            if ($actionName === 'CreateManyNPC' && count($args) > 2) {
                throw new \InvalidArgumentException('CreateManyNPC args must have at most two entries');
            }
            $action = new $actionClass(...$args);
        } else {
            $action = new $actionClass();
        }
        $actionEnvironment = [
            'year' => $year,
            'month' => $month,
            'startyear' => $startYear,
        ];
        if ($actionName === 'UpdateNationLevel') {
            $killturn = $environment['killturn'] ?? null;
            $turnterm = $environment['turnterm'] ?? null;
            if (!is_int($killturn) || !is_int($turnterm) || $turnterm <= 0) {
                throw new \InvalidArgumentException('UpdateNationLevel requires integer killturn and positive turnterm');
            }
            $actionEnvironment['killturn'] = $killturn;
            $actionEnvironment['turnterm'] = $turnterm;
        }
        if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation'], true)) {
            $turnterm = $environment['turnterm'] ?? null;
            $turntime = $environment['turntime'] ?? null;
            $showImgLevel = $environment['show_img_level'] ?? 3;
            $fiction = $environment['fiction'] ?? [0];
            if (
                !is_int($turnterm)
                || $turnterm <= 0
                || !is_string($turntime)
                || !is_int($showImgLevel)
                || !is_array($fiction)
            ) {
                throw new \InvalidArgumentException(
                    "{$actionName} requires positive turnterm, string turntime, integer show_img_level, and array fiction",
                );
            }
            $actionEnvironment += [
                'turnterm' => $turnterm,
                'turntime' => $turntime,
                'show_img_level' => $showImgLevel,
                'stored_icons' => $environment['stored_icons'] ?? [],
                'icon_path' => $environment['icon_path'] ?? '.',
                'fiction' => $fiction,
            ];
        }
        $action->run($actionEnvironment);
        unset($action);
        gc_collect_cycles();

        $after = comparisonTurnStateSnapshot($snapshotRequest);
        $afterDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
        $createdGenerals = [];
        if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation'], true)) {
            foreach ($db->query('SELECT * FROM general WHERE no > %i ORDER BY no', $generalIdBeforeAction) as $row) {
                $generalId = (int)$row['no'];
                $turnRows = $db->query(
                    'SELECT turn_idx, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
                    $generalId,
                );
                $createdGenerals[] = comparisonPickRow(
                    $row,
                    [
                        'id' => 'no',
                        'name' => 'name',
                        'nationId' => 'nation',
                        'cityId' => 'city',
                        'leadership' => 'leadership',
                        'strength' => 'strength',
                        'intelligence' => 'intel',
                        'experience' => 'experience',
                        'dedication' => 'dedication',
                        'officerLevel' => 'officer_level',
                        'gold' => 'gold',
                        'rice' => 'rice',
                        'crew' => 'crew',
                        'crewTypeId' => 'crewtype',
                        'train' => 'train',
                        'atmos' => 'atmos',
                        'turnTime' => 'turntime',
                        'killturn' => 'killturn',
                        'age' => 'age',
                        'npcState' => 'npc',
                        'npcOriginalState' => 'npc_org',
                        'affinity' => 'affinity',
                        'personality' => 'personal',
                        'specialDomestic' => 'special',
                        'specialWar' => 'special2',
                        'specAge' => 'specage',
                        'specAge2' => 'specage2',
                        'bornYear' => 'bornyear',
                        'deadYear' => 'deadyear',
                        'picture' => 'picture',
                    ],
                ) + [
                    'turnCount' => count($turnRows),
                    'turnActions' => array_values(array_unique(array_column($turnRows, 'action'))),
                    'rankCount' => (int)$db->queryFirstField(
                        'SELECT count(*) FROM rank_data WHERE general_id = %i',
                        $generalId,
                    ),
                    'nonZeroRankCount' => (int)$db->queryFirstField(
                        'SELECT count(*) FROM rank_data WHERE general_id = %i AND value != 0',
                        $generalId,
                    ),
                ];
            }
        }
        $createdNations = [];
        if ($actionName === 'RaiseNPCNation') {
            foreach ($db->query('SELECT * FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction) as $row) {
                $nationId = (int)$row['nation'];
                $createdNations[] = comparisonPickRow(
                    $row,
                    [
                        'id' => 'nation',
                        'name' => 'name',
                        'color' => 'color',
                        'capitalCityId' => 'capital',
                        'generalCount' => 'gennum',
                        'gold' => 'gold',
                        'rice' => 'rice',
                        'tech' => 'tech',
                        'level' => 'level',
                        'typeCode' => 'type',
                        'bill' => 'bill',
                        'rate' => 'rate',
                        'scout' => 'scout',
                        'war' => 'war',
                        'strategicCommandLimit' => 'strategic_cmd_limit',
                        'surrenderLimit' => 'surlimit',
                        'aux' => 'aux',
                    ],
                    ['aux'],
                ) + [
                    'nationTurnCount' => (int)$db->queryFirstField(
                        'SELECT count(*) FROM nation_turn WHERE nation_id = %i',
                        $nationId,
                    ),
                    'diplomacyCount' => (int)$db->queryFirstField(
                        'SELECT count(*) FROM diplomacy WHERE me = %i OR you = %i',
                        $nationId,
                        $nationId,
                    ),
                ];
            }
        }
        $response = [
            'schemaVersion' => 1,
            'engine' => 'ref',
            'action' => $actionName,
            'before' => $before,
            'beforeDetails' => $beforeDetails,
            'after' => $after,
            'afterDetails' => $afterDetails,
        ];
        if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC'], true)) {
            $response['createdGenerals'] = $createdGenerals;
        }
        if ($actionName === 'RaiseNPCNation') {
            $response['createdGenerals'] = $createdGenerals;
            $response['createdNations'] = $createdNations;
        }
        echo json_encode(
            $response,
            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);
    }
}

comparisonMonthlyEventTraceMain();
