<?php

declare(strict_types=1);

namespace Duplicator\Addons\ScheduleAddon;

use Duplicator\Addons\ScheduleAddon\Ajax\ServicesSchedule;
use Duplicator\Addons\ScheduleAddon\Controllers\SchedulePageController;
use Duplicator\Addons\ScheduleAddon\Controllers\ScheduleSettingsController;
use Duplicator\Addons\ScheduleAddon\Core\CapSchedule;
use Duplicator\Addons\ScheduleAddon\Core\LicenseSchedule;
use Duplicator\Addons\ScheduleAddon\Models\ActivityLog\LogEventSchedule;
use Duplicator\Addons\ScheduleAddon\Models\ScheduleEntity;
use Duplicator\Addons\ScheduleAddon\Models\SchedulePackageRelation;
use Duplicator\Addons\ScheduleAddon\Package\SchedulePackageHandler;
use Duplicator\Addons\ProBase\License\DupliLicenseException;
use Duplicator\Core\Addons\AbstractAddonCore;
use Duplicator\Core\Bootstrap;
use Duplicator\Core\Controllers\AbstractMenuPageController;
use Duplicator\Core\MigrationMng;
use Duplicator\Core\Views\TplMng;
use Duplicator\Models\ActivityLog\LogUtils;
use Duplicator\Package\AbstractPackage;

/**
 * Schedule addon class
 *
 * Adds scheduled backup support: create, manage, and execute
 * automated backup schedules with cron-based processing.
 */
class ScheduleAddon extends AbstractAddonCore
{
    const ADDON_PATH = __DIR__;

    const FLAG_SCHEDULE         = 'SCHEDULE';
    const FLAG_SCHEDULE_RUN_NOW = 'SCHEDULE_RUN_NOW';

    /**
     * Initialize the addon
     *
     * @return void
     */
    public function init(): void
    {
        CapSchedule::register();
        ScheduleSettingsController::register();

        add_action('duplicator_register_types', [ScheduleEntity::class, 'registerType']);

        // Page registration
        add_filter('duplicator_menu_pages', [$this, 'addMenuPage']);

        // Template resolution
        add_filter('duplicator_template_file', [self::class, 'getTemplateFile'], 10, 2);

        // Package flags
        add_filter('duplicator_package_update_flags', [self::class, 'setScheduleFlags'], 10, 2);
        add_action('duplicator_package_flags_icons', [self::class, 'renderScheduleFlagIcons']);
        add_filter('duplicator_package_flags_legend', [self::class, 'addLegendEntries']);
        add_filter('duplicator_package_flags_tooltip_groups', [self::class, 'appendTooltipGroup'], 10, 2);

        // Package type string + scheduled check
        add_filter('duplicator_package_type_string', [self::class, 'resolvePackageTypeString'], 10, 2);

        // License check for scheduled builds
        add_action('duplicator_build_start', [self::class, 'checkScheduleLicense']);

        // License expired notice: schedule DRM message
        add_action('duplicator_license_expired_features_list', [self::class, 'renderExpiredScheduleMessage']);

        // Schedule processing
        add_action('duplicator_runner_pre_process', [ScheduleProcessor::class, 'processSchedules']);

        // Post-build processing
        add_action('duplicator_package_after_set_status', [SchedulePackageHandler::class, 'onStatusChange'], 10, 2);

        // Activity log
        add_action('duplicator_register_activity_log_types', [self::class, 'registerLogTypes']);
        add_filter('duplicator_backup_create_log_data', [self::class, 'addScheduleNameToLogData'], 10, 2);
        add_action('duplicator_backup_create_log_extra_fields', [self::class, 'renderScheduleNameLogField']);

        // Dashboard widget
        add_action('duplicator_dashboard_widget_sections', [ScheduleDashboardWidget::class, 'renderSection']);

        // Plugin deactivation
        add_action('duplicator_after_deactivation', [self::class, 'deactivateAllSchedules']);

        // Post-migration
        add_action(MigrationMng::HOOK_FIRST_LOGIN_AFTER_INSTALL, [self::class, 'updateNextSchedules']);

        // Upgrade
        add_action('duplicator_upgrade', [self::class, 'createSchedulePackageTable'], 10, 4);
        add_action('duplicator_upgrade', [self::class, 'resaveAllSchedules'], 10001, 4);

        // NameFormat
        add_filter('duplicator_name_format_result', [self::class, 'resolveScheduleNameTag']);
        add_filter('duplicator_name_format_tags', [self::class, 'registerNameFormatTag']);

        // Email summary
        add_filter('duplicator_email_summary_new_entity_messages', [ScheduleEmailIntegration::class, 'addNewEntityMessage']);
        add_action('duplicator_email_summary_extra_sections', [ScheduleEmailIntegration::class, 'renderEmailSummarySection']);
        add_action('duplicator_after_schedule_create', [ScheduleEmailIntegration::class, 'onScheduleCreate']);
        add_action('duplicator_after_schedule_delete', [ScheduleEmailIntegration::class, 'onScheduleDelete']);

        // Package deletion cleanup
        add_action('duplicator_package_after_delete', function (int $id): void {
            SchedulePackageRelation::deleteByPackageId($id);
        });
        add_action('duplicator_packages_after_bulk_delete', function (array $ids): void {
            SchedulePackageRelation::deleteByPackageIds($ids);
        });

        // Storage cleanup + delete confirm UI
        add_action('duplicator_storage_after_delete_cleanup', [self::class, 'cleanupStorageFromSchedules']);
        add_action('duplicator_after_storage_list', [SchedulePageController::class, 'renderStorageDeleteScheduleCheck']);

        // DRM active tasks check
        add_filter('duplicator_has_active_automated_tasks', [self::class, 'hasActiveSchedules']);

        // Settings export/import
        add_filter('duplicator_settings_export_data', [self::class, 'addSettingsExportData']);
        add_action('duplicator_settings_import_data', [self::class, 'importSettingsData'], 10, 4);
        add_action('duplicator_import_settings_checkboxes', [self::class, 'renderImportCheckbox']);

        // Help context
        add_filter('duplicator_help_page_context', [self::class, 'addHelpContext'], 10, 3);

        // Capability labels
        add_filter('duplicator_capability_labels', [self::class, 'addCapabilityLabels']);

        // Disable schedule capability on staging sites
        add_filter('duplicator_staging_disabled_caps', [self::class, 'addStagingDisabledCaps']);

        // Upgrade entity classes
        add_filter('duplicator_upgrade_entity_classes', [self::class, 'addUpgradeEntityClasses']);

        // Usage stats
        ScheduleUsageStats::register();

        // Telemetry: enrich backup_* events with execution_type
        ScheduleTelemetryIntegration::register();

        // AJAX services
        (new ServicesSchedule())->init();
    }

