<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Service;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use MauticPlugin\DealFlowBundle\Integration\DealFlowIntegration;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;

final class UpdateService
{
    private const CACHE_KEY = 'dealflow_update_check';
    private const CACHE_TTL = 86400; // 24 hours
    private const API_CONNECT_TIMEOUT = 5;
    private const API_READ_TIMEOUT = 15;
    private const ALLOWED_DOWNLOAD_HOST = 'mauticdealflow.com';

    private ?Client $httpClient = null;

    public function __construct(
        private readonly LicenseService $licenseService,
        private readonly CoreParametersHelper $params,
        private readonly CacheItemPoolInterface $cache,
        private readonly LoggerInterface $logger,
        private readonly IntegrityService $integrityService,
        private readonly IntegrationHelper $integrationHelper,
    ) {
    }

    public function checkForUpdates(bool $forceRefresh = false): UpdateCheckResult
    {
        $currentVersion = $this->getCurrentVersion();

        if (!$forceRefresh) {
            $cached = $this->getCachedResult($currentVersion);
            if ($cached !== null) {
                return $cached;
            }
        }

        $key = $this->licenseService->getCurrentKey();
        if ($key === null) {
            return new UpdateCheckResult(
                updateAvailable: false,
                latestVersion: $currentVersion,
                currentVersion: $currentVersion,
                error: true,
                errorMessage: 'No license key configured',
            );
        }

        $subdomain = $this->licenseService->getSubdomain();
        if ($subdomain === null) {
            return new UpdateCheckResult(
                updateAvailable: false,
                latestVersion: $currentVersion,
                currentVersion: $currentVersion,
                error: true,
                errorMessage: 'Could not determine instance hostname',
            );
        }

        $apiUrl = $this->getApiUrl();
        if ($apiUrl === null) {
            return new UpdateCheckResult(
                updateAvailable: false,
                latestVersion: $currentVersion,
                currentVersion: $currentVersion,
                error: true,
                errorMessage: 'License API URL not configured or invalid',
            );
        }

        try {
            $response = $this->getHttpClient()->get($apiUrl.'/api/updates/check', [
                'query' => [
                    'key' => $key,
                    'subdomain' => $subdomain,
                    'current_version' => $currentVersion,
                ],
                'connect_timeout' => self::API_CONNECT_TIMEOUT,
                'timeout' => self::API_READ_TIMEOUT,
                'http_errors' => false,
            ]);

            $statusCode = $response->getStatusCode();
            $body = json_decode((string) $response->getBody(), true);

            if ($statusCode === 403) {
                $message = (string) ($body['error'] ?? 'License validation failed');

                return new UpdateCheckResult(
                    updateAvailable: false,
                    latestVersion: $currentVersion,
                    currentVersion: $currentVersion,
                    error: true,
                    errorMessage: $message,
                );
            }

            if ($statusCode !== 200 || !\is_array($body)) {
                return new UpdateCheckResult(
                    updateAvailable: false,
                    latestVersion: $currentVersion,
                    currentVersion: $currentVersion,
                    error: true,
                    errorMessage: 'Unexpected response from update server (HTTP '.$statusCode.')',
                );
            }

            $result = new UpdateCheckResult(
                updateAvailable: (bool) ($body['update_available'] ?? false),
                latestVersion: (string) ($body['latest_version'] ?? $currentVersion),
                currentVersion: $currentVersion,
                downloadUrl: isset($body['download_url']) ? (string) $body['download_url'] : null,
                changelog: isset($body['changelog']) ? (string) $body['changelog'] : null,
                sha256: isset($body['sha256']) ? (string) $body['sha256'] : null,
                requiresMigration: (bool) ($body['requires_migration'] ?? false),
                eligible: (bool) ($body['eligible'] ?? true),
                updatesUntil: isset($body['updates_until']) ? (string) $body['updates_until'] : null,
            );

            $this->cacheResult($result);
            $this->logger->info('Deal Flow update check: '.($result->updateAvailable ? 'update available ('.$result->latestVersion.')' : 'up to date'));

            return $result;
        } catch (GuzzleException $e) {
            $this->logger->warning('Deal Flow update check failed (API unreachable): '.$e->getMessage());

            return new UpdateCheckResult(
                updateAvailable: false,
                latestVersion: $currentVersion,
                currentVersion: $currentVersion,
                error: true,
                errorMessage: 'Could not reach update server - please try again later',
            );
        }
    }

