<?php
/**
 * Template.
 *
 * @author    Guido Scialfa <dev@guidoscialfa.com>
 * @copyright Copyright (c) 2017, Guido Scialfa
 * @license   GNU General Public License, version 2
 *
 * Copyright (C) 2017 Guido Scialfa <dev@guidoscialfa.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

declare(strict_types=1);

namespace Inpsyde\BackWPup\Infrastructure\Restore;

use function add_action;
use function backwpup_template;
use Inpsyde\BackWPup\Infrastructure\Restore\LogDownloader\DownloaderFactory;
use Inpsyde\Restore\Api\Module\Registry;
use Inpsyde\Restore\Api\Module\Upload\BackupUpload;
use Inpsyde\Restore\Log\LevelExtractorFactory;
use Inpsyde\Restore\Api\Module\Manifest\ManifestFile;
use Pimple\Container;
use SplFileObject;
use function untrailingslashit;
use function wp_create_nonce;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class Template.
 *
 * @psalm-type Item=array{bind?: object, view?: string}
 */
final class TemplateLoader
{
    /**
     * Step.
     *
     * @var int The current step to load
     */
    private $step = 1;

    /**
     * Skip.
     *
     * If the current step must be skipped
     *
     * @var bool True to skip, false otherwise
     */
    private $skip = false;

    /**
     * Default step.
     *
     * @var string The default step if none can be set
     */
    private $defaultStepView = 'step1';

    /**
     * Step List.
     *
     * @var array<string, Item> Step's list
     */
    private $list = [];

    /**
     * Container.
     *
     * @var Container The container of the instances
     */
    private $container;

    /**
     * TemplateLoader constructor.
     *
     * @param Container $container the container of the instances
     */
    public function __construct(Container $container)
    {
        $this->container = $container;

        // Default to one. The upload step.
        // phpcs:disable
        $this->step = (int) (
            isset($_GET['step'])
            ? filter_var($_GET['step'], FILTER_SANITIZE_NUMBER_INT)
            : $this->step
        );
        // phpcs:enable
    }

    /**
     * Load.
     *
     * @return $this
     */
    public function load(): self
    {
        add_action('backwpup_restore_upload_content', [$this, 'template']); // @phpstan-ignore-line
        add_action(
            'backwpup_restore_before_upload_content',
            function (): void {
                $this->template('dashboard');
            }
        );
        add_action(
            'backwpup_restore_before_main_content',
            function (): void {
                $this->template('top');
            }
        );
        add_action(
            'backwpup_restore_main_content',
            function (): void {
                $this->template('action');
            }
        );

        return $this;
    }

    /**
     * Template.
     *
     * @param string $which which Template to load
     *
     * @return TemplateLoader $this The instance for concatenation
     */
    public function template(string $which = ''): self
    {
        $this->setContext();

        // Retrieve the item for which to load the template.
        $item = $this->item($which);

        // Prevent infinite loop or max call stack.
        if ($which && !$item) {
            return $this;
        }

        // If item found, load it.
        if (!empty($item) && isset($item['bind'], $item['view'])) {
            backwpup_template($item['bind'], $item['view']);

            return $this;
        }

        backwpup_template($this, '/restore/main.php');

        return $this;
    }

    /**
     * Container.
     *
     * @param string|null $what what container to retrieve
     *
     * @return mixed|null The instance request or empty if the instance doesn't exist
     */
    public function container(?string $what = '')
    {
        if (!$what) {
            return $this->container;
        }

        return $this->container[$what] ?? null;
    }

    /**
     * Set Context for step.
     */
    private function setContext(): void
    {
        // Get the base views path.
        $path = untrailingslashit(\BackWPup::get_plugin_data('plugindir')) . '/views/restore';

        // Top template.
        $this->list['top'] = $this->setStep('top', $path);
        // Action Template.
        $this->list['action'] = $this->setStep('action', $path);
        // Dashboard Template.
        $this->list['dashboard'] = $this->setDashboard();
    }

    /**
     * Set Step.
     *
     * @param string $portion the portion of the view for which set the data
     * @param string $path    the base path where looking for the template view
     *
     * @return array{bind: object, view: string} The data needed by the view
     */
    private function setStep(string $portion, string $path): array
    {
        // Action Template
        $item = [
            'bind' => $this,
            'view' => "{$path}/steps/step{$this->defaultStepView}_{$portion}.php",
        ];

        if (!$this->skip && file_exists("{$path}/steps/step{$this->step}_{$portion}.php")) {
            $item['bind'] = $this->createBindFromStep($this->step);
            $item['view'] = "/restore/steps/step{$this->step}_{$portion}.php";
        }

        return $item;
    }

