<?php

declare(strict_types=1);

namespace ShieldX\Core;

defined('ABSPATH') || exit;

use ShieldX\Maintenance\RestorePointManager;
use ShieldX\Security\AuditLogger;
use ShieldX\Security\Lockdown;
use ShieldX\Support\RuntimeCacheInvalidator;

final class UpdateCoordinator
{
    private const OPTION_NAME = 'shieldx_update_coordination';

    /** @var Lockdown */
    private $lockdown;

    /** @var AuditLogger|null */
    private $logger;

    /** @var RestorePointManager|null */
    private $restorePoints;

    /** @var bool */
    private $suspendedThisRequest = false;

    /** @var bool */
    private $resumedThisRequest = false;

    public function __construct(Lockdown $lockdown, ?AuditLogger $logger = null, ?RestorePointManager $restorePoints = null)
    {
        $this->lockdown = $lockdown;
        $this->logger = $logger;
        $this->restorePoints = $restorePoints;
    }

    public static function fromWordPress(Lockdown $lockdown, ?AuditLogger $logger = null): self
    {
        return new self($lockdown, $logger);
    }

    public function register(): void
    {
        if (function_exists('add_filter')) {
            add_filter('upgrader_pre_install', [$this, 'beforeInstall'], 5, 2);
            add_filter('upgrader_post_install', [$this, 'afterInstall'], 50, 3);
        }

        if (function_exists('add_action')) {
            add_action('upgrader_process_complete', [$this, 'afterProcessComplete'], 50, 2);
            add_action('automatic_updates_complete', [$this, 'afterAutomaticUpdates'], 50, 1);
        }

        register_shutdown_function([$this, 'shutdownResume']);
    }

    /**
     * @param mixed               $response
     * @param array<string,mixed> $hookExtra
     * @return mixed
     */
    public function beforeInstall($response, array $hookExtra)
    {
        if (!$this->isUpdateOperation($hookExtra)) {
            return $response;
        }

        $state = self::state();
        if (!empty($state['active'])) {
            return $response;
        }

        $operation = $this->operationLabel($hookExtra);
        try {
            $restorePoints = $this->restorePoints();
            $restorePoint = $restorePoints->create('pre_update:' . $operation, $restorePoints->updatePathsFromHookExtra($hookExtra), 0, false);
        } catch (\Throwable $error) {
            $restorePoint = [
                'ok' => false,
                'message' => $error->getMessage(),
                'restore_id' => '',
                'files' => 0,
                'truncated' => false,
            ];
        }
        $this->log('update.restore_point_created', [
            'operation' => $operation,
            'restore_id' => (string) ($restorePoint['restore_id'] ?? ''),
            'files' => (int) ($restorePoint['files'] ?? 0),
            'truncated' => !empty($restorePoint['truncated']),
        ]);

        $status = $this->lockdown->status();
        if (empty($status['enabled'])) {
            $this->record([
                'active' => false,
                'last_event' => 'skip_not_enabled',
                'operation' => $operation,
                'restore_id' => (string) ($restorePoint['restore_id'] ?? ''),
                'restore_status' => !empty($restorePoint['ok']) ? 'created' : 'failed',
                'restore_message' => (string) ($restorePoint['message'] ?? ''),
                'updated_at' => gmdate('c'),
            ]);

            return $response;
        }

        $token = $this->lockdown->suspend();
        $suspend = isset($token['suspend']) && is_array($token['suspend']) ? $token['suspend'] : ['ok' => true];
        if (empty($suspend['ok'])) {
            $message = (string) ($suspend['message'] ?? 'SHIELD-X could not suspend Lockdown before the update.');
            $this->record([
                'active' => false,
                'last_event' => 'suspend_failed',
                'operation' => $operation,
                'token' => $token,
                'restore_id' => (string) ($restorePoint['restore_id'] ?? ''),
                'restore_status' => !empty($restorePoint['ok']) ? 'created' : 'failed',
                'restore_message' => (string) ($restorePoint['message'] ?? ''),
                'message' => $message,
                'updated_at' => gmdate('c'),
            ]);
            $this->log('update.lockdown_suspend_failed', [
                'operation' => $operation,
                'message' => $message,
            ]);

            if (class_exists('\WP_Error')) {
                return new \WP_Error('shieldx_lockdown_suspend_failed', $message);
            }

            return false;
        }

        $this->suspendedThisRequest = !empty($token['enabled']);
        $this->record([
            'active' => $this->suspendedThisRequest,
            'last_event' => $this->suspendedThisRequest ? 'suspended' : 'skip_not_needed',
            'operation' => $operation,
            'token' => $token,
            'restore_id' => (string) ($restorePoint['restore_id'] ?? ''),
            'restore_status' => !empty($restorePoint['ok']) ? 'created' : 'failed',
            'restore_message' => (string) ($restorePoint['message'] ?? ''),
            'suspended_at' => gmdate('c'),
            'updated_at' => gmdate('c'),
        ]);
        $this->log('update.lockdown_suspended', ['operation' => $operation]);

        return $response;
    }