    public function getCurrentVersion(): string
    {
        $configPath = \dirname(__DIR__).'/Config/config.php';
        if (!file_exists($configPath)) {
            return '0.0.0';
        }

        $config = require $configPath;
        if (\is_array($config) && isset($config['version'])) {
            return (string) $config['version'];
        }

        return '0.0.0';
    }

    public function performUpdate(string $downloadUrl, string $expectedSha256): UpdateResult
    {
        // Always do a fresh API check to get current download URL and SHA256.
        // The form values may be from a stale cache.
        $freshCheck = $this->checkForUpdates(true);
        if (!$freshCheck->updateAvailable) {
            return new UpdateResult(false, 'No update available (already up to date)');
        }
        if ($freshCheck->downloadUrl === null || $freshCheck->sha256 === null) {
            return new UpdateResult(false, 'Update check did not return a download URL');
        }

        $downloadUrl = $freshCheck->downloadUrl;
        $expectedSha256 = $freshCheck->sha256;

        // Domain pinning: verify the download URL starts with the allowed host
        $parsedUrl = parse_url($downloadUrl);
        $host = $parsedUrl['host'] ?? '';
        if ($host !== self::ALLOWED_DOWNLOAD_HOST && !str_ends_with($host, '.'.self::ALLOWED_DOWNLOAD_HOST)) {
            return new UpdateResult(false, 'Download URL is not from an allowed domain');
        }

        if (($parsedUrl['scheme'] ?? '') !== 'https') {
            return new UpdateResult(false, 'Download URL must use HTTPS');
        }

        $pluginDir = \dirname(__DIR__);
        $pluginsDir = \dirname($pluginDir);
        $tempFile = sys_get_temp_dir().'/DealFlowBundle-update-'.time().'.zip';

        // Step 1: Download the zip
        try {
            $response = $this->getHttpClient()->get($downloadUrl, [
                'connect_timeout' => self::API_CONNECT_TIMEOUT,
                'timeout' => 60,
                'sink' => $tempFile,
                'http_errors' => false,
            ]);

            if ($response->getStatusCode() !== 200) {
                @unlink($tempFile);

                return new UpdateResult(false, 'Download failed with HTTP '.$response->getStatusCode());
            }
        } catch (GuzzleException $e) {
            @unlink($tempFile);
            $this->logger->error('Deal Flow update download failed: '.$e->getMessage());

            return new UpdateResult(false, 'Download failed: '.$e->getMessage());
        }

        // Step 2: Verify SHA256
        $actualHash = hash_file('sha256', $tempFile);
        if ($actualHash !== $expectedSha256) {
            @unlink($tempFile);
            $this->logger->error('Deal Flow update SHA256 mismatch: expected '.$expectedSha256.', got '.$actualHash);

            return new UpdateResult(false, 'Integrity check failed - file hash does not match');
        }

        // Step 3: Verify it's a valid zip
        $zip = new \ZipArchive();
        $openResult = $zip->open($tempFile);
        if ($openResult !== true) {
            @unlink($tempFile);

            return new UpdateResult(false, 'Downloaded file is not a valid zip archive');
        }

        // Verify the zip contains a DealFlowBundle directory
        $hasBundleDir = false;
        for ($i = 0; $i < $zip->numFiles; ++$i) {
            $name = $zip->getNameIndex($i);
            if ($name !== false && str_starts_with($name, 'DealFlowBundle/')) {
                $hasBundleDir = true;
                break;
            }
        }

        if (!$hasBundleDir) {
            $zip->close();
            @unlink($tempFile);

            return new UpdateResult(false, 'Update package does not contain a DealFlowBundle directory');
        }

        $zip->close();

        // Step 4: Backup current plugin directory
        $backupDir = $pluginsDir.'/DealFlowBundle.bak.'.time();
        if (!@rename($pluginDir, $backupDir)) {
            @unlink($tempFile);

            return new UpdateResult(false, 'Failed to create backup — the plugin directory is not writable. Run: chown -R www-data:www-data '.$pluginsDir);
        }

        // Step 5: Extract the zip
        $zip = new \ZipArchive();
        $zip->open($tempFile);
        $extractResult = $zip->extractTo($pluginsDir);
        $zip->close();
        @unlink($tempFile);

        if (!$extractResult) {
            // Attempt to restore from backup
            @rename($backupDir, $pluginDir);

            return new UpdateResult(false, 'Failed to extract update package');
        }

        // Verify the extraction created the plugin directory
        if (!is_dir($pluginDir)) {
            @rename($backupDir, $pluginDir);

            return new UpdateResult(false, 'Extraction did not create the expected plugin directory');
        }

        // Read the new version from the extracted config
        $newVersion = $this->getCurrentVersion();

        // Clear the update cache so a fresh check happens next time
        $this->clearUpdateCache();

        // Clear Symfony's compiled container cache — the new code may have different service definitions
        $cacheDir = \dirname(__DIR__, 5).'/var/cache/prod';
        if (is_dir($cacheDir)) {
            $this->removeDirectory($cacheDir);
            $this->logger->info('Deal Flow: cleared Symfony cache after update');
        }

        // Recalculate file checksum so the integrity warning clears
        $this->recalculateFileChecksum();

        $this->logger->info('Deal Flow plugin updated successfully to '.$newVersion);

        return new UpdateResult(true, 'Successfully updated to v'.$newVersion, $newVersion);
    }

