<?php

/**
 * Internal Linking Operations Manager
 *
 * Handles scheduled operational cycles, run history, and optional email summaries.
 *
 * @package ProRank\SEO\Modules\Content\InternalLinking
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Content\InternalLinking;

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

class OperationsManager {
    public const STATE_OPTION_KEY = 'prorank_internal_linking_operations_state';
    public const RUN_HISTORY_OPTION_KEY = 'prorank_internal_linking_operations_runs';
    public const RUN_HISTORY_LIMIT = 30;
    public const SCHEDULE_HOOK = 'prorank_internal_linking_scheduled_cycle';

    /**
     * Register cron hooks and schedule filter.
     *
     * @return void
     */
    public static function register_hooks(): void {
        add_filter('cron_schedules', [self::class, 'filter_cron_schedules']);
        add_action(self::SCHEDULE_HOOK, [self::class, 'handle_scheduled_cycle']);
    }

    /**
     * Add monthly recurrence for operations cycles.
     *
     * @param array $schedules Existing cron schedules.
     * @return array
     */
    public static function filter_cron_schedules(array $schedules): array {
        if (!isset($schedules['monthly'])) {
            $schedules['monthly'] = [
                'interval' => MONTH_IN_SECONDS,
                'display' => __('Once Monthly', 'prorank-seo'),
            ];
        }

        return $schedules;
    }

    /**
     * Get operations state + recent runs payload.
     *
     * @param int $run_limit Max runs to include.
     * @return array<string,mixed>
     */
    public static function get_payload(int $run_limit = 8): array {
        $next_timestamp = wp_next_scheduled(self::SCHEDULE_HOOK);

        return [
            'state' => self::get_state(),
            'runs' => self::get_recent_runs($run_limit),
            'next_run_at' => $next_timestamp ? wp_date('c', (int) $next_timestamp) : null,
            'next_run_label' => $next_timestamp ? wp_date('Y-m-d H:i:s', (int) $next_timestamp) : '',
            'frequencies' => [
                ['value' => 'daily', 'label' => __('Daily', 'prorank-seo')],
                ['value' => 'weekly', 'label' => __('Weekly', 'prorank-seo')],
                ['value' => 'monthly', 'label' => __('Monthly', 'prorank-seo')],
            ],
        ];
    }

    /**
     * Return normalized current state.
     *
     * @return array<string,mixed>
     */
    public static function get_state(): array {
        $stored = get_option(self::STATE_OPTION_KEY, []);
        $stored = is_array($stored) ? $stored : [];

        $defaults = self::get_default_state();
        $state = wp_parse_args($stored, $defaults);

        $state['enabled'] = !empty($state['enabled']);
        $state['email_enabled'] = !empty($state['email_enabled']);
        $state['frequency'] = in_array((string) ($state['frequency'] ?? ''), ['daily', 'weekly', 'monthly'], true)
            ? (string) $state['frequency']
            : 'weekly';
        $state['email_recipients'] = self::sanitize_recipient_string((string) ($state['email_recipients'] ?? ''));

        if ($state['email_recipients'] === '') {
            $state['email_recipients'] = self::sanitize_recipient_string((string) get_option('admin_email', ''));
        }

        $state['last_run_at'] = (string) ($state['last_run_at'] ?? '');
        $state['last_run_status'] = (string) ($state['last_run_status'] ?? 'idle');
        $state['last_run_message'] = (string) ($state['last_run_message'] ?? '');

        return $state;
    }

    /**
     * Persist state and sync cron schedule.
     *
     * @param array $input Input state patch.
     * @return array<string,mixed>
     */
    public static function update_state(array $input): array {
        $state = self::get_state();

        if (array_key_exists('enabled', $input)) {
            $state['enabled'] = !empty($input['enabled']);
        }

        if (array_key_exists('frequency', $input)) {
            $candidate_frequency = sanitize_key((string) $input['frequency']);
            if (in_array($candidate_frequency, ['daily', 'weekly', 'monthly'], true)) {
                $state['frequency'] = $candidate_frequency;
            }
        }

        if (array_key_exists('email_enabled', $input)) {
            $state['email_enabled'] = !empty($input['email_enabled']);
        }

        if (array_key_exists('email_recipients', $input)) {
            $state['email_recipients'] = self::sanitize_recipient_string((string) $input['email_recipients']);
        }

        if ($state['email_recipients'] === '') {
            $state['email_recipients'] = self::sanitize_recipient_string((string) get_option('admin_email', ''));
        }

        update_option(self::STATE_OPTION_KEY, $state, false);
        self::sync_schedule();

        return $state;
    }

    /**
     * Ensure schedule state matches saved operations settings.
     *
     * @return void
     */
    public static function sync_schedule(): void {
        $state = self::get_state();
        $target_recurrence = self::get_recurrence_key((string) $state['frequency']);

        if (empty($state['enabled'])) {
            self::unschedule_all();
            return;
        }

        $event = function_exists('wp_get_scheduled_event')
            ? wp_get_scheduled_event(self::SCHEDULE_HOOK)
            : null;

        if ($event && !empty($event->recurrence) && $event->recurrence !== $target_recurrence) {
            self::unschedule_all();
        }

        if (!wp_next_scheduled(self::SCHEDULE_HOOK)) {
            wp_schedule_event(time() + 300, $target_recurrence, self::SCHEDULE_HOOK);
        }
    }

    /**
     * Run one full internal-linking operations cycle.
     *
     * @param string $triggered_by Trigger source label.
     * @return array<string,mixed>
     */
    public static function run_cycle(string $triggered_by = 'manual'): array {
        $started_micro = microtime(true);
        $started_at = current_time('mysql');
        $run_id = uniqid('ops_', true);
        $status = 'success';
        $message = __('Internal linking cycle completed.', 'prorank-seo');
        $metrics = [];

        try {
            self::ensure_scan_endpoint_loaded();

            if (!function_exists('\\ProRank\\SEO\\Core\\RestApi\\start_full_scan')) {
                throw new \RuntimeException('Internal linking scan endpoint is unavailable.');
            }

            $request = new \WP_REST_Request('POST', '/prorank-seo/v1/linking/start-scan');
            $response = \ProRank\SEO\Core\RestApi\start_full_scan($request);

            $data = [];
            if ($response instanceof \WP_REST_Response) {
                $data = (array) $response->get_data();
            } elseif (is_array($response)) {
                $data = $response;
            }

            if (empty($data['success'])) {
                $status = 'failed';
                $message = (string) ($data['message'] ?? __('Internal linking cycle failed.', 'prorank-seo'));
            } else {
                $message = (string) ($data['message'] ?? $message);
            }

            $metrics = self::normalize_metrics((array) ($data['stats'] ?? []));
        } catch (\Throwable $error) {
            $status = 'failed';
            $message = $error->getMessage() !== ''
                ? $error->getMessage()
                : __('Internal linking cycle failed.', 'prorank-seo');
        }

        $completed_at = current_time('mysql');
        $duration_seconds = max(0, (int) round(microtime(true) - $started_micro));

        $entry = [
            'run_id' => $run_id,
            'started_at' => $started_at,
            'completed_at' => $completed_at,
            'status' => $status,
            'message' => $message,
            'triggered_by' => $triggered_by,
            'duration_seconds' => $duration_seconds,
            'metrics' => $metrics,
        ];

        self::record_run($entry);
        self::update_last_run_state($entry);

        if ($status === 'success') {
            self::maybe_send_email_summary($entry);
        }

        return $entry;
    }

    /**
     * Handle scheduled cron run.
     *
     * @return void
     */
    public static function handle_scheduled_cycle(): void {
        $state = self::get_state();
        if (empty($state['enabled'])) {
            return;
        }

        self::run_cycle('scheduled');
    }

    /**
     * Get recent run history.
     *
     * @param int $limit Max runs.
     * @return array<int,array<string,mixed>>
     */
    public static function get_recent_runs(int $limit = 8): array {
        $stored = get_option(self::RUN_HISTORY_OPTION_KEY, []);
        if (!is_array($stored)) {
            return [];
        }

        $runs = [];
        foreach ($stored as $entry) {
            if (!is_array($entry)) {
                continue;
            }

            $runs[] = self::normalize_run_entry($entry);
        }

        usort($runs, static function (array $a, array $b): int {
            return strcmp((string) ($b['started_at'] ?? ''), (string) ($a['started_at'] ?? ''));
        });

        if ($limit > 0) {
            $runs = array_slice($runs, 0, $limit);
        }

        return $runs;
    }

    /**
     * Save run entry to persistent history.
     *
     * @param array<string,mixed> $entry Run entry.
     * @return void
     */
    private static function record_run(array $entry): void {
        $history = self::get_recent_runs(0);
        $history[] = self::normalize_run_entry($entry);

        usort($history, static function (array $a, array $b): int {
            return strcmp((string) ($b['started_at'] ?? ''), (string) ($a['started_at'] ?? ''));
        });

        if (count($history) > self::RUN_HISTORY_LIMIT) {
            $history = array_slice($history, 0, self::RUN_HISTORY_LIMIT);
        }

        update_option(self::RUN_HISTORY_OPTION_KEY, $history, false);
    }

    /**
     * Update state markers from latest run.
     *
     * @param array<string,mixed> $entry Run entry.
     * @return void
     */
    private static function update_last_run_state(array $entry): void {
        $state = self::get_state();
        $state['last_run_at'] = (string) ($entry['completed_at'] ?? '');
        $state['last_run_status'] = (string) ($entry['status'] ?? 'idle');
        $state['last_run_message'] = (string) ($entry['message'] ?? '');

        update_option(self::STATE_OPTION_KEY, $state, false);
    }

    /**
     * Send summary email if enabled.
     *
     * @param array<string,mixed> $entry Run entry.
     * @return void
     */
    private static function maybe_send_email_summary(array $entry): void {
        $state = self::get_state();
        if (empty($state['email_enabled'])) {
            return;
        }

        $recipients = self::parse_recipient_list((string) ($state['email_recipients'] ?? ''));
        if (empty($recipients)) {
            return;
        }

        $metrics = (array) ($entry['metrics'] ?? []);
        $subject = sprintf(
            '[%s] %s',
            wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES),
            __('Internal Linking Cycle Summary', 'prorank-seo')
        );

        $body_lines = [
            __('Internal linking automation cycle completed successfully.', 'prorank-seo'),
            '',
            sprintf('%s %s', __('Run ID:', 'prorank-seo'), (string) ($entry['run_id'] ?? '')),
            sprintf('%s %s', __('Completed at:', 'prorank-seo'), (string) ($entry['completed_at'] ?? '')),
            sprintf('%s %ss', __('Duration:', 'prorank-seo'), (string) ($entry['duration_seconds'] ?? 0)),
            '',
            sprintf('%s %d', __('Total posts:', 'prorank-seo'), (int) ($metrics['total_posts'] ?? 0)),
            sprintf('%s %d', __('Internal links:', 'prorank-seo'), (int) ($metrics['total_internal_links'] ?? 0)),
            sprintf('%s %d', __('External links:', 'prorank-seo'), (int) ($metrics['total_external_links'] ?? 0)),
            sprintf('%s %d', __('Orphan pages:', 'prorank-seo'), (int) ($metrics['orphan_count'] ?? 0)),
            sprintf('%s %d', __('Broken links:', 'prorank-seo'), (int) ($metrics['broken_links'] ?? 0)),
            sprintf('%s %s%%', __('Link coverage:', 'prorank-seo'), (string) ($metrics['link_coverage'] ?? 0)),
        ];

        wp_mail($recipients, $subject, implode("\n", $body_lines));
    }

    /**
     * Ensure internal-linking endpoint functions are available in non-REST contexts.
     *
     * @return void
     */
    private static function ensure_scan_endpoint_loaded(): void {
        if (function_exists('\\ProRank\\SEO\\Core\\RestApi\\start_full_scan')) {
            return;
        }

        $path = '';
        if (defined('PRORANK_PLUGIN_DIR')) {
            $path = PRORANK_PLUGIN_DIR . 'includes/Core/RestApi/InternalLinkingEndpoints.php';
        }

        if ($path === '') {
            $path = dirname(__DIR__, 3) . '/Core/RestApi/InternalLinkingEndpoints.php';
        }

        if (file_exists($path)) {
            require_once $path;
        }
    }

    /**
     * Normalize metrics structure for stored runs.
     *
     * @param array<string,mixed> $metrics Raw metrics.
     * @return array<string,mixed>
     */
    private static function normalize_metrics(array $metrics): array {
        return [
            'total_posts' => max(0, (int) ($metrics['total_posts'] ?? 0)),
            'total_internal_links' => max(0, (int) ($metrics['total_internal_links'] ?? 0)),
            'total_external_links' => max(0, (int) ($metrics['total_external_links'] ?? 0)),
            'orphan_count' => max(0, (int) ($metrics['orphan_count'] ?? 0)),
            'broken_links' => max(0, (int) ($metrics['broken_links'] ?? 0)),
            'link_coverage' => round((float) ($metrics['link_coverage'] ?? 0), 2),
        ];
    }

    /**
     * Normalize a run entry payload.
     *
     * @param array<string,mixed> $entry Raw entry.
     * @return array<string,mixed>
     */
    private static function normalize_run_entry(array $entry): array {
        return [
            'run_id' => sanitize_text_field((string) ($entry['run_id'] ?? '')),
            'started_at' => sanitize_text_field((string) ($entry['started_at'] ?? '')),
            'completed_at' => sanitize_text_field((string) ($entry['completed_at'] ?? '')),
            'status' => in_array((string) ($entry['status'] ?? ''), ['success', 'failed'], true)
                ? (string) $entry['status']
                : 'failed',
            'message' => sanitize_text_field((string) ($entry['message'] ?? '')),
            'triggered_by' => sanitize_text_field((string) ($entry['triggered_by'] ?? 'manual')),
            'duration_seconds' => max(0, (int) ($entry['duration_seconds'] ?? 0)),
            'metrics' => self::normalize_metrics((array) ($entry['metrics'] ?? [])),
        ];
    }

    /**
     * Build clean comma-separated recipient string.
     *
     * @param string $raw Raw recipient value.
     * @return string
     */
    private static function sanitize_recipient_string(string $raw): string {
        $emails = self::parse_recipient_list($raw);
        return implode(', ', $emails);
    }

    /**
     * Parse recipients into unique valid email list.
     *
     * @param string $raw Raw recipients string.
     * @return array<int,string>
     */
    private static function parse_recipient_list(string $raw): array {
        $parts = preg_split('/[\s,;]+/', $raw) ?: [];
        $emails = [];

        foreach ($parts as $part) {
            $candidate = sanitize_email((string) $part);
            if ($candidate !== '' && is_email($candidate)) {
                $emails[] = $candidate;
            }
        }

        return array_values(array_unique($emails));
    }

    /**
     * Resolve recurrence key from frequency.
     *
     * @param string $frequency Frequency value.
     * @return string
     */
    private static function get_recurrence_key(string $frequency): string {
        return match ($frequency) {
            'daily' => 'daily',
            'monthly' => 'monthly',
            default => 'weekly',
        };
    }

    /**
     * Remove all scheduled operation events.
     *
     * @return void
     */
    private static function unschedule_all(): void {
        while (true) {
            $timestamp = wp_next_scheduled(self::SCHEDULE_HOOK);
            if (!$timestamp) {
                break;
            }
            wp_unschedule_event((int) $timestamp, self::SCHEDULE_HOOK);
        }
    }

    /**
     * Default state payload.
     *
     * @return array<string,mixed>
     */
    private static function get_default_state(): array {
        return [
            'enabled' => false,
            'frequency' => 'weekly',
            'email_enabled' => false,
            'email_recipients' => self::sanitize_recipient_string((string) get_option('admin_email', '')),
            'last_run_at' => '',
            'last_run_status' => 'idle',
            'last_run_message' => '',
        ];
    }
}
