<?php

// SPDX-FileCopyrightText: 2026 Ovation S.r.l. <dev@novamira.ai>
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace Novamira\OAuth\ConnectedApps;

if (!defined('ABSPATH')) {
    exit();
}

function register(): void
{
    $hook = add_submenu_page(
        parent_slug: '',
        page_title: 'Connected Apps',
        menu_title: '',
        capability: \novamira_manage_capability(),
        menu_slug: 'novamira-connected-apps',
        callback: __NAMESPACE__ . '\\render',
    );

    // The Revoke POST must redirect back before any admin HTML is sent. The page callback runs
    // after the admin header (headers already flushed, so wp_redirect is a no-op and the browser is
    // left on a blank page), so the POST is handled on the load hook, which fires before any output.
    if (is_string($hook) && $hook !== '') {
        add_action('load-' . $hook, __NAMESPACE__ . '\\handle_load');
    }
}

/**
 * Fires before the admin header. Handles the Revoke POST (nonce check, revoke, redirect); GET
 * requests fall through untouched so the page callback can draw the list.
 */
function handle_load(): void
{
    if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
        return;
    }
    if (!is_user_logged_in() || !\novamira_current_user_can_manage()) {
        return;
    }
    handle_post(get_current_user_id());
}

function render(): void
{
    if (!is_user_logged_in()) {
        wp_die('You must be logged in.', title: '', args: ['response' => 403]);
        return;
    }
    if (!\novamira_current_user_can_manage()) {
        wp_die('You are not allowed to manage Novamira connected apps.', title: '', args: ['response' => 403]);
        return;
    }

    render_page(get_current_user_id());
}

function handle_post(int $user_id): void
{
    check_admin_referer('novamira_connected_apps_revoke');

    $raw = $_POST['client_id'] ?? null;
    $client_id = is_string($raw) ? sanitize_key($raw) : '';
    if ($client_id !== '') {
        revoke_client_access($client_id, $user_id);
    }

    wp_redirect(add_query_arg(['revoked' => '1'], admin_url('admin.php?page=novamira-connected-apps')));
    exit();
}

function revoke_client_access(string $client_id, int $user_id): void
{
    // @mago-expect lint:no-global
    global $wpdb;
    /** @var \wpdb $wpdb */
    $t = $wpdb->prefix . 'novamira_oauth_access_tokens';
    $r = $wpdb->prefix . 'novamira_oauth_refresh_tokens';

    // Revoke refresh tokens linked to this client's access tokens.
    // @mago-expect analysis:possibly-invalid-argument
    // @mago-expect analysis:possibly-invalid-argument
    $wpdb->query($wpdb->prepare(
        "UPDATE `{$r}` rt
         JOIN `{$t}` at ON at.identifier_hash = rt.access_token_hash
         SET rt.revoked = 1
         WHERE at.client_id = %s AND at.user_id = %d",
        $client_id,
        $user_id,
    ));

    // Revoke all access tokens for this client and user.
    $wpdb->update($t, ['revoked' => 1], ['client_id' => $client_id, 'user_id' => $user_id]);
}

// @mago-expect lint:halstead
function render_page(int $user_id): void
{
    // @mago-expect lint:no-global
    global $wpdb;
    /** @var \wpdb $wpdb */
    $t = $wpdb->prefix . 'novamira_oauth_access_tokens';
    $r = $wpdb->prefix . 'novamira_oauth_refresh_tokens';
    $c = $wpdb->prefix . 'novamira_oauth_clients';
    $now = gmdate('Y-m-d H:i:s');

    // Key the list off refresh tokens, not access tokens. Access tokens live one hour, so
    // basing the list on them would drop a still-connected app from the view an hour after
    // its last use and show a misleading one-hour expiry. The refresh token (renewed on
    // each use) is the real connection lifetime, so its expiry is what we surface.
    // @mago-expect analysis:possibly-invalid-argument
    // @mago-expect analysis:non-existent-constant
    // @mago-expect analysis:mixed-argument
    // @mago-expect analysis:possibly-invalid-argument
    $rows = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT c.client_name, at.client_id, at.scopes, MAX(rt.expires_at) AS expires_at
         FROM `{$r}` rt
         JOIN `{$t}` at ON at.identifier_hash = rt.access_token_hash
         JOIN `{$c}` c ON c.client_id = at.client_id
         WHERE at.user_id = %d AND rt.revoked = 0 AND rt.expires_at > %s
         GROUP BY at.client_id, c.client_name, at.scopes
         ORDER BY expires_at DESC",
            $user_id,
            $now,
        ),
        ARRAY_A,
    );

    $apps = is_array($rows) ? $rows : [];

    $raw_revoked = $_GET['revoked'] ?? null;
    $was_revoked = is_string($raw_revoked) && $raw_revoked === '1';

    echo '<div class="wrap">';
    echo '<h1>' . esc_html__('Connected Apps', domain: 'novamira') . '</h1>';
    echo
        '<p>'
            . esc_html__(
                'These applications have been granted access to your WordPress account via Novamira. The connection renews automatically while in use; the expiry shown is when it lapses if the app stops connecting.',
                domain: 'novamira',
            )
            . '</p>'
    ;

    if ($was_revoked) {
        echo
            '<div class="notice notice-success is-dismissible"><p>'
                . esc_html__('Access revoked successfully.', domain: 'novamira')
                . '</p></div>'
        ;
    }

    if ($apps === []) {
        echo '<p>' . esc_html__('No apps are currently connected to your account.', domain: 'novamira') . '</p>';
        echo '</div>';
        return;
    }

    echo '<table class="wp-list-table widefat fixed striped">';
    echo '<thead><tr>';
    echo '<th>' . esc_html__('Application', domain: 'novamira') . '</th>';
    echo '<th>' . esc_html__('Scope', domain: 'novamira') . '</th>';
    echo '<th>' . esc_html__('Connection expires', domain: 'novamira') . '</th>';
    echo '<th></th>';
    echo '</tr></thead><tbody>';

    foreach ($apps as $app) {
        // @mago-expect analysis:invalid-array-access
        $name = (string) $app['client_name'];
        // @mago-expect analysis:invalid-array-access
        $cid = (string) $app['client_id'];
        // @mago-expect analysis:invalid-array-access
        $scopes_raw = (string) $app['scopes'];
        // @mago-expect analysis:invalid-array-access
        $expires = (string) $app['expires_at'];

        // @mago-expect analysis:mixed-assignment
        $scopes_arr = json_decode($scopes_raw, associative: true);
        $scopes_str = is_array($scopes_arr)
            ? implode(' ', array_map(static fn(mixed $s): string => is_string($s) ? $s : '', $scopes_arr))
            : $scopes_raw;

        echo '<tr>';
        echo '<td><strong>' . esc_html($name) . '</strong></td>';
        echo '<td>' . esc_html($scopes_str) . '</td>';
        echo '<td>' . esc_html($expires) . '</td>';
        echo '<td>';
        echo '<form method="post">';
        wp_nonce_field('novamira_connected_apps_revoke');
        echo '<input type="hidden" name="client_id" value="' . esc_attr($cid) . '">';
        echo '<button type="submit" class="button">' . esc_html__('Revoke Access', domain: 'novamira') . '</button>';
        echo '</form>';
        echo '</td>';
        echo '</tr>';
    }

    echo '</tbody></table>';
    echo '</div>';
}