    /**
     * Add schedule page to the menu
     *
     * @param AbstractMenuPageController[] $pages Menu pages
     *
     * @return AbstractMenuPageController[]
     */
    public function addMenuPage(array $pages): array
    {
        $pages[]  = SchedulePageController::getInstance();
        $pageSlug = SchedulePageController::getInstance()->getMenuHookSuffix();

        add_action('admin_print_scripts-' . $pageSlug, [Bootstrap::class, 'enqueueScripts']);
        add_action('admin_print_styles-' . $pageSlug, [Bootstrap::class, 'enqueueStyles']);

        return $pages;
    }

    /**
     * Return template file path for addon templates
     *
     * @param string $path    Current path
     * @param string $slugTpl Template slug
     *
     * @return string
     */
    public static function getTemplateFile(string $path, string $slugTpl): string
    {
        if (strpos($slugTpl, 'scheduleaddon/') === 0) {
            return self::getAddonPath() . '/template/' . $slugTpl . '.php';
        }

        return $path;
    }

    /**
     * Register activity log types for schedule events
     *
     * @return void
     */
    public static function registerLogTypes(): void
    {
        LogUtils::registerLogType(LogEventSchedule::getType(), LogEventSchedule::class);
    }

    /**
     * Render schedule DRM message in license expired notice
     *
     * @return void
     */
    public static function renderExpiredScheduleMessage(): void
    {
        ?>
        <li><?php TplMng::getInstance()->render('scheduleaddon/licensing/drm_schedules_msg'); ?></li>
        <?php
    }

    /**
     * Add schedule name to backup create activity log data
     *
     * @param array<string, mixed> $data    Log data
     * @param AbstractPackage      $package The package
     *
     * @return array<string, mixed>
     */
    public static function addScheduleNameToLogData(array $data, AbstractPackage $package): array
    {
        $relation = SchedulePackageRelation::getByPackageId($package->getId());
        if ($relation !== null) {
            $data['scheduleName'] = $relation['schedule_name'];
        }

        return $data;
    }

