<?php

namespace sammo\API\Global;

use sammo\Session;
use DateTimeInterface;
use sammo\VersionGit;
use Nette\Caching\Cache;
use RuntimeException;
use sammo\APICacheResult;
use sammo\CityConst;
use sammo\Enums\APIRecoveryType;
use sammo\GameConst;
use sammo\GameUnitConst;
use sammo\Json;
use sammo\TimeUtil;
use sammo\UniqueConst;

use function sammo\prepareDir;

class GetConst extends \sammo\BaseAPI
{
    /** 반환하는 StaticValues 타입이 달라지면 +1 */
    const CONST_API_VERSION = 3;
    const CACHE_KEY = 'JSConst';

    private ?string $cacheKey = null;

    public function validateArgs(): ?string
    {
        return null;
    }

    public function getRequiredSessionMode(): int
    {
        return static::NO_SESSION;
    }

    public function findLastModified(): ?int
    {
        $rawResult = explode("\n", shell_exec('git status -u -s --no-rename'));
        $files = [];
        foreach ($rawResult as $rawLine) {
            if (strlen($rawLine) < 3) {
                continue;
            }
            $filePath = trim(substr($rawLine, 3));
            if ($filePath[0] == '"') {
                $filePath = substr($filePath, 1, strlen($filePath) - 2);
            }
            if (!file_exists($filePath)) {
                continue;
            }
            $files[] = $filePath;
        }

        $lastModified = null;
        foreach ($files as $filePath) {
            $fileModified = filemtime($filePath);
            if ($fileModified === false) {
                continue;
            }
            if ($lastModified === null) {
                $lastModified = $fileModified;
                continue;
            }
            if ($lastModified > $fileModified) {
                continue;
            }
            $lastModified = $fileModified;
        }
        if ($lastModified === null) {
            return null;
        }
        return $lastModified;
    }

    public function getCacheKey(): string
    {
        if ($this->cacheKey !== null) {
            return $this->cacheKey;
        }

        if (is_subclass_of('\\sammo\\VersionGit', '\\sammo\VersionGitDynamic')) {
            $lastModified = $this->findLastModified();
            if ($lastModified !== null) {
                $versionHash = "lt{$lastModified}";
            } else {
                $versionHash = VersionGit::getVersion();
            }
        } else {
            $versionHash = VersionGit::getVersion();
        }

        $apiVersion = static::CONST_API_VERSION;
        $serverID = UniqueConst::$serverID;


        $cacheKey = "{$apiVersion}_{$serverID}_{$versionHash}";
        $this->cacheKey = $cacheKey;

        return $cacheKey;
    }

    public function tryCache(): ?APICacheResult
    {
        if (is_subclass_of('\\sammo\\VersionGit', '\\sammo\VersionGitDynamic')) {
            return new APICacheResult(TimeUtil::secondsToDateTime($this->findLastModified()??\time(), true, true));
        }

        return new APICacheResult(null, $this->getCacheKey(), 60, true);
    }

    public function readCache(Cache $cache): null|string|array|int|float|bool
    {
        $rawJSONCache = $cache->load(static::CACHE_KEY);
        if ($rawJSONCache === null) {
            return null;
        }
        $jsonCache = Json::decode($rawJSONCache);
        if (!key_exists('cacheKey', $jsonCache)) {
            return null;
        }

        $jsonCacheKey = $jsonCache['cacheKey'];
        if ($jsonCacheKey !== $this->getCacheKey()) {
            return null;
        }

        return $jsonCache['data'] ?? null;
    }

    public function extractObjClassInfo(string $objKey, callable $callerFunction): array
    {
        /** @var \sammo\iAction */
        $target = $callerFunction($objKey);
        if (!($target instanceof \sammo\iAction)) {
            throw new \RuntimeException("{$objKey}의 대상이 iAction이 아님");
        }
        return [
            'value' => $objKey,
            'name' => $target->getName(),
            'info' => $target->getInfo(),
        ];
    }

    public function extractObjClassInfoFromArray(array $constArray, callable $callerFunction): array
    {
        $result = [];
        foreach ($constArray as $key => $target) {
            if (is_string($target)) {
                $actionInfo = $this->extractObjClassInfo($target, $callerFunction);
                if (is_string($key)) {
                    $result[$key] = $actionInfo;
                } else {
                    $result[$target] = $actionInfo;
                }
                continue;
            }
            if (is_array($target)) {
                $result[$key] = $this->extractObjClassInfoFromArray($target, $callerFunction);
                continue;
            }
            if(is_string($key) && is_int($target)){
                //역전된 상황이다.
                $result[$key] = $this->extractObjClassInfo($key, $callerFunction);
                continue;
            }
            $result[$key] = $target;
        }
        return $result;
    }