    public function rollback(): UpdateResult
    {
        $pluginDir = \dirname(__DIR__);
        $pluginsDir = \dirname($pluginDir);

        // Find the most recent backup
        $backups = glob($pluginsDir.'/DealFlowBundle.bak.*');
        if ($backups === false || $backups === []) {
            return new UpdateResult(false, 'No backup found to rollback to');
        }

        // Sort by timestamp (most recent first)
        usort($backups, static function (string $a, string $b): int {
            $timestampA = (int) substr($a, strrpos($a, '.') + 1);
            $timestampB = (int) substr($b, strrpos($b, '.') + 1);

            return $timestampB <=> $timestampA;
        });

        $latestBackup = $backups[0];

        // Remove the current plugin directory
        if (is_dir($pluginDir)) {
            $failedDir = $pluginsDir.'/DealFlowBundle.failed.'.time();
            if (!@rename($pluginDir, $failedDir)) {
                return new UpdateResult(false, 'Failed to move current plugin directory out of the way');
            }
        }

        // Restore from backup
        if (!@rename($latestBackup, $pluginDir)) {
            return new UpdateResult(false, 'Failed to restore backup from '.$latestBackup);
        }

        $restoredVersion = $this->getCurrentVersion();
        $this->clearUpdateCache();

        $this->logger->info('Deal Flow plugin rolled back to '.$restoredVersion);

        return new UpdateResult(true, 'Rolled back successfully to v'.$restoredVersion, $restoredVersion);
    }