    /**
     * Set Dashboard.
     *
     * @return array The data needed by the view
     * @psalm-return Item
     */
    private function setDashboard(): array
    {
        try {
            // Download Url view.
            $downloaderFactory = new DownloaderFactory();
            $downloader = $downloaderFactory->create();
        } catch (\RuntimeException $exc) {
            return [];
        }

        return [
            'bind' => (object) [
                'downloader' => $downloader,
            ],
            'view' => '/restore/dashboard.php',
        ];
    }

    /**
     * Create bind object for the view.
     *
     * @param int $step the step for which create the bind object
     *
     * @return object The bind object
     */
    private function createBindFromStep(int $step): object
    {
        $bind = [];

        switch ($step) {
            case 2:
                /** @var BackupUpload $backupUpload */
                $backupUpload = $this->container('backup_upload');
                /** @var Registry $registry */
                $registry = $this->container('registry');

                $bind = [
                    'backupUpload' => $backupUpload,
                    'upload_is_archive' => empty($registry->uploaded_file) ? null : $backupUpload::upload_is_archive(
                        $registry->uploaded_file
                    ),
                    'upload_is_sql' => empty($registry->uploaded_file) ? null : $backupUpload::upload_is_sql(
                        $registry->uploaded_file
                    ),
                    'restore_capabilities' => $this->restoreCapabilities($registry->manifest_file),
                ];
                break;

            case 3:
                // Only go to migrate step if in pro version
                // Note that this is hard-coded: if new steps are inserted, please modify as necessary
                $bind = [
                    'migrate_allowed' => \BackWPup::is_pro(),
                ];
                break;

            case 5:
                /** @var Registry $registry */
                $registry = $this->container('registry');
                $caps = $this->restoreCapabilities($registry->manifest_file ?? null);

                $bind = [
                    'has_db' => (bool) ($caps->has_db ?? false),
                    'has_files' => (bool) ($caps->has_files ?? false),
                ];
                break;

            case 6:
                /** @var LevelExtractorFactory $levelExtractorFactory */
                $levelExtractorFactory = $this->container('level_extractor_factory');

                $levelExtractor = $levelExtractorFactory->create();
                $logFilePath = (string) $this->container('log_file');
                if (file_exists($logFilePath)) {
                    $logFile = new SplFileObject($logFilePath);
                    $bind['errors'] = $levelExtractor->extractError($logFile);
                } else {
                    $bind['errors'] = [];
                }
                break;

            default:
                break;
        }

        // Set nonce to use within the template.
        $bind['nonce'] = $this->currentNonce();

        return (object) $bind;
    }

    /**
     * Retrieve the item from the list.
     *
     * @param string $item the item to retrieve from the list
     *
     * @return array The item found or empty array if the requested item doesn't exist
     * @psalm-return Item
     */
    private function item(string $item): array
    {
        return $this->list[$item] ?? [];
    }

    /**
     * Determines the restore capabilities available in a given manifest.json file.
     *
     * This method attempts to load and parse the provided manifest file in order to
     * detect which restore operations are supported (database, files, full restore, etc.).
     * If the manifest file is missing, unreadable, invalid, or an error occurs during
     * processing, a safe fallback object with all capabilities disabled is returned.
     *
     * @param string|null $manifestFile Absolute path to the manifest.json file.
     * @return \stdClass Object describing the available restore capabilities.
     */
    private function restoreCapabilities(?string $manifestFile): \stdClass
    {
        $fallback = (object)[
            'has_db' => false,
            'has_files' => false,
            'can_full_restore' => false,
            'mode' => 'unknown',
            'job_types' => [],
        ];

        /** @var ManifestFile $manifest */
        $manifest = $this->container('manifest');
        if (!$manifest instanceof ManifestFile) {
            return $fallback;
        }

        if (empty($manifestFile) || !is_readable($manifestFile)) {
            return $fallback;
        }

        try {
            $manifest->set_manifest_file($manifestFile);
            return $manifest->get_restore_capabilities();
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log(sprintf('[BackWPup Restore] Failed to load manifest capabilities: %s', $e->getMessage()));
            }
            return $fallback;
        }
    }

    /**
     * Retrieve the current BackWPup restore nonce.
     *
     * This method attempts to reuse the existing `backwpup_action_nonce` passed via
     * the request (GET), which is required to keep AJAX restore actions working
     * across wizard steps without breaking the nonce chain.
     *
     * If no nonce is present in the request, a new nonce is generated as a safe
     * fallback to allow the restore flow to continue.
     *
     * @return string The existing or newly generated BackWPup action nonce.
     */
    private function currentNonce(): string
    {
        $nonce = isset($_GET['backwpup_action_nonce'])
            ? sanitize_text_field((string) $_GET['backwpup_action_nonce'])
            : '';

        if ($nonce === 'undefined' || $nonce === 'null') {
            $nonce = '';
        }

        return $nonce !== '' ? $nonce : wp_create_nonce('backwpup_action_nonce');
    }
}