    /**
     * @param mixed               $response
     * @param array<string,mixed> $hookExtra
     * @param mixed               $result
     * @return mixed
     */
    public function afterInstall($response, array $hookExtra, $result)
    {
        if ($this->isError($result) || $this->isError($response)) {
            $this->rollbackRestorePoint('post_install_error', $hookExtra);
            $this->resume('post_install_error', $hookExtra);

            return $response;
        }

        $this->invalidateUpdatedShieldXPluginCache($hookExtra, $result);

        return $response;
    }

    /**
     * @param mixed               $upgrader
     * @param array<string,mixed> $hookExtra
     */
    public function afterProcessComplete($upgrader, array $hookExtra): void
    {
        $this->resume('process_complete', $hookExtra);
    }

    /**
     * @param mixed $results
     */
    public function afterAutomaticUpdates($results): void
    {
        $this->resume('automatic_updates_complete', ['type' => 'automatic', 'action' => 'update']);
    }

    public function shutdownResume(): void
    {
        if ($this->suspendedThisRequest && !$this->resumedThisRequest) {
            $this->resume('shutdown_resume', ['type' => 'unknown', 'action' => 'update']);
        }
    }

    /**
     * @return array<string,mixed>
     */
    public static function status(): array
    {
        return array_merge([
            'active' => false,
            'last_event' => '',
            'operation' => '',
            'suspended_at' => '',
            'resumed_at' => '',
            'resume_status' => '',
            'message' => '',
            'restore_id' => '',
            'restore_status' => '',
            'restore_message' => '',
            'rollback_at' => '',
            'updated_at' => '',
        ], self::state());
    }

    /**
     * @param array<string,mixed> $hookExtra
     */
    private function resume(string $event, array $hookExtra): void
    {
        $state = self::state();
        if (empty($state['active']) || empty($state['token']) || !is_array($state['token'])) {
            return;
        }

        $result = $this->lockdown->resume($state['token']);
        $this->resumedThisRequest = true;
        $this->record([
            'active' => false,
            'last_event' => $event,
            'operation' => (string) ($state['operation'] ?? $this->operationLabel($hookExtra)),
            'suspended_at' => (string) ($state['suspended_at'] ?? ''),
            'resumed_at' => gmdate('c'),
            'resume_status' => (string) ($result['status'] ?? ''),
            'message' => (string) ($result['message'] ?? ''),
            'restore_id' => (string) ($state['restore_id'] ?? ''),
            'restore_status' => (string) ($state['restore_status'] ?? ''),
            'restore_message' => (string) ($state['restore_message'] ?? ''),
            'rollback_at' => (string) ($state['rollback_at'] ?? ''),
            'updated_at' => gmdate('c'),
        ]);
        $this->log('update.lockdown_resumed', [
            'event' => $event,
            'operation' => (string) ($state['operation'] ?? ''),
            'status' => (string) ($result['status'] ?? ''),
            'ok' => !empty($result['ok']),
        ]);
    }

