<?php

declare(strict_types=1);

class Awardit_Magento_Cache {
    const DEFAULT_LIFETIME = 7200;
    const INVALIDATED_TYPES_KEY = 'core_cache_invalidate';
    const XML_PATH_TYPES = "global/cache/types";

    private string $idPrefix = "";
    private Zend_Cache_Core $frontend;

    /**
     * @param array{backend?:class-string, backend_options?:array, id_prefix?:string} $options
     */
    public function __construct(array $options = []) {
        $backend = $options["backend"] ?? Awardit_Magento_Cache_ProcessMemory::class;
        $backendOptions = $options["backend_options"] ?? [];

        $this->idPrefix = $options["id_prefix"] ?? "";
        $this->frontend = Zend_Cache::factory(
            Varien_Cache_Core::class,
            $backend,
            // Frontend options
            [
                "caching" => true,
                "automatic_cleaning_factor" => 0,
                "lifetime" => self::DEFAULT_LIFETIME,
                // We trust our cache, do not check writes:
                "write_control" => false,
                // "logging" => true,
                // "logger" => new Awardit_Magento_Logger("cache"),
            ],
            $backendOptions,
            true,
            true,
            true
        );
    }

    private function _id(string $id): string {
        return $this->idPrefix.strtoupper($id);
    }

    /**
     * @param Array<string> $tags
     */
    private function _tags(array $tags): array {
        return array_map(function(string $tag): string {
            return $this->idPrefix.strtoupper($tag);
        }, $tags);
    }

    public function getFrontend(): Zend_Cache_Core {
        return $this->frontend;
    }

    public function load(string $id): mixed {
        return $this->frontend->load($this->_id($id));
    }

    /**
     * @param Array<string> $tags
     */
    public function save(
        string $data,
        string $id,
        array $tags = [],
        int|false $lifeTime = null
    ): bool {
        return $this->frontend->save($data, $this->_id($id), $this->_tags($tags), $lifeTime ?: 0);
    }

    public function test(string $id): int|false {
        return $this->frontend->test($this->_id($id));
    }

    public function remove(string $id): bool {
        return $this->frontend->remove($this->_id($id));
    }

    /**
     * @param Array<string>|string $tags
     */
    public function clean(array|string $tags = []): bool {
        $mode = Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG;

        if(!empty($tags)) {
            if(!is_array($tags)) {
                $tags = [$tags];
            }

            return $this->frontend->clean($mode, $this->_tags($tags));
        }

        // Clean all
        return $this->frontend->clean();
    }

    public function flush(array $tags = []): bool {
        return $this->frontend->clean();
    }

    public function getDbAdapter(): Zend_Db_Adapter_Abstract {
        throw new Exception(__METHOD__.": Will not be implemented");
    }

    public function saveOptions(array $options): self {
        // Do nothing, we do not allow disabling cache through admin
        return $this;
    }

    public function canUse(string $typeCode): bool {
        // We can always use the cache
        return true;
    }

    // Unused
    // public function banUse(string $typeCode): bool

    // Unused outside class
    // public
    /**
     * @return Array<string>
     */
    private function getTagsByType(string $type): array {
        $path = self::XML_PATH_TYPES . '/' . $type . '/tags';
        $tagsConfig = Mage::getConfig()->getNode($path);

        if($tagsConfig) {
            return explode(',', (string) $tagsConfig);
        }

        return [];
    }

    public function getTypes(): array {
        $types = [];
        $config = Mage::getConfig()->getNode(self::XML_PATH_TYPES);

        if($config) {
            foreach($config->children() as $type => $node) {
                /**
                 * @var string $type
                 */
                $types[$type] = new Varien_Object([
                    "id" => $type,
                    "cache_type" => Mage::helper("core")->__((string)$node->label),
                    "description" => Mage::helper("core")->__((string)$node->description),
                    "tags" => strtoupper((string) $node->tags),
                    "status" => (int)$this->canUse($type),
                ]);
            }
        }

        return $types;
    }

    public function getInvalidatedTypes(): array {
        $invalidatedTypes = [];
        $types = $this->_getInvalidatedTypes();

        if($types) {
            $allTypes = $this->getTypes();

            foreach($types as $type => $flag) {
                if(array_key_exists($type, $allTypes) && $this->canUse($type)) {
                    $invalidatedTypes[$type] = $allTypes[$type];
                }
            }
        }

        return $invalidatedTypes;
    }

    public function invalidateType(string|array $typeCode): self {
        $types = $this->_getInvalidatedTypes();

        if (!is_array($typeCode)) {
            $typeCode = [$typeCode];
        }

        foreach ($typeCode as $code) {
            $types[$code] = 1;
        }

        $this->_saveInvalidatedTypes($types);

        return $this;
    }

    public function cleanType(string $typeCode): self {
        $this->clean($this->getTagsByType($typeCode));

        $types = $this->_getInvalidatedTypes();

        unset($types[$typeCode]);

        $this->_saveInvalidatedTypes($types);

        return $this;
    }

    public function processRequest(): bool {
        // We are never using canned responses
        return false;
    }

    /**
     * @return Array<string, 1>
     */
    private function _getInvalidatedTypes(): array {
        $types = $this->load(self::INVALIDATED_TYPES_KEY);

        if($types) {
            /**
             * @return Array<string, 1>
             */
            return json_decode($types, true);
        }

        return [];
    }

    /**
     * @param Array<string, 1> $types
     */
    private function _saveInvalidatedTypes(array $types): void {
        $this->save(json_encode($types), self::INVALIDATED_TYPES_KEY);
    }
}