    /**
     * Render schedule name field in backup create activity log
     *
     * @param array<string, mixed> $data Log data
     *
     * @return void
     */
    public static function renderScheduleNameLogField(array $data): void
    {
        if (empty($data['scheduleName'])) {
            return;
        }
        ?>
        <div class="dup-log-type-wrapper">
            <strong><?php esc_html_e('Schedule Name:', 'duplicator-pro'); ?></strong>
            <span class="dup-log-type">
                <?php echo esc_html($data['scheduleName']); ?>
            </span>
        </div>
        <?php
    }

    /**
     * Deactivate all schedules on plugin deactivation
     *
     * Resets next_run_time to prevent orphaned cron executions.
     *
     * @return void
     */
    public static function deactivateAllSchedules(): void
    {
        $activeSchedules = ScheduleEntity::getActive();
        foreach ($activeSchedules as $schedule) {
            $schedule->next_run_time = -1;
            $schedule->save();
        }
    }

    /**
     * Recalculate active schedules next run time after migration
     *
     * @return void
     */
    public static function updateNextSchedules(): void
    {
        $schedules = ScheduleEntity::getActive();
        foreach ($schedules as $schedule) {
            $schedule->updateNextRuntime();
        }
    }

    /**
     * Create the schedule_packages table during upgrade
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function createSchedulePackageTable($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        SchedulePackageRelation::createTable();
    }

    /**
     * Resave all schedule entities on upgrade
     *
     * Runs at priority 10001, after core resaveAllEntities at 10000.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function resaveAllSchedules($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        $schedules = ScheduleEntity::getAll();
        if ($schedules === false) {
            return;
        }

        foreach ($schedules as $schedule) {
            $schedule->save();
        }
    }

    /**
     * Resolve the %schedulename% placeholder in name format
     *
     * @param string $parsed The partially parsed name format string
     *
     * @return string
     */
    public static function resolveScheduleNameTag(string $parsed): string
    {
        if (strpos($parsed, '%schedulename%') === false) {
            return $parsed;
        }

        $schedule = ScheduleProcessor::getCurrentSchedule();
        $name     = ($schedule !== null) ? $schedule->name : '';

        return str_replace('%schedulename%', sanitize_file_name($name), $parsed);
    }

    /**
     * Register the schedulename tag description for the name format UI
     *
     * @param array<string, string> $tags Existing name format tags
     *
     * @return array<string, string>
     */
    public static function registerNameFormatTag(array $tags): array
    {
        $tags['schedulename'] = __('Backup Schedule Name', 'duplicator-pro');

        return $tags;
    }

    /**
     * Return true if there are active schedules
     *
     * @param bool $hasActive Current value
     *
     * @return bool
     */
    public static function hasActiveSchedules(bool $hasActive): bool
    {
        if ($hasActive) {
            return true;
        }

        return count(ScheduleEntity::getActive()) > 0;
    }

    /**
     * Set schedule flags on package via filter
     *
     * Only queries the SchedulePackageRelation during the early phase
     * (STATUS_PRE_PROCESS) to avoid unnecessary DB queries on every save.
     *
     * @param string[]        $flags   Current flags array
     * @param AbstractPackage $package The package
     *
     * @return string[]
     */
    public static function setScheduleFlags(array $flags, AbstractPackage $package): array
    {
        // Past early phase or no DB ID yet: nothing to do
        if ($package->getStatus() > AbstractPackage::STATUS_PRE_PROCESS || $package->getId() <= 0) {
            return $flags;
        }

        // Early phase with DB ID: determine schedule flags from relation
        $flags = array_values(array_diff($flags, [self::FLAG_SCHEDULE, self::FLAG_SCHEDULE_RUN_NOW]));

        $relation = SchedulePackageRelation::getByPackageId($package->getId());
        if ($relation === null) {
            return $flags;
        }

        $flags[] = $relation['run_now'] ? self::FLAG_SCHEDULE_RUN_NOW : self::FLAG_SCHEDULE;
        return $flags;
    }

