<?php
/**
 * SwapAds Client — Approvals Hub Page (F120, 2026-08-01).
 *
 * Two-way transparency view: shows the operator (A) which OTHER
 * operators (B) approved which of A's backlink offers.
 *
 * Server-side GET /swapads-server/v1/backlinks/approvals returns:
 *   {
 *     success: true,
 *     items: [
 *       { approval_id, backlink_id, status, approved_at, rejected_at,
 *         rejection_reason, approver_site_id, approver_site_url,
 *         approver_site_domain, source_url, anchor_text,
 *         target_audience, created_at },
 *       ...
 *     ],
 *     count: N,
 *     filter: { backlink_id, status }
 *   }
 *
 * Visible in the Backlinks Hub as the 3rd tab "Approvals" (after
 * "Your Backlinks" + "Place Backlinks").
 *
 * Per "build-for-change" rule (locked 2026-07-28): every render path
 * goes through SwapAds\Client\Admin\Renderer static helpers.
 *
 * Permission model: requires `manage_options` (standard operator-only
 * capability; matches the rest of the Backlinks hub).
 *
 * @since 1.5.4
 */

declare(strict_types=1);

namespace SwapAds\Client\Admin;

use SwapAds\Client\Api\RestClient;
use SwapAds\Client\License\LicenseManager;
use SwapAds\Client\Http\Request;

/**
 * Class ApprovalsHubPage.
 *
 * @since 1.5.4
 */
final class ApprovalsHubPage
{
    /** Menu slug for the legacy/routeable placeholder. */
    public const MENU_SLUG = 'swapads-client-approvals';

    /** Transient key for surfacing server errors to operators. */
    public const ERROR_CONTEXT = 'swapads_client_approvals';

    /** Default rows per page (matches BacklinkApprovalPage::DEFAULT_LIMIT). */
    public const DEFAULT_LIMIT = 25;

    /** Max rows server allows in a single page (matches RestClient guard). */
    public const MAX_LIMIT = 500;

    /** Allowed status filter values — mirrors BacklinkRepository. */
    public const ALLOWED_STATUSES = ['pending', 'approved', 'placed', 'rejected'];

    /**
     * Register WP hooks.
     *
     * F2XX (2026-08-01): register() MUST wire `admin_menu` action. Missing
     * it (the regression) left addMenu() never invoked.
     *
     * @since 1.5.4
     */
    public static function register(): void
    {
        add_action('admin_menu', [self::class, 'addMenu']);
        // F2XX-fix 2026-08-01: hide the sidebar link via CSS instead of
        // remove_submenu_page() (which broke the cap check in WP 7.0).
        add_action('admin_head', [self::class, 'hideSubmenuFromSidebar']);
    }

    /**
     * Register as a HIDDEN submenu of the operator dashboard hub.
     *
     * Hidden (visible-through-direct-URL-only) because the operator-facing
     * entry point is the 3rd tab on the Backlinks Hub
     * (swapads-client-backlinks-hub?tab=approvals). The legacy slug is
     * preserved so existing bookmarks keep working.
     *
     * @since 1.5.4
     */
    public static function addMenu(): void
    {
        add_submenu_page(
            OperatorDashboardHubPage::MENU_SLUG,
            'Backlink Approvals',
            'Backlink Approvals',
            'manage_options',
            self::MENU_SLUG,
            [self::class, 'render']
        );
        // F2XX (2026-08-01): keep the sidebar count locked to 4 visible
        // submenus. This page is reachable via direct URL + the Backlinks
        // Hub's "Approvals" tab.
        // F2XX-fix 2026-08-01: do NOT call remove_submenu_page() — it breaks
        // admin-post.php routing in WP 7.0. Sidebar link is hidden via
        // admin_head CSS (see hideSubmenuFromSidebar).
    }

    /**
     * Render the full page (used when accessed via the legacy URL).
     *
     * For normal navigation, BacklinksHubPage delegates the actual
     * rendering here via renderSection().
     *
     * @since 1.5.4
     */
    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 '<h1>' . esc_html__('Backlink Approvals', 'swapads-client') . '</h1>';
        echo '<p class="description">'
            . esc_html__('Two-way transparency: see which other operators approved your backlink offers.', 'swapads-client')
            . '</p>';

        self::renderSection();

