<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Health;

// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
// Custom plugin tables (prli_*): table names interpolated from $wpdb->prefix (trusted),
// user values bind through $wpdb->prepare(). No caching: these tables are the source
// of truth for click/redirect data and must read-through. "meta_key"/"meta_value" here
// refer to our own prli_link_metas table, not wp_postmeta.
use PrettyLinks\Pro\Options\Store as OptionsStore;

/**
 * Sends a periodic digest email listing links that the HealthChecker has
 * flagged as broken (non-2xx, non-3xx HTTP status). Runs on its own cron
 * cadence — independent of when health checks themselves run.
 */
class EmailDigest
{
    public const EVENT = 'prli_send_broken_link_emails';

    /**
     * Pro options store.
     *
     * @var OptionsStore
     */
    private OptionsStore $options;

    /**
     * Constructor.
     *
     * @param OptionsStore $options Pro options store.
     */
    public function __construct(OptionsStore $options)
    {
        $this->options = $options;
    }

    /**
     * Schedules or clears the digest cron event based on the current options.
     *
     * @return void
     */
    public function ensureScheduled(): void
    {
        if (!(bool) $this->options->getPro('enable_link_health_emails', false)) {
            self::unscheduleStatic();
            return;
        }

        $frequency = (string) $this->options->getPro('link_health_emails_frequency', 'weekly');
        // V4 no longer offers Daily — normalize legacy stored values so
        // the UI dropdown reflects what the cron actually does.
        if ($frequency !== 'weekly' && $frequency !== 'monthly') {
            $frequency = 'weekly';
            $this->options->setPro('link_health_emails_frequency', 'weekly');
        }
        $recurrence = self::recurrenceFor($frequency);

        // If the user changed frequency since the last schedule, clear the
        // existing event so we don't keep firing on the old cadence.
        $current = wp_get_schedule(self::EVENT);
        if ($current !== false && $current !== $recurrence) {
            wp_clear_scheduled_hook(self::EVENT);
        }

        if (!wp_next_scheduled(self::EVENT)) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, $recurrence, self::EVENT);
        }
    }

    /**
     * Unschedules and clears the digest cron event.
     *
     * @return void
     */
    public static function unscheduleStatic(): void
    {
        $timestamp = wp_next_scheduled(self::EVENT);
        if ($timestamp) {
            wp_unschedule_event($timestamp, self::EVENT);
        }
        wp_clear_scheduled_hook(self::EVENT);
    }

    /**
     * Builds and sends the broken-links digest email to the configured recipients.
     *
     * @return void
     */
    public function run(): void
    {
        $brokenLinks = $this->fetchBrokenLinks();
        if ($brokenLinks === []) {
            return;
        }

        $recipients = $this->resolveRecipients();
        if ($recipients === []) {
            return;
        }

        $siteName = (string) get_bloginfo('name');
        $adminUrl = admin_url('admin.php?page=pretty-link-edit&id=');

        ob_start();
        include __DIR__ . '/templates/broken-links.php';
        $message = (string) ob_get_clean();

        $subject = sprintf(
            // phpcs:ignore Squiz.Commenting.InlineComment.NotCapital,Squiz.Commenting.InlineComment.InvalidEndChar -- translators comment must stay verbatim for i18n tooling.
            // translators: %s: site name
            __('Broken Links Report - %s', 'pretty-link-pro'),
            $siteName
        );

        $fromName  = (string) apply_filters('prli_email_from_name', get_bloginfo('name'));
        $fromEmail = (string) apply_filters('prli_email_from_email', get_option('admin_email'));

        $args = apply_filters('prli_link_health_email_args', [
            'to'      => $recipients,
            'subject' => $subject,
            'message' => $message,
            'headers' => [
                'From: ' . $fromName . ' <' . $fromEmail . '>',
                'Content-Type: text/html; charset=' . get_option('blog_charset'),
            ],
        ]);

        wp_mail(
            $args['to'],
            (string) $args['subject'],
            (string) $args['message'],
            (array) $args['headers']
        );
    }

    /**
     * Queries links currently flagged as broken by the health checker.
     *
     * @return array<int, array<string, mixed>>
     */
    private function fetchBrokenLinks(): array
    {
        global $wpdb;

        $linksTable = $wpdb->prefix . 'prli_links';
        $metaTable  = $wpdb->prefix . 'prli_link_metas';

        $sql = "SELECT l.id, l.name, l.slug, l.url,
                       MAX(CASE WHEN m.meta_key = %s THEN m.meta_value END) AS status,
                       MAX(CASE WHEN m.meta_key = %s THEN m.meta_value END) AS status_code,
                       MAX(CASE WHEN m.meta_key = %s THEN m.meta_value END) AS last_checked
                FROM {$linksTable} l
                LEFT JOIN {$metaTable} m
                    ON m.link_id = l.id AND m.meta_key IN (%s, %s, %s)
                WHERE l.deleted_at IS NULL
                GROUP BY l.id
                HAVING status = 'inactive'";

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                $sql,
                HealthChecker::META_STATUS,
                HealthChecker::META_STATUS_CODE,
                HealthChecker::META_LAST_CHECKED,
                HealthChecker::META_STATUS,
                HealthChecker::META_STATUS_CODE,
                HealthChecker::META_LAST_CHECKED
            ),
            ARRAY_A
        );

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

    /**
     * Resolves the list of recipient email addresses for the digest.
     *
     * @return array<int, string>
     */
    private function resolveRecipients(): array
    {
        $raw  = (string) $this->options->getPro('link_health_emails', '');
        $list = [];
        foreach (explode(',', $raw) as $candidate) {
            $email = trim($candidate);
            if ($email !== '' && is_email($email)) {
                $list[] = $email;
            }
        }
        if ($list === []) {
            $admin = (string) get_option('admin_email');
            if ($admin !== '' && is_email($admin)) {
                $list[] = $admin;
            }
        }
        return $list;
    }

    /**
     * Maps a frequency option to a WP-Cron recurrence key.
     *
     * @param  string $frequency Stored frequency option value.
     * @return string
     */
    private static function recurrenceFor(string $frequency): string
    {
        // 'daily' was an option in early 4.0 builds and is no longer
        // selectable — coerce any legacy stored value to 'weekly'.
        if ($frequency === 'monthly') {
            return 'prli_monthly';
        }
        return 'weekly';
    }

    /**
     * Registers the custom monthly cron schedule.
     *
     * @param  array<string, array<string, mixed>> $schedules Existing cron schedules.
     * @return array<string, array<string, mixed>>
     */
    public static function cronSchedules(array $schedules): array
    {
        if (!isset($schedules['prli_monthly'])) {
            $schedules['prli_monthly'] = [
                'interval' => 30 * DAY_IN_SECONDS,
                'display'  => 'Monthly (Pretty Links)',
            ];
        }
        return $schedules;
    }
}