    /**
     * Render schedule flag icons in the package list
     *
     * Also renders the manual backup icon (hand) when no schedule
     * flags are present — the manual concept only exists with this addon.
     *
     * @param AbstractPackage $package The package
     *
     * @return void
     */
    public static function renderScheduleFlagIcons(AbstractPackage $package): void
    {
        if ($package->hasFlag(self::FLAG_SCHEDULE)) {
            ?>
            <span class="icon-wrapper" title="<?php esc_attr_e('Schedule Backup', 'duplicator-pro'); ?>">
                <i class="fa-solid fa-clock"></i>
            </span>
            <?php
        } else {
            ?>
            <span class="icon-wrapper" title="<?php esc_attr_e('Manual Backup', 'duplicator-pro'); ?>">
                <i class="fa-solid fa-hand"></i>
            </span>
            <?php
        }
    }

    /**
     * Append schedule/manual entry to the package flags aggregated tooltip.
     *
     * @param array<int, array{icon: string, label: string, details: string}> $groups  Existing tooltip groups
     * @param AbstractPackage                                                 $package The package
     *
     * @return array<int, array{icon: string, label: string, details: string}>
     */
    public static function appendTooltipGroup(array $groups, AbstractPackage $package): array
    {
        if ($package->hasFlag(self::FLAG_SCHEDULE)) {
            $groups[] = [
                'icon'    => 'fa-solid fa-clock',
                'label'   => esc_html__('Schedule Backup', 'duplicator-pro'),
                'details' => '',
            ];
        } else {
            $groups[] = [
                'icon'    => 'fa-solid fa-hand',
                'label'   => esc_html__('Manual Backup', 'duplicator-pro'),
                'details' => '',
            ];
        }
        return $groups;
    }

    /**
     * Append schedule-related entries to the Status column legend.
     *
     * @param array<int, array{icon: string, label: string}> $entries Existing legend entries
     *
     * @return array<int, array{icon: string, label: string}>
     */
    public static function addLegendEntries(array $entries): array
    {
        $entries[] = [
            'icon'  => '<i class="fa-solid fa-hand"></i>',
            'label' => __('Manual Backup', 'duplicator-pro'),
        ];
        $entries[] = [
            'icon'  => '<i class="fa-solid fa-clock"></i>',
            'label' => __('Schedule Backup', 'duplicator-pro'),
        ];
        return $entries;
    }

    /**
     * Resolve the package type string for schedule packages
     *
     * @param string          $typeString Current type string
     * @param AbstractPackage $package    The package
     *
     * @return string
     */
    public static function resolvePackageTypeString(string $typeString, AbstractPackage $package): string
    {
        $execType = SchedulePackageRelation::getExecutionType($package->getId());

        switch ($execType) {
            case SchedulePackageRelation::EXEC_TYPE_SCHEDULED:
                return __('Schedule', 'duplicator-pro');
            case SchedulePackageRelation::EXEC_TYPE_RUN_NOW:
                return __('Schedule (Run Now)', 'duplicator-pro');
            default:
                return $typeString;
        }
    }

    /**
     * Check schedule license before build starts
     *
     * @param AbstractPackage $package The package
     *
     * @return void
     *
     * @throws DupliLicenseException If not licensed for schedules
     */
    public static function checkScheduleLicense(AbstractPackage $package): void
    {
        $execType = SchedulePackageRelation::getExecutionType($package->getId());

        if ($execType === SchedulePackageRelation::EXEC_TYPE_SCHEDULED && !LicenseSchedule::can()) {
            throw new DupliLicenseException(
                "Can't process package schedule " . $package->getId() . " because Duplicator isn't licensed",
                DupliLicenseException::CODE_SCHEDULE_LICENSE_BLOCKED,
                __(
                    'The scheduled backup was blocked because the Duplicator license is expired or invalid.
                    Renew the license to resume scheduled backups.',
                    'duplicator-pro'
                )
            );
        }
    }

    /**
     * Remove deleted storage from all schedules
     *
     * @param int $storageId The deleted storage ID
     *
     * @return void
     */
    public static function cleanupStorageFromSchedules(int $storageId): void
    {
        ScheduleEntity::listCallback(function (ScheduleEntity $schedule) use ($storageId): void {
            if (($key = array_search($storageId, $schedule->storage_ids)) !== false) {
                $key = (int) $key;
                array_splice($schedule->storage_ids, $key, 1);
                if (count($schedule->storage_ids) === 0) {
                    $schedule->setActive(false);
                }
                $schedule->save();
            }
        });
    }

