<?php

/**
 * LolaCore — Hub Pairing Registrar
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Hub;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Custom LolaCore config table queries with validated inputs.

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class PairingRegistrar
 *
 * Records the LolaHub master connection on the child site (sprint C0, DEC-1).
 *
 * Pairing uses the native WordPress Application Passwords authorization flow
 * (wp-admin/authorize-application.php): WP core shows its consent screen,
 * generates the credential and returns it to the master. There is no custom
 * handshake token and no cryptographic signature of our own — authentication
 * is the Application Password itself (PRD-MASTER §1.1).
 *
 * This class only observes that native lifecycle:
 *
 * - An Application Password whose name starts with "LolaHub", created for a
 *   user with `manage_options`, registers the master identity (label, user,
 *   credential uuid, timestamp) and flips the node to `connected`.
 * - Deleting that same Application Password (revocation from the child's own
 *   wp-admin, or from the master) purges the master identity atomically and
 *   returns the node to `disconnected`. The connection status never lies.
 *
 * The node identity (`hub_node_id`) is independent of any master and
 * survives disconnection.
 */
class PairingRegistrar
{
    /**
     * Application Password name prefix that identifies a LolaHub master.
     *
     * The master sends its label through the native flow's `app_name`
     * parameter (e.g. "LolaHub — Marta's Agency"), which becomes the
     * Application Password name and, here, the master label.
     */
    public const APP_NAME_PREFIX = 'lolahub';

    /**
     * Config keys owned by the master connection.
     *
     * Deleted as one family on disconnect (revocation must be trivial, C2).
     * `hub_node_id` is deliberately NOT here: it is the node's own identity,
     * not the master's.
     *
     * @var string[]
     */
    private const MASTER_KEYS = [
        'hub_master_label',
        'hub_master_user_id',
        'hub_app_password_uuid',
        'hub_connected_at',
    ];

    /**
     * Hook into the native Application Passwords lifecycle.
     */
    public function register(): void
    {
        \add_action('wp_create_application_password', [$this, 'onApplicationPasswordCreated'], 10, 2);
        \add_action('wp_delete_application_password', [$this, 'onApplicationPasswordDeleted'], 10, 2);
    }

    /**
     * Register the master connection when its Application Password is created.
     *
     * @param int                  $userId User the Application Password belongs to.
     * @param array<string, mixed> $item   The new Application Password item (uuid, name, app_id, ...).
     */
    public function onApplicationPasswordCreated(int $userId, array $item): void
    {
        $name = isset($item['name']) ? (string) $item['name'] : '';

        if (!$this->isLolaHubName($name)) {
            return;
        }

        // Remote orders will execute as this user (C1): require an admin.
        if (!\user_can($userId, 'manage_options')) {
            return;
        }

        $this->setConfigValue('hub_master_label', \sanitize_text_field($name));
        $this->setConfigValue('hub_master_user_id', (string) $userId);
        $this->setConfigValue('hub_app_password_uuid', isset($item['uuid']) ? (string) $item['uuid'] : '');
        $this->setConfigValue('hub_connected_at', \current_time('mysql'));
        $this->setConfigValue('hub_connection_status', 'connected');
    }

    /**
     * Disconnect when the master's Application Password is revoked.
     *
     * Any other Application Password deletion is ignored.
     *
     * @param int                  $userId User the Application Password belonged to.
     * @param array<string, mixed> $item   The deleted Application Password item.
     */
    public function onApplicationPasswordDeleted(int $userId, array $item): void
    {
        unset($userId);

        $uuid       = isset($item['uuid']) ? (string) $item['uuid'] : '';
        $storedUuid = (string) ($this->getConfigValue('hub_app_password_uuid', '') ?? '');

        if ($uuid === '' || $storedUuid === '' || !hash_equals($storedUuid, $uuid)) {
            return;
        }

        $this->disconnect();
    }

    /**
     * Purge the master identity atomically and mark the node disconnected.
     *
     * The node keeps its own identity (`hub_node_id`).
     */
    public function disconnect(): void
    {
        foreach (self::MASTER_KEYS as $key) {
            $this->deleteConfigValue($key);
        }

        $this->setConfigValue('hub_connection_status', 'disconnected');
    }

    /**
     * Stable node identity. Regenerates (and persists) if missing.
     */
    public function getNodeId(): string
    {
        $nodeId = (string) ($this->getConfigValue('hub_node_id', '') ?? '');

        if ($nodeId === '') {
            $nodeId = \wp_generate_uuid4();
            $this->setConfigValue('hub_node_id', $nodeId);
        }

        return $nodeId;
    }

    /**
     * Connection state, consumed by the discover endpoint (and by C2 later).
     *
     * @return array{status: string, master_label: string, connected_at: string}
     */
    public function getConnectionState(): array
    {
        return [
            'status'       => (string) ($this->getConfigValue('hub_connection_status', 'disconnected') ?? 'disconnected'),
            'master_label' => (string) ($this->getConfigValue('hub_master_label', '') ?? ''),
            'connected_at' => (string) ($this->getConfigValue('hub_connected_at', '') ?? ''),
        ];
    }

    /**
     * Whether the node currently has an active master connection.
     */
    public function isConnected(): bool
    {
        return ($this->getConfigValue('hub_connection_status', 'disconnected') ?? 'disconnected') === 'connected';
    }

    /**
     * WordPress user ID of the paired master (Application Password owner).
     */
    public function getMasterUserId(): int
    {
        return (int) ($this->getConfigValue('hub_master_user_id', '0') ?? '0');
    }

    /**
     * Human-readable master label from the Application Password name.
     */
    public function getMasterLabel(): string
    {
        return (string) ($this->getConfigValue('hub_master_label', '') ?? '');
    }

    /**
     * True when the current REST request is authenticated as the paired master.
     */
    public function isAuthenticatedMaster(): bool
    {
        if (!$this->isConnected()) {
            return false;
        }

        $masterId = $this->getMasterUserId();
        if ($masterId <= 0) {
            return false;
        }

        if (!function_exists('get_current_user_id')) {
            return false;
        }

        return (int) get_current_user_id() === $masterId;
    }

    /**
     * Whether an Application Password name identifies a LolaHub master.
     */
    private function isLolaHubName(string $name): bool
    {
        $name = strtolower(trim($name));

        return $name !== '' && str_starts_with($name, self::APP_NAME_PREFIX);
    }

    /**
     * Retrieve a value from the lolacore_config table.
     *
     * @param string      $key     The config key.
     * @param string|null $default Default value if key not found.
     */
    private function getConfigValue(string $key, ?string $default = null): ?string
    {
        global $wpdb;
        $table = $wpdb->prefix . 'lolacore_config';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Custom LolaCore table query.
        $value = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT config_value FROM {$table} WHERE config_key = %s",
                $key,
            )
        );

        return $value ?? $default;
    }

    /**
     * Set a value in the lolacore_config table (upsert).
     */
    private function setConfigValue(string $key, string $value): void
    {
        global $wpdb;
        $table = $wpdb->prefix . 'lolacore_config';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Custom LolaCore table query.
        $wpdb->query(
            $wpdb->prepare(
                "INSERT INTO {$table} (config_key, config_value) VALUES (%s, %s)
                 ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)",
                $key,
                $value,
            )
        );
    }

    /**
     * Delete a config entry by key.
     */
    private function deleteConfigValue(string $key): void
    {
        global $wpdb;
        $table = $wpdb->prefix . 'lolacore_config';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Custom LolaCore table query.
        $wpdb->delete($table, ['config_key' => $key], ['%s']);
    }
}
