<?php
/**
 * Source Check Admin Page (F117).
 *
 * Operator-facing UI for the daily source-URL check.
 *
 * Per BACKLINK-SYSTEM-ARCHITECTURE §6.4: server daily GET request to
 * each offer's source URL. On 404 / no_content / error → mark offer
 * paused + refund all approvers (handled by SourceCheckService).
 *
 * This page:
 *   - Lists all of the operator's backlink offers with their
 *     source-check status (last_checked_at, last_source_check_status).
 *   - Highlights offers that are overdue (>20h since last check).
 *   - Highlights offers whose status is not "ok" (paused, 404, etc).
 *   - Lets the operator manually trigger a recheck on individual offers
 *     or all active offers in one click.
 *
 * Errors from the API are surfaced inline via ClientErrorRenderer
 * (F198 pattern).
 *
 * @package SwapAds\Client\Admin
 * @since   1.5.0
 */

declare(strict_types=1);

namespace SwapAds\Client\Admin;

use SwapAds\Client\Admin\OperatorDashboardHubPage;
use SwapAds\Client\Api\RestClient;
use SwapAds\Client\License\LicenseManager;
use SwapAds\Client\Http\Redirect;
use SwapAds\Client\Http\Request;

/**
 * Class SourceCheckPage.
 *
 * @since 1.5.0
 */
final class SourceCheckPage
{
    public const MENU_SLUG    = 'swapads-client-source-check';
    public const NONCE_ACTION = 'swapads_client_source_check';
    public const ERROR_CONTEXT = 'swapads_client_source_check';
    public const RECHECK_BATCH_LIMIT = 50;

    /**
     * Register WP hooks.
     */
    public static function register(): void
    {
        add_action('admin_menu', [self::class, 'addMenu']);
        add_action('admin_post_swapads_client_source_check_recheck', [self::class, 'handleRecheck']);
    }

    /**
     * Register the admin page (sub-menu of SwapAds).
     */
    public static function addMenu(): void
    {
        add_submenu_page(
            OperatorDashboardHubPage::MENU_SLUG,
            'Source Check Status',
            'Source Check',
            'manage_options',
            self::MENU_SLUG,
            [self::class, 'render']
        );
    }

    /**
     * Render the source-check admin page (full page wrapper).
     */
    public static function render(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die('Insufficient permissions', 'Forbidden', ['response' => 403]);
        }

