<?php

declare(strict_types=1);

namespace StaticPHP\Artifact;

use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Util\FileSystem;

class ArtifactCache
{
    /**
     * @var array<string, array{
     *     source: null|array{
     *         lock_type: 'binary'|'source',
     *         cache_type: 'archive'|'file'|'git'|'local',
     *         filename?: string,
     *         dirname?: string,
     *         extract: null|'&custom'|string,
     *         hash: null|string,
     *         time: int,
     *         downloader: null|string
     *     },
     *     binary: array{
     *         windows-x86_64?: null|array{
     *             lock_type: 'binary'|'source',
     *             cache_type: 'archive'|'file'|'git'|'local',
     *             filename?: string,
     *             dirname?: string,
     *             extract: null|'&custom'|string,
     *             hash: null|string,
     *             time: int,
     *             version?: null|string,
     *             downloader: null|string
     *         }
     *     }
     * }>
     */
    protected array $cache = [];

    /**
     * @param string $cache_file Lock file position
     */
    public function __construct(protected string $cache_file = DOWNLOAD_PATH . '/.cache.json')
    {
        if (!file_exists($this->cache_file)) {
            logger()->debug("Cache file does not exist, creating new one at {$this->cache_file}");
            FileSystem::createDir(dirname($this->cache_file));
            file_put_contents($this->cache_file, json_encode([]));
        } else {
            $content = file_get_contents($this->cache_file);
            $this->cache = json_decode($content ?: '{}', true) ?? [];
        }
    }

    /**
     * Checks if the source of an artifact is already downloaded.
     *
     * @param string $artifact_name Artifact name
     * @param bool   $compare_hash  Whether to compare hash of the downloaded source
     */
    public function isSourceDownloaded(string $artifact_name, bool $compare_hash = false): bool
    {
        $item = $this->cache[$artifact_name] ?? null;
        if ($item === null) {
            return false;
        }
        return $this->isObjectDownloaded($this->cache[$artifact_name]['source'] ?? null, $compare_hash);
    }

    /**
     * Check if the binary of an artifact for target OS is already downloaded.
     *
     * @param string $artifact_name Artifact name
     * @param string $target_os     Target OS (accepts {windows|linux|macos}-{x86_64|aarch64})
     * @param bool   $compare_hash  Whether to compare hash of the downloaded binary
     */
    public function isBinaryDownloaded(string $artifact_name, string $target_os, bool $compare_hash = false): bool
    {
        $item = $this->cache[$artifact_name] ?? null;
        if ($item === null) {
            return false;
        }
        return $this->isObjectDownloaded($this->cache[$artifact_name]['binary'][$target_os] ?? null, $compare_hash);
    }