    /**
     * @param array<string,mixed> $hookExtra
     */
    private function rollbackRestorePoint(string $event, array $hookExtra): void
    {
        $state = self::state();
        $restoreId = (string) ($state['restore_id'] ?? '');
        if ($restoreId === '') {
            return;
        }

        try {
            $result = $this->restorePoints()->restore($restoreId, 0, ['include_database' => false]);
        } catch (\Throwable $error) {
            $result = [
                'ok' => false,
                'message' => $error->getMessage(),
            ];
        }
        $this->record(array_merge($state, [
            'last_event' => $event . '_rollback',
            'operation' => (string) ($state['operation'] ?? $this->operationLabel($hookExtra)),
            'restore_status' => !empty($result['ok']) ? 'restored' : 'restore_failed',
            'restore_message' => (string) ($result['message'] ?? ''),
            'rollback_at' => gmdate('c'),
            'updated_at' => gmdate('c'),
        ]));
        $this->log('update.restore_point_rollback', [
            'event' => $event,
            'operation' => (string) ($state['operation'] ?? ''),
            'restore_id' => $restoreId,
            'ok' => !empty($result['ok']),
            'message' => (string) ($result['message'] ?? ''),
        ]);
    }

    private function restorePoints(): RestorePointManager
    {
        if (!$this->restorePoints instanceof RestorePointManager) {
            $this->restorePoints = RestorePointManager::fromWordPress();
        }

        return $this->restorePoints;
    }

    /**
     * @param array<string,mixed> $hookExtra
     */
    private function isUpdateOperation(array $hookExtra): bool
    {
        $type = strtolower((string) ($hookExtra['type'] ?? ''));
        $action = strtolower((string) ($hookExtra['action'] ?? ''));

        return in_array($type, ['plugin', 'theme', 'core', 'translation'], true)
            && in_array($action, ['install', 'update', 'upgrade'], true);
    }

    /**
     * @param array<string,mixed> $hookExtra
     * @param mixed               $result
     */
    private function invalidateUpdatedShieldXPluginCache(array $hookExtra, $result): int
    {
        if (!$this->isShieldXPluginOperation($hookExtra)) {
            return 0;
        }

        $invalidated = 0;
        $targetFound = false;
        foreach ($this->updatedPluginDirectories($hookExtra, $result) as $directory) {
            $targetFound = true;
            $invalidated += RuntimeCacheInvalidator::invalidateDirectory($directory);
        }

        if (!$targetFound) {
            return 0;
        }

        RequestCache::clear();
        RuntimeCacheInvalidator::clearPluginUpdateCache();
        $this->log('update.runtime_cache_invalidated', [
            'operation' => $this->operationLabel($hookExtra),
            'php_files' => $invalidated,
        ]);

        return $invalidated;
    }

    /**
     * @param array<string,mixed> $hookExtra
     */
    private function isShieldXPluginOperation(array $hookExtra): bool
    {
        return strtolower((string) ($hookExtra['type'] ?? '')) === 'plugin'
            && $this->shieldXPluginDirectoryNames($hookExtra) !== [];
    }

    /**
     * @param array<string,mixed> $hookExtra
     * @param mixed               $result
     * @return array<int,string>
     */
    private function updatedPluginDirectories(array $hookExtra, $result): array
    {
        $directoryNames = $this->shieldXPluginDirectoryNames($hookExtra);
        if ($directoryNames === []) {
            return [];
        }

        $candidates = [];
        if (is_array($result) && isset($result['destination']) && is_scalar($result['destination'])) {
            $candidates[] = (string) $result['destination'];
        }
        if (isset($hookExtra['destination']) && is_scalar($hookExtra['destination'])) {
            $candidates[] = (string) $hookExtra['destination'];
        }

        if ($candidates === [] && $this->isShieldXCorePluginOperation($hookExtra) && defined('SHIELDX_PLUGIN_DIR') && is_string(SHIELDX_PLUGIN_DIR)) {
            $candidates[] = (string) SHIELDX_PLUGIN_DIR;
        }

        $directories = [];
        foreach ($candidates as $directory) {
            $directory = rtrim($directory, '/\\');
            if ($directory === '' || !in_array(strtolower(basename($directory)), $directoryNames, true) || !is_dir($directory) || is_link($directory)) {
                continue;
            }
            $directories[$directory] = $directory;
        }

        return array_values($directories);
    }