        echo '<div class="wrap swapads-page">';
        echo Renderer::pageHeader(
            'Source Check Status',
            'Each of your backlink offers is checked daily. Offers that return 404 or no content are auto-paused and approvers are refunded.'
        );
        self::renderSection();
        echo Renderer::pageFooter();
        echo '</div>'; // .wrap
    }

    /**
     * Render just the source-check cards (no page wrapper).
     *
     * TODO-CLIENT-007 (2026-08-02): embeddable version used by the
     * BacklinksHubPage "My Backlinks" tab. No <div class="wrap"> wrapper.
     *
     * @since 1.5.10
     */
    public static function renderSection(): void
    {
        // F198: surface any persisted error from previous recheck attempt.
        // F2XX (2026-08-01): also surface the success-summary transient that
        // handleRecheck() writes on a successful run (was getting rendered as
        // a red "failed" banner before this fix).
        $persisted = get_transient(self::ERROR_CONTEXT);
        if (is_array($persisted)) {
            echo ClientErrorRenderer::render(self::ERROR_CONTEXT, $persisted, 'recheck source URLs');
            delete_transient(self::ERROR_CONTEXT);
        }
        $summary = get_transient('swapads_source_check_summary');
        if (is_array($summary)) {
            echo ClientErrorRenderer::render('swapads_source_check_summary', $summary, 'recheck source URLs');
            delete_transient('swapads_source_check_summary');
        }

        if (!LicenseManager::isLicensed()) {
            echo Renderer::card(
                'License required',
                '<p>Activate your license before viewing source-check status.</p>',
                'swapads-card-warning'
            );
            return;
        }

        $data = self::fetchSourceCheckStatus();
        if (!is_array($data)) {
            echo Renderer::card(
                'Source check unavailable',
                '<p>Could not fetch source-check data from the server. See the notice above for the exact response.</p>',
                'swapads-card-error'
            );
            return;
        }

        $items = $data['items'] ?? [];
        $totalCount = (int) ($data['count'] ?? count($items));
        echo self::renderSummaryCard($items, $totalCount);

        if (count($items) === 0) {
            echo Renderer::card(
                'No backlink offers yet',
                '<p>Once you create your first backlink offer, it will appear here with daily source-check status.</p>',
                'swapads-card-info'
            );
            echo self::renderRecheckAllForm();
            return;
        }

        echo self::renderTable($items);
        echo self::renderRecheckAllForm();
    }

    /**
     * Fetch source-check status from server.
     *
     * F198: RestClient::request() never throws for REST/transport
     * failures — lastError() captures the envelope and we persist it
     * for render() to surface inline.
     *
     * @return array<string, mixed>|null Null on error (error persisted to transient).
     */
    public static function fetchSourceCheckStatus(): ?array
    {
        $client = RestClient::fromOption();
        try {
            $response = $client->get('/v1/backlinks/source-check');
        } catch (\Throwable $e) {
            set_transient(self::ERROR_CONTEXT, [
                'error_code'  => 'FATAL',
                'message'     => 'Unexpected fatal error: ' . $e->getMessage(),
                'http_status' => 0,
                'when'        => time(),
            ], MINUTE_IN_SECONDS);
            return null;
        }
        if (!is_array($response)) {
            if (($err = $client->lastError()) !== null) {
                set_transient(self::ERROR_CONTEXT, $err, MINUTE_IN_SECONDS);
            }
            return null;
        }
        if (empty($response['success'])) {
            $err = $client->lastError() ?? $response;
            set_transient(self::ERROR_CONTEXT, $err, MINUTE_IN_SECONDS);
            return null;
        }
        return $response;
    }

    /**
     * Render the summary card (counts by status).
     *
     * @param array<int, array<string, mixed>> $items
     */
    private static function renderSummaryCard(array $items, int $totalCount): string
    {
        $ok = 0;
        $paused = 0;
        $overdue = 0;
        $other = 0;
        $overdueThresholdHours = 20;
        foreach ($items as $it) {
            $status = (string) ($it['status'] ?? '');
            $age    = $it['last_check_age_hours'];
            if ($status === 'active') {
                $ok++;
            } elseif ($status === 'paused') {
                $paused++;
            } else {
                $other++;
            }
            if ($age !== null && (int) $age > $overdueThresholdHours) {
                $overdue++;
            }
        }
        $html  = '<div class="swapads-card">';
        $html .= '<h2>Summary</h2>';
        $html .= '<div class="swapads-metric-grid">';
        $html .= self::renderMetricTile('Total offers',     $totalCount, 'all your backlink offers');
        $html .= self::renderMetricTile('Active',           $ok,         'currently in rotation');
        $html .= self::renderMetricTile('Auto-paused',      $paused,     'paused by source check');
        $html .= self::renderMetricTile('Other status',     $other,      'depleted, removed, etc.');
        $html .= self::renderMetricTile('Overdue check',    $overdue,    'not checked in >20h');
        $html .= '</div></div>';
        return $html;
    }

    /**
     * Render the per-offer source-check table.
     *
     * TODO-CLIENT-007 (2026-08-02): source URL is now clickable + opens in
     * new tab. The single "Recheck" action is replaced by 4 actions
     * (Retract / Reactivate / Delete / Check still alive) via the
     * BacklinkStatusActions class.
     *
     * @param array<int, array<string, mixed>> $items
     */
    private static function renderTable(array $items): string
    {
        $html  = '<div class="swapads-card">';
        $html .= '<h2>Per-Offer Source-Check Status</h2>';
        $html .= '<table class="widefat striped" style="margin-top:8px;">';
        $html .= '<thead><tr>';
        $html .= '<th style="width:60px;">ID</th>';
        $html .= '<th>Source URL</th>';
        $html .= '<th style="width:100px;">Status</th>';
        $html .= '<th style="width:160px;">Last Checked</th>';
        $html .= '<th style="width:160px;">Check Result</th>';
        $html .= '<th style="width:280px;">Actions</th>';
        $html .= '</tr></thead><tbody>';

        foreach ($items as $it) {
            $id = (int) ($it['id'] ?? 0);
            $sourceUrl = (string) ($it['source_url'] ?? '');
            $status = (string) ($it['status'] ?? '');
            $lastCheckedAt = $it['last_checked_at'] ?? null;
            $lastStatus = (string) ($it['last_source_check_status'] ?? '');
            $ageHours = $it['last_check_age_hours'];
            $ageLabel = $ageHours === null ? 'never' : ((int) $ageHours) . 'h ago';

            $statusBadge = self::renderStatusBadge($status);
            $resultBadge = self::renderResultBadge($lastStatus);

            $html .= '<tr>';
            $html .= '<td>' . esc_html((string) $id) . '</td>';
            $html .= '<td>';
            if ($sourceUrl !== '') {
                $html .= '<a href="' . esc_url($sourceUrl) . '" target="_blank" rel="noopener noreferrer">'
                    . '<code style="font-size:11px;">' . esc_html($sourceUrl) . '</code>'
                    . '<span class="screen-reader-text"> (opens in new tab)</span></a>';
            } else {
                $html .= '<code style="font-size:11px;">—</code>';
            }
            $html .= '</td>';
            $html .= '<td>' . $statusBadge . '</td>';
            $html .= '<td>' . esc_html($ageLabel) . ($lastCheckedAt !== null ? '<br><small>' . esc_html((string) $lastCheckedAt) . '</small>' : '') . '</td>';
            $html .= '<td>' . $resultBadge . '</td>';
            $html .= '<td>';
            if ($id > 0) {
                // TODO-CLIENT-007: 4 per-row actions (Retract / Reactivate / Delete / Check still alive).
                $html .= \SwapAds\Client\Admin\BacklinkStatusActions::renderRowActions($it);
            }
            $html .= '</td>';
            $html .= '</tr>';
        }

        $html .= '</tbody></table></div>';
        return $html;
    }

    /**
     * Render the "recheck all active" form.
     */
    private static function renderRecheckAllForm(): string
    {
        $action = esc_url(admin_url('admin-post.php'));
        $nonce  = wp_create_nonce(self::NONCE_ACTION);
        return sprintf(
            '<div class="swapads-card" style="margin-top:16px;">
                <h2>Manual Recheck</h2>
                <p>Force a recheck of all your active offers right now (in addition to the daily 03:00 UTC cron run).</p>
                <form method="post" action="%s">
                    <input type="hidden" name="action" value="swapads_client_source_check_recheck">
                    <input type="hidden" name="_wpnonce" value="%s">
                    <input type="hidden" name="all" value="1">
                    <button type="submit" class="button button-primary">Recheck all active offers</button>
                </form>
            </div>',
            $action,
            esc_attr($nonce)
        );
    }

    /**
     * Handle the recheck form POST.
     */
    public static function handleRecheck(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die('Insufficient permissions', 'Forbidden', ['response' => 403]);
        }
        check_admin_referer(self::NONCE_ACTION);

        $all = Request::input('all', '', 'post') === '1';
        $backlinkId = Request::inputInt('backlink_id', 0, 'post');

        $client = RestClient::fromOption();
        try {
            if ($all) {
                $response = $client->post('/v1/backlinks/source-check/recheck', ['all' => true]);
            } elseif ($backlinkId > 0) {
                $response = $client->post('/v1/backlinks/source-check/recheck', ['backlink_ids' => [$backlinkId]]);
            } else {
                self::redirectWithError('Either all=1 or backlink_id required');
                return;
            }
        } catch (\Throwable $e) {
            set_transient(self::ERROR_CONTEXT, [
                'error_code'  => 'FATAL',
                'message'     => 'Unexpected fatal error: ' . $e->getMessage(),
                'http_status' => 0,
                'when'        => time(),
            ], MINUTE_IN_SECONDS);
            self::redirectWithError($e->getMessage());
            return;
        }

        if (is_array($response) && empty($response['success'])) {
            $err = $client->lastError() ?? $response;
            set_transient(self::ERROR_CONTEXT, $err, MINUTE_IN_SECONDS);
            self::redirectWithError((string) ($response['error_code'] ?? 'unknown'));
            return;
        }

        if (is_array($response) && !empty($response['success'])) {
            $results = $response['results'] ?? [];
            $ok = 0;
            $bad = 0;
            $refunds = 0;
            foreach ($results as $r) {
                $res = (string) ($r['result'] ?? '');
                if ($res === 'ok') { $ok++; } else { $bad++; }
                $refunds += (int) ($r['refunds'] ?? 0);
            }
            // F2XX (2026-08-01): use a SEPARATE transient for success + non-error
            // messaging. ClientErrorRenderer renders RECHECK_SUMMARY as a green
            // success notice (was red "failed" banner before this fix).
            set_transient('swapads_source_check_summary', [
                'error_code'  => 'RECHECK_SUMMARY',
                'message'     => sprintf('Recheck complete: %d ok, %d issues, %d refunds.', $ok, $bad, $refunds),
                'http_status' => 200,
                'when'        => time(),
            ], MINUTE_IN_SECONDS);
        }

        Redirect::to(add_query_arg(['page' => self::MENU_SLUG, 'rechecked' => '1'], admin_url('admin.php')));
    }

    /**
     * Redirect back with an error code.
     */
    private static function redirectWithError(string $errorCode): void
    {
        Redirect::to(add_query_arg(
            ['page' => self::MENU_SLUG, 'error' => rawurlencode($errorCode)],
            admin_url('admin.php')
        ));
    }

    /**
     * Render a status badge (active/paused/depleted).
     */
    private static function renderStatusBadge(string $status): string
    {
        $colors = [
            'active'   => '#46b450',
            'paused'   => '#d63638',
            'depleted' => '#996633',
            'removed'  => '#666',
        ];
        $color = $colors[$status] ?? '#666';
        return sprintf(
            '<span style="display:inline-block;padding:2px 8px;border-radius:3px;background:%s;color:#fff;font-size:11px;font-weight:600;">%s</span>',
            esc_attr($color),
            esc_html($status)
        );
    }

    /**
     * Render a check-result badge (ok/404/no_content/error).
     */
    private static function renderResultBadge(string $result): string
    {
        if ($result === '') {
            return '<span style="color:#666;">—</span>';
        }
        $colors = [
            'ok'         => '#46b450',
            '404'        => '#d63638',
            'no_content' => '#dba617',
            'error'      => '#d63638',
        ];
        $color = $colors[$result] ?? '#996633';
        return sprintf(
            '<span style="display:inline-block;padding:2px 8px;border-radius:3px;background:%s;color:#fff;font-size:11px;font-weight:600;">%s</span>',
            esc_attr($color),
            esc_html($result)
        );
    }

    /**
     * Render a metric tile (used in summary).
     */
    private static function renderMetricTile(string $label, int $value, string $hint): string
    {
        return sprintf(
            '<div class="swapads-metric-tile"><div class="swapads-metric-value">%s</div><div class="swapads-metric-label">%s</div><div class="swapads-metric-hint">%s</div></div>',
            esc_html((string) $value),
            esc_html($label),
            esc_html($hint)
        );
    }
}
