<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Rotation;

// 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\Redirect\DynamicMode;

/**
 * Resolves the next rotation target for a link, if any rotations are
 * configured. Hooks into `prli_target_url` to swap the redirect target
 * on the hot path, and listens for `prli_click_written` (emitted by
 * ClickWriter on shutdown) to record which rotation URL the click went to.
 *
 * V3 drop-in: honors `prli_dynamic_redirection` link meta — rotation
 * only runs when the mode is unset (compose) or `rotate`. v3's
 * mutually-exclusive mode model is preserved when a link explicitly
 * picked `geo`/`tech`/`time`/`none` instead.
 */
class RotationService
{
    /**
     * Per-request record of rotation picks, keyed by link id. Lets the
     * shutdown-time click-writer correlate the newly-inserted click row
     * with the specific rotation row we chose earlier in the request.
     * Stores both id (for the normalized pointer) and url (v3 fallback if
     * the variant is later edited or deleted).
     *
     * @var array<int, array{id: int, url: string}>
     */
    private static array $picksThisRequest = [];

    /**
     * `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;
        }

        $mode = DynamicMode::forLink($linkId);
        if (!DynamicMode::allowsRotation($mode)) {
            return $data;
        }

        $rotations = $this->loadRotations($linkId);
        if (!$rotations) {
            return $data;
        }

        // V3 model: the link's own Target URL is always a participant in the
        // draw — its weight lives in the `prli-target-url-weight` meta, and the
        // rotations table holds only the extra URLs. Prepend it (id 0 — it has
        // no `prli_link_rotations` row; the click recorder stores it with a
        // NULL rotation_id) so the pool is [primary, ...extras] like v3.
        $primaryUrl = (string) ($data['url'] ?? '');
        if ($primaryUrl !== '') {
            array_unshift($rotations, [
                'id'       => 0,
                'url'      => $primaryUrl,
                'weight'   => $this->primaryWeight($linkId),
                'position' => -1,
            ]);
        }

        $strategy = $this->modeFor($linkId) === 'sequential'
            ? new Sequential()
            : new WeightedRandom();

        $picked = $strategy->pick($linkId, $rotations);
        if (!is_array($picked) || empty($picked['url'])) {
            return $data;
        }

        self::$picksThisRequest[$linkId] = [
            'id'  => (int) ($picked['id'] ?? 0),
            'url' => (string) $picked['url'],
        ];
        $data['url']                     = (string) $picked['url'];
        return $data;
    }

    /**
     * `prli_click_written` action callback. Writes a `prli_clicks_rotations`
     * row matching the click we just wrote to whichever rotation URL we
     * picked during the `prli_target_url` filter earlier in this request.
     *
     * Every served URL is recorded — the extra rotation URLs AND the link's
     * own primary Target URL — matching v3 (`PlpLinkRotation::record_click`),
     * where the Split Test report counts each variant by its URL string. The
     * primary pick has no `prli_link_rotations` row (id 0), so it's stored
     * with a NULL `rotation_id`, consistent with the column default and the
     * v3→v4 backfill migration. Without this, a primary-URL pick would never
     * reach `prli_clicks_rotations` and the report would show it at 0 (#671).
     *
     * @param  integer $linkId  Link ID the click belongs to.
     * @param  integer $clickId Newly-written click row ID.
     * @return void
     */
    public function recordRotationClick(int $linkId, int $clickId): void
    {
        if ($clickId <= 0) {
            return;
        }
        if (!isset(self::$picksThisRequest[$linkId])) {
            return;
        }
        $pick = self::$picksThisRequest[$linkId];

        global $wpdb;
        $wpdb->insert(
            $wpdb->prefix . 'prli_clicks_rotations',
            [
                'click_id'    => $clickId,
                'link_id'     => $linkId,
                'url'         => $pick['url'],
                // Primary pick (id 0) → NULL; $wpdb writes a NULL value as the
                // literal NULL regardless of the format placeholder.
                'rotation_id' => $pick['id'] > 0 ? $pick['id'] : null,
            ],
            ['%d', '%d', '%s', '%d']
        );
    }

    /**
     * Loads the configured extra rotation URLs for a link.
     *
     * @param  integer $linkId Link ID to load rotations for.
     * @return array<int, array<string, mixed>>
     */
    private function loadRotations(int $linkId): array
    {
        global $wpdb;
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, url, weight, position FROM {$wpdb->prefix}prli_link_rotations
                 WHERE link_id = %d ORDER BY position ASC, id ASC",
                $linkId
            ),
            ARRAY_A
        ) ?: [];
        return $rows;
    }

    /**
     * Returns the configured rotation strategy mode for a link.
     *
     * @param  integer $linkId Link ID to read the rotation mode for.
     * @return string
     */
    private function modeFor(int $linkId): string
    {
        global $wpdb;
        $row  = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT meta_value FROM {$wpdb->prefix}prli_link_metas
                 WHERE link_id = %d AND meta_key = %s LIMIT 1",
                $linkId,
                'rotation_mode'
            )
        );
        $mode = is_string($row) ? $row : '';
        return $mode === 'sequential' ? 'sequential' : 'weighted';
    }

    /**
     * Weight of the link's own Target URL within the rotation pool. v3 stored
     * it in the `prli-target-url-weight` link meta (default 100); v4 reads it
     * from the same place, so it carries forward with no migration.
     *
     * @param  integer $linkId Link ID to read the primary-URL weight for.
     * @return integer
     */
    private function primaryWeight(int $linkId): int
    {
        global $wpdb;
        $val = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT meta_value FROM {$wpdb->prefix}prli_link_metas
                 WHERE link_id = %d AND meta_key = %s LIMIT 1",
                $linkId,
                'prli-target-url-weight'
            )
        );
        return ($val === null || $val === '') ? 100 : (int) $val;
    }
}