    /**
     * @param array<string,mixed> $hookExtra
     * @return array<int,string>
     */
    private function shieldXPluginDirectoryNames(array $hookExtra): array
    {
        $names = [];
        $coreBasename = defined('SHIELDX_PLUGIN_BASENAME') ? strtolower((string) SHIELDX_PLUGIN_BASENAME) : '';
        foreach (['plugin', 'plugins'] as $key) {
            $value = $hookExtra[$key] ?? [];
            $plugins = is_array($value) ? $value : [$value];
            foreach ($plugins as $plugin) {
                if (!is_scalar($plugin)) {
                    continue;
                }

                $plugin = strtolower(str_replace('\\', '/', trim((string) $plugin)));
                $directory = dirname($plugin);
                $isCore = $plugin === $coreBasename || strpos($plugin, 'xerotact-shield-x-security/') === 0;
                $isExtensions = strpos($plugin, 'xerotact-shield-x-extensions/') === 0;
                if ($directory !== '.' && $directory !== '' && ($isCore || $isExtensions)) {
                    $names[strtolower(basename($directory))] = strtolower(basename($directory));
                }
            }
        }

        return array_values($names);
    }

    /**
     * @param array<string,mixed> $hookExtra
     */
    private function isShieldXCorePluginOperation(array $hookExtra): bool
    {
        $coreBasename = defined('SHIELDX_PLUGIN_BASENAME') ? strtolower((string) SHIELDX_PLUGIN_BASENAME) : '';
        foreach (['plugin', 'plugins'] as $key) {
            $value = $hookExtra[$key] ?? [];
            $plugins = is_array($value) ? $value : [$value];
            foreach ($plugins as $plugin) {
                if (!is_scalar($plugin)) {
                    continue;
                }

                $plugin = strtolower(str_replace('\\', '/', trim((string) $plugin)));
                if ($plugin !== '' && ($plugin === $coreBasename || strpos($plugin, 'xerotact-shield-x-security/') === 0)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @param array<string,mixed> $hookExtra
     */
    private function operationLabel(array $hookExtra): string
    {
        $type = strtolower((string) ($hookExtra['type'] ?? 'unknown'));
        $action = strtolower((string) ($hookExtra['action'] ?? 'update'));
        $items = [];

        foreach (['plugin', 'plugins', 'theme', 'themes'] as $key) {
            if (isset($hookExtra[$key])) {
                $value = $hookExtra[$key];
                if (is_array($value)) {
                    $items = array_merge($items, array_map('strval', $value));
                } elseif (is_scalar($value)) {
                    $items[] = (string) $value;
                }
            }
        }

        return trim($action . ':' . $type . (!empty($items) ? ':' . implode(',', array_unique($items)) : ''));
    }

    /**
     * @param mixed $value
     */
    private function isError($value): bool
    {
        if (function_exists('is_wp_error') && is_wp_error($value)) {
            return true;
        }

        return $value === false;
    }

    /**
     * @return array<string,mixed>
     */
    private static function state(): array
    {
        if (!function_exists('get_option')) {
            return [];
        }

        $state = get_option(self::OPTION_NAME, []);

        return is_array($state) ? $state : [];
    }

    /**
     * @param array<string,mixed> $state
     */
    private function record(array $state): void
    {
        if (function_exists('update_option')) {
            update_option(self::OPTION_NAME, $state, false);
        }
    }

    /**
     * @param array<string,mixed> $context
     */
    private function log(string $event, array $context): void
    {
        if ($this->logger) {
            $this->logger->info($event, $context);
        }
    }
}
