<?php

declare(strict_types=1);

namespace EasyChat\Library\Update;

use EasyChat\Library\Http\ExternalHttpClient;
use EasyChat\Library\Logger;
use EasyChat\Library\SafeRuntime;

final class GenericUpdateInstaller
{
    private $registry;
    private VerifiedArtifactDownloader $downloader;
    private SafeUpdateArchive $archive;

    public function __construct($registry, ?VerifiedArtifactDownloader $downloader = null, ?SafeUpdateArchive $archive = null)
    {
        $this->registry = $registry;
        $this->downloader = $downloader ?: new VerifiedArtifactDownloader(new ExternalHttpClient($registry));
        $this->archive = $archive ?: new SafeUpdateArchive();
    }

     
    public function install(array $artifact, string $version): array
    {
        $lock = $this->acquireLock();
        if (!is_resource($lock)) {
            $this->audit('install_rejected', $version, 'update_already_running');
            return ['success' => false, 'error' => 'update_already_running'];
        }

        $work = '';
        try {
            $this->audit('install_started', $version);
            $work = $this->createWorkDirectory();
            $package = $this->downloader->download($artifact, $work);
            $source = $this->archive->extractGeneric($package, $work . '/extracted');
            if (!class_exists('EasyChatOpenCartRuntimePublisher')) {
                require_once SITE_PATH . '/library/update/runtime_publisher.php';
            }
            $publisher = new \EasyChatOpenCartRuntimePublisher(0);
             
             
            if (function_exists('easychat_runtime_reader_lock_release')
                && !easychat_runtime_reader_lock_release()
            ) {
                $this->audit('install_failed', $version, 'update_reader_release_failed');
                return ['success' => false, 'error' => 'update_reader_release_failed'];
            }
            if (!$publisher->publish($source, SITE_PATH, $version)) {
                $error = (string)$publisher->lastError();
                $retryAfter = $error === 'legacy_drain_pending'
                    ? max(1, (int)$publisher->legacyDrainReadyAt() - time())
                    : 0;
                $result = [
                    'success' => false,
                    'error' => $error !== '' ? $error : 'update_publish_failed',
                    'retry_after' => $retryAfter,
                ];
                if ($error === 'legacy_drain_pending') {
                    $result['resume_token'] = (string)$publisher->legacyDrainResumeToken();
                    $this->audit('legacy_drain_pending', $version, $error, ['retry_after' => $retryAfter]);
                } else {
                    $this->audit('install_failed', $version, $result['error']);
                }
                return $result;
            }

            $this->audit('install_succeeded', $version);
            return ['success' => true, 'version' => $version, 'reload_url' => 'index.php?route=update'];
        } catch (\Throwable $e) {
            $error = $this->safeError($e->getMessage());
            $this->audit('install_failed', $version, $error);
            return ['success' => false, 'error' => $error];
        } finally {
            if ($work !== '') {
                $this->removeTree($work);
            }
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

    private function acquireLock()
    {
        $directory = SITE_PATH . '/log';
        if ((!is_dir($directory) && !mkdir($directory, 0700, true)) || is_link($directory)) {
            return false;
        }
        $path = $directory . '/easychat-update-request.lock';
        $handle = fopen($path, 'c+b');
        if (!is_resource($handle)) {
            return false;
        }
        chmod($path, 0600);
        if (!flock($handle, LOCK_EX | LOCK_NB)) {
            fclose($handle);
            return false;
        }
        return $handle;
    }

    private function createWorkDirectory(): string
    {
        $base = rtrim(sys_get_temp_dir(), '/\\');
        for ($attempt = 0; $attempt < 5; $attempt++) {
            $path = $base . '/easychat-update-' . bin2hex(random_bytes(16));
            if (mkdir($path, 0700) && chmod($path, 0700)) {
                return $path;
            }
        }
        throw new \RuntimeException('update_work_directory_unavailable');
    }

    private function removeTree(string $path): void
    {
        if (!file_exists($path) && !is_link($path)) {
            return;
        }
        if (is_link($path) || !is_dir($path)) {
            SafeRuntime::unlink($path);
            return;
        }
        $items = scandir($path);
        if (is_array($items)) {
            foreach ($items as $item) {
                if ($item !== '.' && $item !== '..') {
                    $this->removeTree($path . '/' . $item);
                }
            }
        }
        SafeRuntime::rmdir($path);
    }

    private function safeError(string $error): string
    {
        return preg_match('/^[a-z0-9_]{3,80}$/D', $error) ? $error : 'update_install_failed';
    }

     
    private function audit(string $event, string $version, string $error = '', array $context = []): void
    {
        $record = [
            'event' => $event,
            'target_version' => preg_match('/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$/D', $version) ? $version : 'invalid',
        ];
        if ($error !== '') {
            $record['error'] = $this->safeError($error);
        }
        foreach ($context as $key => $value) {
            if (preg_match('/^[a-z_]{2,40}$/D', (string)$key)) {
                $record[(string)$key] = $value;
            }
        }
        try {
            Logger::info(json_encode($record, JSON_UNESCAPED_SLASHES), 'update.log');
        } catch (\Throwable $ignored) {
             
             
        }
    }
}