    public function extractObjClassInfoFromGameConst(string $gameConstKey, callable $callerFunction): array
    {
        $gameConstKeyList = explode(".", $gameConstKey);
        $target = (GameConst::${$gameConstKeyList[0]}) ?? [];
        foreach(\array_slice($gameConstKeyList, 1) as $gameConstSubKey){
            $target = $target[$gameConstSubKey] ?? [];
        }

        if (is_string($target)) {
            return [$target => $this->extractObjClassInfo($target, $callerFunction)];
        }

        if (!is_array($target)) {
            throw new \RuntimeException("GameConst::{$gameConstKey}의 값이 {$target}임");
        }

        return $this->extractObjClassInfoFromArray($target, $callerFunction);
    }

    public function genConstData()
    {
        /** @var array<string,array{0:string,1:string[],2?:int> */
        $gameConstKeys = [
            'nationType' => [
                '\sammo\buildNationTypeClass',
                ['availableNationType', 'neutralNationType']
            ],
            'specialDomestic' => [
                '\sammo\buildGeneralSpecialDomesticClass',
                ['defaultSpecialDomestic', 'availableSpecialDomestic', 'optionalSpecialDomestic']
            ],
            'specialWar' => [
                '\sammo\buildGeneralSpecialWarClass',
                ['defaultSpecialWar', 'availableSpecialWar', 'optionalSpecialWar']
            ],
            'personality' => [
                '\sammo\buildPersonalityClass',
                ['neutralPersonality', 'availablePersonality', 'optionalPersonality']
            ],
            'item' => [
                '\sammo\buildItemClass',
                ['allItems'],
                1
            ],
        ];
        //GameConst 중 BaseCommand는 시간 흐름, 장수에 따라 정보가 달라지므로 따로 처리해야함


        $iActionInfo = [];
        $iActionKeyMap = [];
        foreach ($gameConstKeys as $mappedKey => $callerTarget) {
            $flatLevel = 0;
            if (is_array($callerTarget)) {
                $callerFunction = $callerTarget[0];
                $gameConstSubKey = $callerTarget[1];
                if(count($callerTarget) > 2){
                    /** @var int */
                    $flatLevel = $callerTarget[2];
                }
            } else {
                $gameConstSubKey = [$mappedKey];
                $callerFunction = $callerTarget;
            }

            if (!is_callable($callerFunction)) {
                throw new \RuntimeException("{$mappedKey} => {$callerFunction}이 callable이 아님");
            }

            $actionInfo = [];
            foreach ($gameConstSubKey as $key) {
                $appendInfo = $this->extractObjClassInfoFromGameConst($key, $callerFunction);
                $actionInfo = array_merge($actionInfo, $appendInfo);
                $iActionKeyMap[$key] = $mappedKey;
            }

            if($flatLevel > 0){
                foreach(range(0, $flatLevel - 1) as $tryFlatLevel){
                    $actionInfo = array_merge(...array_values($actionInfo));
                }
            }


            $iActionInfo[$mappedKey] = $actionInfo;
        }

        $crewtypeMap = [];
        foreach(GameUnitConst::all() as $crewtypeObj){
            $crewtypeMap[$crewtypeObj->id] = [
                'value'=>(string)$crewtypeObj->id,
                'name'=>$crewtypeObj->name,
                'info'=>$crewtypeObj->getInfo(),
            ];
        }
        $iActionInfo['crewtype'] = $crewtypeMap;


        return [
            'gameConst' => get_class_vars('\sammo\GameConst'),
            'gameUnitConst' => GameUnitConst::all(),
            'cityConst' => CityConst::all(),
            'cityConstMap' => [
                'region' => CityConst::$regionMap,
                'level' => CityConst::$levelMap,
            ],
            'iActionInfo' => $iActionInfo,
            'iActionKeyMap' => $iActionKeyMap,
            'version' => VersionGit::getVersion(),
        ];
    }

    public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
    {
        $cacheDir = $this->rootPath . '/data/file_cache';

        if (!prepareDir($cacheDir)) {
            throw new RuntimeException('cache 폴더 없음');
        }
        $absolutePath = realpath($cacheDir);
        if (!$absolutePath) {
            throw new RuntimeException('cache 폴더 없음');
        }
        $storage = new \Nette\Caching\Storages\FileStorage($cacheDir);
        $cache = new Cache($storage);

        $currentCacheKey = $this->tryCache();
        if($modifiedSince !== null && $currentCacheKey->lastModified == $modifiedSince){
            return null;
        }
        if($reqEtag !== null && $currentCacheKey->etag == $reqEtag){
            return null;
        }

        $constCache = $this->readCache($cache);
        if ($constCache !== null) {
            return [
                'result' => true,
                'cacheKey' => $this->getCacheKey(),
                'data' => $constCache,
            ];
        }

        $constCache = $this->genConstData();
        $cache->save(static::CACHE_KEY, Json::encode([
            'cacheKey' => $this->getCacheKey(),
            'data' => $constCache
        ]));

        return [
            'result' => true,
            'cacheKey' => $this->getCacheKey(),
            'data' => $constCache,
        ];
    }
}