        echo '</div>'; // .wrap
    }

    /**
     * Render just the approvals card + table, suitable for embedding
     * inside the BacklinksHubPage's "Approvals" tab.
     *
     * @since 1.5.4
     */
    public static function renderSection(): void
    {
        if (!LicenseManager::isLicensed()) {
            echo Renderer::card(
                'License required',
                '<p>You need an active license before viewing backlink approvals.</p>'
                    . '<p><a href="' . esc_url(admin_url('admin.php?page=' . SettingsPage::MENU_SLUG)) . '" class="button">Activate license</a></p>',
                'swapads-card-warning'
            );
            return;
        }

        // F2XX (2026-08-01): surface persisted error envelopes + clear them.
        $persisted = get_transient(self::ERROR_CONTEXT);
        if (is_array($persisted)) {
            // Don't render as a red error if this transient carries a
            // success summary — keep the success/error separation invariant.
            if (($persisted['error_code'] ?? '') !== 'APPROVALS_SUMMARY') {
                echo ClientErrorRenderer::render(self::ERROR_CONTEXT, $persisted, 'load backlink approvals');
            }
            delete_transient(self::ERROR_CONTEXT);
        }

        // Read filters from query string (with sanitization).
        $statusFilter = Request::input('status', '', 'get');
        if (!in_array($statusFilter, self::ALLOWED_STATUSES, true)) {
            $statusFilter = '';
        }
        $limit = Request::inputInt('limit', self::DEFAULT_LIMIT, 'get');
        $limit = max(1, min(self::MAX_LIMIT, $limit));

        $data = self::fetchApprovals(0, $statusFilter, $limit);

        if (isset($data['error'])) {
            echo Renderer::card(
                'Server unreachable',
                '<p>Could not fetch approvals: <code>' . esc_html((string) $data['error']) . '</code></p>',
                'swapads-card-warning'
            );
            return;
        }

        $items = $data['items'];
        $count = (int) $data['count'];

        // Top stats bar + filter form
        $statsHtml  = '<p>';
        $statsHtml .= '<span class="swapads-stat"><strong>' . (int) $count . '</strong> approvals</span>';
        $statsHtml .= '</p>';

        $filterUrl = add_query_arg([], remove_query_arg(['status', 'limit']));
        $filterHtml  = '<form method="get" class="swapads-inline-form">';
        $filterHtml .= '<input type="hidden" name="page" value="' . esc_attr(BacklinksHubPage::MENU_SLUG) . '">';
        $filterHtml .= '<input type="hidden" name="tab" value="approvals">';
        $filterHtml .= '<label>Status:&nbsp;<select name="status">';
        $filterHtml .= '<option value="">All statuses</option>';
        foreach (self::ALLOWED_STATUSES as $st) {
            $sel = $statusFilter === $st ? ' selected' : '';
            $filterHtml .= '<option value="' . esc_attr($st) . '"' . $sel . '>' . esc_html(ucfirst($st)) . '</option>';
        }
        $filterHtml .= '</select></label> &nbsp;';
        $filterHtml .= '<label>Limit:&nbsp;<input type="number" name="limit" value="' . esc_attr((string) $limit) . '" min="1" max="' . esc_attr((string) self::MAX_LIMIT) . '" style="width:80px"></label>';
        $filterHtml .= '&nbsp;<button type="submit" class="button">Apply</button>';
        $filterHtml .= '</form>';

        echo Renderer::card('Approvals', $statsHtml . $filterHtml);

        if ($count === 0) {
            echo Renderer::card(
                '',
                '<p>No approvals yet. When other operators approve your backlink offers, they will appear here.</p>'
            );
            return;
        }

        // Table of approvals
        $table  = '<table class="widefat swapads-table">';
        $table .= '<thead>';
        $table .= '<tr>';
        $table .= '<th>Partner</th>';
        $table .= '<th>Your backlink</th>';
        $table .= '<th>Status</th>';
        $table .= '<th>When</th>';
        $table .= '<th>Note</th>';
        $table .= '</tr>';
        $table .= '</thead>';
        $table .= '<tbody>';
        foreach ($items as $it) {
            $table .= self::renderRow($it);
        }
        $table .= '</tbody>';
        $table .= '</table>';

        echo $table;

        // Legend / help text below the table
        $legend  = '<p class="description">';
        $legend .= '<strong>Status meanings:</strong> '
            . '<code>pending</code> = partner has not yet decided; '
            . '<code>approved</code> = partner said yes (your backlink should appear on their page); '
            . '<code>placed</code> = partner confirmed the link is now live on their site (source-check verified); '
            . '<code>rejected</code> = partner declined (with optional reason).';
        $legend .= '</p>';
        echo Renderer::card('', $legend);
    }

    /**
     * Render a single row of the approvals table.
     *
     * @param array<string, mixed> $it
     *
     * @since 1.5.4
     */
    private static function renderRow(array $it): string
    {
        $partner = (string) ($it['approver_site_domain'] ?? '');
        if ($partner === '') {
            $partner = (string) ($it['approver_site_url'] ?? '');
        }
        $anchor  = (string) ($it['anchor_text'] ?? '');
        $source  = (string) ($it['source_url'] ?? '');
        $status  = (string) ($it['status'] ?? 'pending');
        $when    = (string) ($it['approved_at'] ?? '');
        if ($when === '' || $when === '0000-00-00 00:00:00') {
            $when = (string) ($it['created_at'] ?? '');
        }
        $reason  = (string) ($it['rejection_reason'] ?? '');
        $audience = (string) ($it['target_audience'] ?? '');
        $bid     = (int) ($it['backlink_id'] ?? 0);
        $approvalId = (int) ($it['approval_id'] ?? 0);

        // Status pill colour
        $pillClass = match ($status) {
            'approved', 'placed' => 'swapads-pill swapads-pill-success',
            'rejected'           => 'swapads-pill swapads-pill-error',
            default              => 'swapads-pill swapads-pill-neutral',
        };

        $row  = '<tr>';
        $row .= '<td>';
        if ($partner !== '') {
            $row .= '<strong>' . esc_html($partner) . '</strong>';
        } else {
            $row .= '<em>unknown partner</em>';
        }
        if ($audience !== '') {
            $row .= '<br><small class="description">audience: ' . esc_html($audience) . '</small>';
        }
        $row .= '</td>';
        $row .= '<td>';
        $row .= '<div>' . esc_html($anchor) . '</div>';
        if ($source !== '') {
            $row .= '<small class="description">' . esc_html($source) . '</small>';
        }
        $row .= '</td>';
        $row .= '<td><span class="' . esc_attr($pillClass) . '">' . esc_html(ucfirst($status)) . '</span></td>';
        $row .= '<td><small>' . esc_html($when) . '</small></td>';
        $row .= '<td><small>' . ($reason !== '' ? esc_html($reason) : '—') . '</small></td>';
        $row .= '</tr>';

        return $row;
    }

    /**
     * Fetch approvals from the server.
     *
     * Returns a normalized array even on transport / auth failure so
     * the caller can always read `items` / `count` without null checks.
     * When the server is unreachable, `error` carries the message.
     *
     * @return array{items: array<int, array<string, mixed>>, count: int, limit: int, error?: string}
     *
     * @since 1.5.4
     */
    public static function fetchApprovals(int $backlinkId = 0, string $status = '', int $limit = self::DEFAULT_LIMIT): array
    {
        $rest = RestClient::fromOption();
        $resp = $rest->listBacklinkApprovals($backlinkId, $status, $limit);

        if (!is_array($resp)) {
            return ['items' => [], 'count' => 0, 'limit' => $limit, 'error' => 'no response from server'];
        }

        // Surface server-side error envelopes (e.g. LICENSE_NOT_FOUND).
        if (isset($resp['success']) && $resp['success'] === false) {
            $errMsg = (string) ($resp['message'] ?? 'server returned an error envelope');
            return ['items' => [], 'count' => 0, 'limit' => $limit, 'error' => $errMsg];
        }

        $items = $resp['items'] ?? [];
        if (!is_array($items)) {
            $items = [];
        }

        return [
            'items' => $items,
            'count' => count($items),
            'limit' => $limit,
        ];
    }


    /**
     * Hide the sidebar link via CSS instead of remove_submenu_page().
     *
     * F2XX-fix 2026-08-01: WP 7.0's user_can_access_admin_page() iterates
     * $submenu[$parent] looking for the slug; if remove_submenu_page() removed
     * the entry, the cap check returned false and the operator saw
     * 'Sorry, you are not allowed to access this page' on form postbacks.
     *
     * Keeping the entry in $submenu (so the cap check passes) and hiding the
     * visual link via CSS is the correct WP 7.0 pattern.
     */
    public static function hideSubmenuFromSidebar(): void
    {
        echo '<style>#adminmenu a[href*="page=swapads-client-approvals-hub"]{display:none!important}</style>';
    }
}