    /**
     * Lock the downloaded artifact info into cache.
     *
     * @param Artifact|string   $artifact        Artifact instance
     * @param 'binary'|'source' $lock_type       Lock type ('source'|'binary')
     * @param DownloadResult    $download_result Download result object
     * @param null|string       $platform        Target platform string for binary lock, null for source lock
     */
    public function lock(Artifact|string $artifact, string $lock_type, DownloadResult $download_result, ?string $platform = null): void
    {
        $artifact_name = $artifact instanceof Artifact ? $artifact->getName() : $artifact;
        if (!isset($this->cache[$artifact_name])) {
            $this->cache[$artifact_name] = [
                'source' => null,
                'binary' => [],
            ];
        }
        $obj = null;
        if ($download_result->cache_type === 'archive') {
            $obj = [
                'lock_type' => $lock_type,
                'cache_type' => 'archive',
                'filename' => $download_result->filename,
                'extract' => $download_result->extract,
                'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
                'time' => time(),
                'version' => $download_result->version,
                'config' => $download_result->config,
                'downloader' => $download_result->downloader,
            ];
        } elseif ($download_result->cache_type === 'file') {
            $obj = [
                'lock_type' => $lock_type,
                'cache_type' => 'file',
                'filename' => $download_result->filename,
                'extract' => $download_result->extract,
                'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
                'time' => time(),
                'version' => $download_result->version,
                'config' => $download_result->config,
                'downloader' => $download_result->downloader,
            ];
        } elseif ($download_result->cache_type === 'git') {
            $obj = [
                'lock_type' => $lock_type,
                'cache_type' => 'git',
                'dirname' => $download_result->dirname,
                'extract' => $download_result->extract,
                'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')),
                'time' => time(),
                'version' => $download_result->version,
                'config' => $download_result->config,
                'downloader' => $download_result->downloader,
            ];
        } elseif ($download_result->cache_type === 'local') {
            $obj = [
                'lock_type' => $lock_type,
                'cache_type' => 'local',
                'dirname' => $download_result->dirname,
                'extract' => $download_result->extract,
                'hash' => null,
                'time' => time(),
                'version' => $download_result->version,
                'config' => $download_result->config,
                'downloader' => $download_result->downloader,
            ];
        }
        if ($obj === null) {
            throw new SPCInternalException("Invalid download result for locking artifact {$artifact_name}");
        }
        if ($lock_type === 'binary') {
            if ($platform === null) {
                throw new SPCInternalException("Invalid download result for locking binary artifact {$artifact_name}: platform cannot be null");
            }
            $obj['platform'] = $platform;
        }
        if ($lock_type === 'source') {
            $this->cache[$artifact_name]['source'] = $obj;
        } elseif ($lock_type === 'binary') {
            $this->cache[$artifact_name]['binary'][$platform] = $obj;
        } else {
            throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}");
        }
        // save cache to file
        file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }

    /**
     * Get source cache info for an artifact.
     *
     * @param  string     $artifact_name Artifact name
     * @return null|array Cache info array or null if not found
     */
    public function getSourceInfo(string $artifact_name): ?array
    {
        return $this->cache[$artifact_name]['source'] ?? null;
    }

    /**
     * Get binary cache info for an artifact on specific platform.
     *
     * @param string $artifact_name Artifact name
     * @param string $platform      Platform string (e.g., 'linux-x86_64')
     * @return null|array{
     *     lock_type: 'binary'|'source',
     *     cache_type: 'archive'|'git'|'local',
     *     filename?: string,
     *     extract: null|'&custom'|string,
     *     hash: null|string,
     *     dirname?: string,
     *     version?: null|string
     * } Cache info array or null if not found
     */
    public function getBinaryInfo(string $artifact_name, string $platform): ?array
    {
        return $this->cache[$artifact_name]['binary'][$platform] ?? null;
    }

    /**
     * Get all binary cache entries for an artifact, keyed by platform string.
     *
     * @param  string               $artifact_name Artifact name
     * @return array<string, array> Map of platform → cache info (may be empty)
     */
    public function getAllBinaryInfo(string $artifact_name): array
    {
        return $this->cache[$artifact_name]['binary'] ?? [];
    }

    /**
     * Get the full path to the cached file/directory.
     *
     * @param  array  $cache_info Cache info from getSourceInfo() or getBinaryInfo()
     * @return string Full path to the cached file or directory
     */
    public function getCacheFullPath(array $cache_info): string
    {
        return match ($cache_info['cache_type']) {
            'archive', 'file' => FileSystem::convertPath(DOWNLOAD_PATH . '/' . $cache_info['filename']),
            'git' => FileSystem::convertPath(DOWNLOAD_PATH . '/' . $cache_info['dirname']),
            'local' => $cache_info['dirname'], // local dirname is absolute path
            default => throw new SPCInternalException("Unknown cache type: {$cache_info['cache_type']}"),
        };
    }

    /**
     * Remove source cache entry for an artifact.
     *
     * @param string $artifact_name Artifact name
     * @param bool   $delete_file   Whether to also delete the cached file/directory
     */
    public function removeSource(string $artifact_name, bool $delete_file = false): void
    {
        $source_info = $this->getSourceInfo($artifact_name);
        if ($source_info === null) {
            return;
        }

        // Optionally delete the actual file/directory
        if ($delete_file) {
            $path = $this->getCacheFullPath($source_info);
            if (in_array($source_info['cache_type'], ['archive', 'file']) && file_exists($path)) {
                unlink($path);
                logger()->debug("Deleted cached archive: {$path}");
            } elseif ($source_info['cache_type'] === 'git' && is_dir($path)) {
                FileSystem::removeDir($path);
                logger()->debug("Deleted cached git repository: {$path}");
            }
        }

        // Remove from cache
        $this->cache[$artifact_name]['source'] = null;
        $this->save();
        logger()->debug("Removed source cache entry for [{$artifact_name}]");
    }

    /**
     * Remove binary cache entry for an artifact on specific platform.
     *
     * @param string $artifact_name Artifact name
     * @param string $platform      Platform string (e.g., 'linux-x86_64')
     * @param bool   $delete_file   Whether to also delete the cached file/directory
     */
    public function removeBinary(string $artifact_name, string $platform, bool $delete_file = false): void
    {
        $binary_info = $this->getBinaryInfo($artifact_name, $platform);
        if ($binary_info === null) {
            return;
        }

        // Optionally delete the actual file/directory
        if ($delete_file) {
            $path = $this->getCacheFullPath($binary_info);
            if (in_array($binary_info['cache_type'], ['archive', 'file']) && file_exists($path)) {
                unlink($path);
                logger()->debug("Deleted cached binary archive: {$path}");
            } elseif ($binary_info['cache_type'] === 'git' && is_dir($path)) {
                FileSystem::removeDir($path);
                logger()->debug("Deleted cached binary git repository: {$path}");
            }
        }

        // Remove from cache
        unset($this->cache[$artifact_name]['binary'][$platform]);
        $this->save();
        logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]");
    }

    /**
     * Get the names of all artifacts that have at least one downloaded entry (source or binary).
     *
     * @return array<string> Artifact names
     */
    public function getCachedArtifactNames(): array
    {
        return array_keys($this->cache);
    }

    /**
     * Save cache to file.
     */
    public function save(): void
    {
        file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }

    private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool
    {
        if ($object === null) {
            return false;
        }
        // check if source is cached and file/dir exists in downloads/ dir
        return match ($object['cache_type'] ?? null) {
            'archive', 'file' => isset($object['filename']) &&
                file_exists(DOWNLOAD_PATH . '/' . $object['filename']) &&
                (!$compare_hash || (
                    isset($object['hash']) &&
                    sha1_file(DOWNLOAD_PATH . '/' . $object['filename']) === $object['hash']
                )),
            'git' => isset($object['dirname']) &&
                is_dir(DOWNLOAD_PATH . '/' . $object['dirname'] . '/.git') &&
                (!$compare_hash || (
                    isset($object['hash']) &&
                    trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $object['dirname']) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')) === $object['hash']
                )),
            'local' => isset($object['dirname']) &&
                is_dir($object['dirname']), // local dirname is absolute path
            default => false,
        };
    }
}
