<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Expiration;

// 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.

/**
 * Link expiration.
 *
 * Fires on `prli_target_url` at priority 5 (ahead of rotation/targeting so
 * those don't pick a fresh URL for an already-expired link). Uses the v3
 * meta shape as canonical so users can roll back to v3 without data
 * translation:
 *
 *   - `enable_expire`        boolean gate
 *   - `expire_type`          `date` or `clicks`
 *   - `expire_date`          strtotime-parseable
 *   - `expire_clicks`        integer threshold
 *   - `enable_expired_url`   use per-link override URL
 *   - `expired_url`          per-link override URL
 *
 * One v4-only addition: `expire_action` (`redirect` or `trash`). `trash`
 * soft-deletes the link on the first expired visit; v3 ignores the key on
 * rollback and falls back to its own 404/override semantics.
 */
class ExpirationChecker
{
    /**
     * `prli_target_url` filter callback. Matches v3's array-in/array-out signature.
     *
     * @param  array<string, mixed> $data Merged link row with v3-compat keys.
     * @return array<string, mixed>
     */
    public function filter(array $data): array
    {
        $linkId = (int) ($data['link_id'] ?? $data['id'] ?? 0);
        if ($linkId <= 0) {
            return $data;
        }

        $metas = $this->loadExpirationMeta($linkId);
        if (!$this->isEnabled($metas)) {
            return $data;
        }

        if (!$this->isExpired($data, $metas)) {
            return $data;
        }

        if (($metas['expire_action'] ?? '') === 'trash') {
            // Soft-delete so the slug stops routing entirely; this visitor
            // still gets the expired 404. If an extension blocks the trash
            // (prli_pre_link_trash), the link stays but remains expired —
            // the dead end is correct either way.
            (new \PrettyLinks\Repositories\Links())->softDelete($linkId);
            $this->renderExpired();
        }

        $expiredUrl = $this->resolveExpiredUrl($metas);
        if ($expiredUrl === '') {
            // V3 parity: an expired link with no override URL is a dead end and
            // returns a 404 (the link form documents this) rather than
            // redirecting anywhere. Terminates the request the same way v3's
            // PlpLinksController did.
            $this->renderExpired();
        }

        $data['url'] = $expiredUrl;
        return $data;
    }

    /**
     * Load the expiration-related meta values for a link.
     *
     * @param  integer $linkId The link ID to load meta for.
     * @return array<string, string>
     */
    private function loadExpirationMeta(int $linkId): array
    {
        global $wpdb;
        $keys         = [
            'enable_expire',
            'expire_type',
            'expire_date',
            'expire_clicks',
            'expire_action',
            'enable_expired_url',
            'expired_url',
        ];
        $placeholders = implode(',', array_fill(0, count($keys), '%s'));
        $params       = array_merge([$linkId], $keys);
        $rows         = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT meta_key, meta_value FROM {$wpdb->prefix}prli_link_metas
                 WHERE link_id = %d AND meta_key IN ({$placeholders})",
                ...$params
            ),
            ARRAY_A
        ) ?: [];

        $out = [];
        foreach ($rows as $row) {
            $out[(string) $row['meta_key']] = (string) ($row['meta_value'] ?? '');
        }
        return $out;
    }

    /**
     * Whether expiration is enabled for the link.
     *
     * @param array<string, string> $metas The link expiration meta values.
     *
     * @return boolean True when expiration is enabled.
     */
    private function isEnabled(array $metas): bool
    {
        return ($metas['enable_expire'] ?? '') === '1';
    }

    /**
     * Whether the link has expired by date or click threshold.
     *
     * @param array<string, mixed>  $link  The merged link row.
     * @param array<string, string> $metas The link expiration meta values.
     *
     * @return boolean True when the link is expired.
     */
    private function isExpired(array $link, array $metas): bool
    {
        $type = $metas['expire_type'] ?? 'date';
        if ($type === 'clicks') {
            $threshold = (int) ($metas['expire_clicks'] ?? 0);
            if ($threshold <= 0) {
                return false;
            }
            // The threshold is interpreted as unique visitors across every
            // tracking mode. Normal/Extended populate `uniques` from the
            // `prli_links` fast-read column; Simple mode populates it from
            // the `static-uniques` meta via `Links::applyMetaClicks()`.
            // Counting raw hits would let one visitor on F5 burn the cap.
            $counter = (int) ($link['uniques'] ?? 0);
            return $counter >= $threshold;
        }

        // Default: date-based.
        // Both `expire_date` and current_time('mysql', false) are site-local
        // Y-m-d H:i:s strings in the same timezone, so lexicographic comparison
        // is correct and avoids the fake-epoch problem of current_time('timestamp').
        $dateRaw = $metas['expire_date'] ?? '';
        if ($dateRaw === '' || !preg_match('/^\d{4}-\d{2}-\d{2}/', $dateRaw)) {
            return false;
        }
        return current_time('mysql', false) >= $dateRaw;
    }

    /**
     * Resolve the override URL to redirect an expired link to.
     *
     * @param array<string, string> $metas The link expiration meta values.
     *
     * @return string The override URL, or empty string when none is set.
     */
    private function resolveExpiredUrl(array $metas): string
    {
        if (
            ($metas['enable_expired_url'] ?? '') === '1'
            && isset($metas['expired_url'])
            && $metas['expired_url'] !== ''
        ) {
            return (string) $metas['expired_url'];
        }

        // No per-link override: v3 returns a 404 (the caller renders it). A
        // site can still opt into a site-wide redirect destination here.
        return (string) apply_filters('prli_expired_link_fallback_url', '');
    }

    /**
     * Emit a 404 for an expired link with no override URL, matching v3's
     * PlpLinksController. Terminates the request — the redirect engine's
     * other terminal handlers (cloak, pay) exit the same way.
     */
    private function renderExpired(): void
    {
        nocache_headers();
        wp_die(
            esc_html__('This link has expired.', 'pretty-link-pro'),
            esc_html__('Link Expired', 'pretty-link-pro'),
            ['response' => 404]
        );
    }
}