    /**
     * Render schedule import checkbox in settings import page
     *
     * @return void
     */
    public static function renderImportCheckbox(): void
    {
        ?>
        <td>
            <input
                onclick="DupliJs.Tools.ChangeImportButtonState();DupliJs.Tools.SchedulesImportClicked();"
                type="checkbox"
                name="import-opts[]"
                id="import-schedules" value="schedules"
                class="margin-0">
            <label for="import-schedules">
                <?php esc_html_e("Schedules", 'duplicator-pro'); ?>
            </label>
        </td>
        <script>
            jQuery(document).ready(function($) {
                DupliJs.Tools.SchedulesImportClicked = function() {
                    if (document.getElementById('import-schedules').checked) {
                        document.getElementById('import-templates').checked = true;
                        document.getElementById('import-storages').checked = true;
                        document.getElementById('import-templates').disabled = true;
                        document.getElementById('import-storages').disabled = true;
                    } else {
                        document.getElementById('import-templates').disabled = false;
                        document.getElementById('import-storages').disabled = false;
                    }
                }
            });
        </script>
        <?php
    }

    /**
     * Add schedule data to settings export payload
     *
     * @param array<string, mixed> $exportData Current export data
     *
     * @return array<string, mixed>
     */
    public static function addSettingsExportData(array $exportData): array
    {
        $schedules = ScheduleEntity::getAll();
        if ($schedules === false) {
            $schedules = [];
        }

        $exportData['schedules'] = $schedules;

        return $exportData;
    }

    /**
     * Import schedule data from settings import
     *
     * @param array<string, mixed> $data        Imported data
     * @param string               $version     Import version
     * @param array<int, int>      $storageMap  Old-to-new storage ID mapping
     * @param array<int, int>      $templateMap Old-to-new template ID mapping
     *
     * @return void
     */
    public static function importSettingsData(
        array $data,
        string $version,
        array $storageMap,
        array $templateMap
    ): void {
        if (!isset($data['schedules']) || !is_array($data['schedules'])) {
            return;
        }

        foreach ($data['schedules'] as $scheduleData) {
            $schedule = new ScheduleEntity();
            $schedule->settingsImport(
                $scheduleData,
                $version,
                [
                    'storage_map'  => $storageMap,
                    'template_map' => $templateMap,
                ]
            );

            $schedule->save();
        }
    }

    /**
     * Add help context for schedule pages
     *
     * @param string      $context   Current help context
     * @param string|null $pageSlug  Current page slug
     * @param string|null $innerPage Current inner page
     *
     * @return string
     */
    public static function addHelpContext(string $context, ?string $pageSlug, ?string $innerPage): string
    {
        if ($pageSlug !== SchedulePageController::SCHEDULES_SUBMENU_SLUG) {
            return $context;
        }

        if ($innerPage === SchedulePageController::INNER_PAGE_EDIT) {
            return 'schedule_edit';
        }

        return 'schedules';
    }

    /**
     * Add schedule capability labels
     *
     * @param array<string, string> $labels Existing capability labels
     *
     * @return array<string, string>
     */
    public static function addCapabilityLabels(array $labels): array
    {
        $labels[CapSchedule::CAP_SCHEDULE] = __('Manage Schedules', 'duplicator-pro');

        return $labels;
    }

    /**
     * Register schedule capability as disabled on staging sites
     *
     * @param string[] $caps Currently disabled capabilities
     *
     * @return string[]
     */
    public static function addStagingDisabledCaps(array $caps): array
    {
        $caps[] = CapSchedule::CAP_SCHEDULE;
        return $caps;
    }

    /**
     * Add schedule entity class to upgrade entity classes list
     *
     * @param array<int, class-string> $classes Entity class list
     *
     * @return array<int, class-string>
     */
    public static function addUpgradeEntityClasses(array $classes): array
    {
        $classes[] = ScheduleEntity::class;

        return $classes;
    }

    /**
     * Get addon path
     *
     * @return string
     */
    public static function getAddonPath(): string
    {
        return __DIR__;
    }

    /**
     * Get addon file
     *
     * @return string
     */
    public static function getAddonFile(): string
    {
        return __FILE__;
    }
}