    /**
     * Recalculate the file checksum after an update so the integrity warning clears.
     * Updates the Integration entity's featureSettings if available.
     */
    private function recalculateFileChecksum(): void
    {
        try {
            $newChecksum = $this->integrityService->calculateFileChecksum();

            // Update Integration featureSettings
            $integration = $this->integrationHelper->getIntegrationObject('DealFlow');
            if ($integration instanceof DealFlowIntegration) {
                $entity = $integration->getIntegrationSettings();
                $settings = $entity->getFeatureSettings() ?? [];
                $settings['file_checksum'] = $newChecksum;
                $entity->setFeatureSettings($settings);
                // Entity will be flushed by the controller or next request
            }

            $this->logger->info('Deal Flow: file checksum recalculated after update');
        } catch (\Throwable $e) {
            $this->logger->warning('Deal Flow: failed to recalculate file checksum: '.$e->getMessage());
        }
    }

    public function clearUpdateCache(): void
    {
        try {
            $this->cache->deleteItem(self::CACHE_KEY);
        } catch (\Throwable $e) {
            $this->logger->debug('Deal Flow: failed to clear update cache: '.$e->getMessage());
        }
    }

    private function getApiUrl(): ?string
    {
        $url = (string) $this->params->get(
            'dealflow_license_api_url',
            'https://deal-flow-license-api-700460720657.us-east1.run.app'
        );

        if (!str_starts_with($url, 'https://')) {
            return null;
        }

        return rtrim($url, '/');
    }

    private function getHttpClient(): Client
    {
        if ($this->httpClient === null) {
            $this->httpClient = new Client();
        }

        return $this->httpClient;
    }

    private function cacheResult(UpdateCheckResult $result): void
    {
        try {
            $cacheItem = $this->cache->getItem(self::CACHE_KEY);
            $cacheItem->set([
                'updateAvailable' => $result->updateAvailable,
                'latestVersion' => $result->latestVersion,
                'currentVersion' => $result->currentVersion,
                'downloadUrl' => $result->downloadUrl,
                'changelog' => $result->changelog,
                'sha256' => $result->sha256,
                'requiresMigration' => $result->requiresMigration,
                'eligible' => $result->eligible,
                'updatesUntil' => $result->updatesUntil,
            ]);
            $cacheItem->expiresAfter(self::CACHE_TTL);
            $this->cache->save($cacheItem);
        } catch (\Throwable $e) {
            $this->logger->debug('Deal Flow: failed to cache update check result: '.$e->getMessage());
        }
    }

    private function getCachedResult(string $currentVersion): ?UpdateCheckResult
    {
        try {
            $cacheItem = $this->cache->getItem(self::CACHE_KEY);
            if ($cacheItem->isHit()) {
                $data = $cacheItem->get();
                if (\is_array($data) && \array_key_exists('updateAvailable', $data)) {
                    // Invalidate cache if current version changed (e.g., manual update)
                    if (isset($data['currentVersion']) && $data['currentVersion'] !== $currentVersion) {
                        $this->cache->deleteItem(self::CACHE_KEY);

                        return null;
                    }

                    return new UpdateCheckResult(
                        updateAvailable: (bool) $data['updateAvailable'],
                        latestVersion: (string) ($data['latestVersion'] ?? $currentVersion),
                        currentVersion: $currentVersion,
                        downloadUrl: isset($data['downloadUrl']) ? (string) $data['downloadUrl'] : null,
                        changelog: isset($data['changelog']) ? (string) $data['changelog'] : null,
                        sha256: isset($data['sha256']) ? (string) $data['sha256'] : null,
                        requiresMigration: (bool) ($data['requiresMigration'] ?? false),
                        eligible: (bool) ($data['eligible'] ?? true),
                        updatesUntil: isset($data['updatesUntil']) ? (string) $data['updatesUntil'] : null,
                        fromCache: true,
                    );
                }
            }
        } catch (\Throwable $e) {
            $this->logger->debug('Deal Flow: update cache read failed: '.$e->getMessage());
        }

        return null;
    }

    private function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }

        $items = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($items as $item) {
            if ($item->isDir()) {
                @rmdir($item->getRealPath());
            } else {
                @unlink($item->getRealPath());
            }
        }

        @rmdir($dir);
    }
}
