<?php
declare(strict_types=1);
// ==================================================
// Nuclear Fancy Pants Edition™ — NUCLEAR+++
// File: /includes/class-x-auth.php
// Description: X (Twitter) OAuth 2.0 PKCE flow, token exchange, encrypted storage, auto-refresh - NUCLEAR+++
// Last Edited: 2026-04-16
// ==================================================

namespace AutoMarketerAI;

// No direct access
defined('ABSPATH') || exit;

final class XAuth
{
    // X API endpoints
    private const AUTH_URL      = 'https://x.com/i/oauth2/authorize';
    private const TOKEN_URL     = 'https://api.x.com/2/oauth2/token';
    private const REVOKE_URL    = 'https://api.x.com/2/oauth2/revoke';
    private const USERS_ME_URL  = 'https://api.x.com/2/users/me';

    // OAuth 2.0 scopes -- tweet.write for posting, media.write for image upload,
    // users.read for profile info, offline.access for refresh tokens
    private const SCOPES = 'tweet.read tweet.write users.read offline.access media.write';

    // Access token lifetime is 2 hours (7200s). Refresh well before expiry.
    private const TOKEN_REFRESH_BUFFER_SECONDS = 300;

    // Cron hook for periodic token refresh
    public const CRON_HOOK = 'amai_x_token_refresh';

    // wp_options keys for PKCE flow state
    private const STATE_OPTION           = 'amai_x_oauth_state';
    private const CODE_VERIFIER_OPTION   = 'amai_x_code_verifier';

    // --------------------------------------------------
    // Public: build the OAuth 2.0 PKCE authorization URL
    // --------------------------------------------------
    public static function getAuthUrl(): string
    {
        if (!defined('AMAI_X_CLIENT_ID') || (string) AMAI_X_CLIENT_ID === '') {
            return '';
        }
        $state = bin2hex(random_bytes(16));
        update_option(self::STATE_OPTION, $state, false);

        // PKCE: generate code_verifier and derive code_challenge
        $codeVerifier = self::generateCodeVerifier();
        update_option(self::CODE_VERIFIER_OPTION, $codeVerifier, false);

        $codeChallenge = self::deriveCodeChallenge($codeVerifier);

        $params = [
            'response_type'         => 'code',
            'client_id'             => self::getClientId(),
            'redirect_uri'          => self::getCallbackUrl(),
            'scope'                 => self::SCOPES,
            'state'                 => $state,
            'code_challenge'        => $codeChallenge,
            'code_challenge_method' => 'S256',
        ];

        return self::AUTH_URL . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
    }

    // --------------------------------------------------
    // Public: handle the OAuth callback
    // --------------------------------------------------
    public static function handleCallback(): void
    {
        if (!current_user_can('manage_automarketer_ai')) {
            wp_die('Unauthorized');
        }

        $code  = isset($_GET['code']) ? sanitize_text_field(wp_unslash($_GET['code'])) : '';
        $state = isset($_GET['state']) ? sanitize_text_field(wp_unslash($_GET['state'])) : '';
        $error = isset($_GET['error']) ? sanitize_text_field(wp_unslash($_GET['error'])) : '';

        if ($error !== '') {
            self::redirectWithError('X authorization was denied: ' . $error);
            return;
        }

        if ($code === '' || $state === '') {
            self::redirectWithError('Missing authorization code or state parameter.');
            return;
        }

        // Verify state matches
        $savedState = get_option(self::STATE_OPTION, '');
        delete_option(self::STATE_OPTION);

        if (!is_string($savedState) || $savedState === '' || !hash_equals($savedState, $state)) {
            self::redirectWithError('Invalid OAuth state. Please try connecting again.');
            return;
        }

        // Retrieve code_verifier for PKCE
        $codeVerifier = get_option(self::CODE_VERIFIER_OPTION, '');
        delete_option(self::CODE_VERIFIER_OPTION);

        if (!is_string($codeVerifier) || $codeVerifier === '') {
            self::redirectWithError('Missing PKCE code verifier. Please try connecting again.');
            return;
        }

        // Exchange authorization code for tokens
        $tokenData = self::exchangeCodeForTokens($code, $codeVerifier);
        if ($tokenData === null) {
            self::redirectWithError('Failed to exchange authorization code for tokens.');
            return;
        }

        // Fetch user profile
        $userInfo = self::fetchUserProfile($tokenData['access_token']);
        if ($userInfo === null) {
            self::redirectWithError('Failed to fetch X user profile.');
            return;
        }

        // Store connection
        $saved = self::saveConnection($tokenData, $userInfo);
        if (!$saved) {
            self::redirectWithError('Failed to save X connection.');
            return;
        }

        // Schedule token refresh cron
        self::scheduleCron();

        // Redirect to X social page with success
        wp_safe_redirect(
            admin_url('admin.php?page=automarketer-ai-social&channel=x&amai_x_connected=1')
        );
        exit;
    }

    // --------------------------------------------------
    // Public: disconnect X account
    // --------------------------------------------------
    public static function disconnect(): bool
    {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_x_connection';

        // Get current token for revocation
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT access_token_enc FROM {$table} WHERE tenant_id = %s AND status = 'active' LIMIT 1",
                self::getTenantId()
            ),
            ARRAY_A
        );

        if (is_array($row) && isset($row['access_token_enc']) && $row['access_token_enc'] !== '') {
            $accessToken = KeyVault::decrypt($row['access_token_enc']);
            if (is_string($accessToken) && $accessToken !== '') {
                self::revokeToken($accessToken);
            }
        }

        // Delete connection row
        $wpdb->delete($table, ['tenant_id' => self::getTenantId()], ['%s']);

        // Cancel scheduled posts
        $postsTable = $wpdb->prefix . 'amai_scheduled_posts';
        $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$postsTable} SET status = 'rejected', failure_reason = 'X account disconnected'
                 WHERE channel = 'x' AND status IN ('draft', 'approved') AND tenant_id = %s",
                self::getTenantId()
            )
        );

        // Unschedule cron
        self::unscheduleCron();

        return true;
    }

    // --------------------------------------------------
    // Public: get the active connection or null
    // --------------------------------------------------
    public static function getConnection(): ?array
    {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_x_connection';

        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE tenant_id = %s LIMIT 1",
                self::getTenantId()
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return null;
        }

        return $row;
    }

    // --------------------------------------------------
    // Public: check if X is connected and active
    // --------------------------------------------------
    public static function isConnected(): bool
    {
        $conn = self::getConnection();
        return $conn !== null && ($conn['status'] ?? '') === 'active';
    }

    // --------------------------------------------------
    // Public: get a valid access token, refreshing if needed
    // --------------------------------------------------
    public static function getAccessToken(): ?string
    {
        $conn = self::getConnection();
        if ($conn === null || $conn['status'] !== 'active') {
            return null;
        }

        $accessToken = KeyVault::decrypt($conn['access_token_enc']);
        if (!is_string($accessToken) || $accessToken === '') {
            return null;
        }

        // Check if token is expired or about to expire
        $expiresAt = $conn['token_expires_at'] ?? null;
        if ($expiresAt !== null) {
            $expiresTs = strtotime($expiresAt);
            if ($expiresTs !== false && $expiresTs <= (time() + self::TOKEN_REFRESH_BUFFER_SECONDS)) {
                // Token expired or expiring soon -- refresh
                $refreshed = self::refreshAccessToken();
                if ($refreshed) {
                    $conn = self::getConnection();
                    if ($conn === null) {
                        return null;
                    }
                    $accessToken = KeyVault::decrypt($conn['access_token_enc']);
                    if (!is_string($accessToken) || $accessToken === '') {
                        return null;
                    }
                }
            }
        }

        return $accessToken;
    }

    // --------------------------------------------------
    // Public: get X user ID from connection
    // --------------------------------------------------
    public static function getXUserId(): ?string
    {
        $conn = self::getConnection();
        if ($conn === null) {
            return null;
        }
        $id = $conn['x_user_id'] ?? '';
        return $id !== '' ? $id : null;
    }

    // --------------------------------------------------
    // Public: get X username from connection
    // --------------------------------------------------
    public static function getUsername(): ?string
    {
        $conn = self::getConnection();
        if ($conn === null) {
            return null;
        }
        $username = $conn['username'] ?? '';
        return $username !== '' ? $username : null;
    }

    // --------------------------------------------------
    // Public: refresh the access token using the refresh token
    // Called by cron and by getAccessToken() when token is expired
    // --------------------------------------------------
    public static function refreshAccessToken(): bool
    {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_x_connection';

        $conn = self::getConnection();
        if ($conn === null || $conn['status'] !== 'active') {
            return false;
        }

        $refreshTokenEnc = $conn['refresh_token_enc'] ?? '';
        if ($refreshTokenEnc === '') {
            return false;
        }

        $refreshToken = KeyVault::decrypt($refreshTokenEnc);
        if (!is_string($refreshToken) || $refreshToken === '') {
            self::markConnectionError('refresh_token_decrypt_failed');
            return false;
        }

        $body = [
            'grant_type'    => 'refresh_token',
            'refresh_token' => $refreshToken,
            'client_id'     => self::getClientId(),
        ];

        $response = wp_remote_post(self::TOKEN_URL, [
            'timeout' => 15,
            'headers' => [
                'Content-Type'  => 'application/x-www-form-urlencoded',
                'Authorization' => 'Basic ' . base64_encode(self::getClientId() . ':' . self::getClientSecret()),
            ],
            'body' => $body,
        ]);

        if (is_wp_error($response)) {
            return false;
        }

        $httpCode = (int) wp_remote_retrieve_response_code($response);
        $json     = json_decode(wp_remote_retrieve_body($response), true);

        if ($httpCode !== 200 || !is_array($json) || !isset($json['access_token'])) {
            // If refresh token is invalid, mark connection as expired
            if ($httpCode === 400 || $httpCode === 401) {
                self::markConnectionError('refresh_token_expired');
            }
            return false;
        }

        $newAccessToken  = (string) $json['access_token'];
        $newRefreshToken = isset($json['refresh_token']) ? (string) $json['refresh_token'] : null;
        $expiresIn       = isset($json['expires_in']) ? (int) $json['expires_in'] : 7200;

        $updateData = [
            'access_token_enc' => KeyVault::encrypt($newAccessToken),
            'token_expires_at' => gmdate('Y-m-d H:i:s', time() + $expiresIn),
            'last_verified_at' => gmdate('Y-m-d H:i:s'),
            'status'           => 'active',
        ];
        $updateFormat = ['%s', '%s', '%s', '%s'];

        // X issues a new refresh token on each refresh -- always store it
        if ($newRefreshToken !== null && $newRefreshToken !== '') {
            $updateData['refresh_token_enc'] = KeyVault::encrypt($newRefreshToken);
            $updateFormat[] = '%s';
        }

        $wpdb->update(
            $table,
            $updateData,
            ['tenant_id' => self::getTenantId()],
            $updateFormat,
            ['%s']
        );

        return true;
    }

    // --------------------------------------------------
    // Cron callback: refresh token
    // --------------------------------------------------
    public static function cronRefreshToken(): void
    {
        if (!self::isConnected()) {
            return;
        }

        self::refreshAccessToken();
    }

    // --------------------------------------------------
    // Schedule/unschedule cron
    // --------------------------------------------------
    public static function scheduleCron(): void
    {
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
        }
    }

    public static function unscheduleCron(): void
    {
        $ts = wp_next_scheduled(self::CRON_HOOK);
        if ($ts !== false) {
            wp_unschedule_event($ts, self::CRON_HOOK);
        }
    }

    // --------------------------------------------------
    // Private: exchange authorization code for tokens
    // --------------------------------------------------
    private static function exchangeCodeForTokens(string $code, string $codeVerifier): ?array
    {
        $body = [
            'grant_type'    => 'authorization_code',
            'code'          => $code,
            'redirect_uri'  => self::getCallbackUrl(),
            'code_verifier' => $codeVerifier,
            'client_id'     => self::getClientId(),
        ];

        $response = wp_remote_post(self::TOKEN_URL, [
            'timeout' => 15,
            'headers' => [
                'Content-Type'  => 'application/x-www-form-urlencoded',
                'Authorization' => 'Basic ' . base64_encode(self::getClientId() . ':' . self::getClientSecret()),
            ],
            'body' => $body,
        ]);

        if (is_wp_error($response)) {
            return null;
        }

        $httpCode = (int) wp_remote_retrieve_response_code($response);
        $json     = json_decode(wp_remote_retrieve_body($response), true);

        if ($httpCode !== 200 || !is_array($json) || !isset($json['access_token'])) {
            return null;
        }

        return [
            'access_token'  => (string) $json['access_token'],
            'refresh_token' => isset($json['refresh_token']) ? (string) $json['refresh_token'] : null,
            'expires_in'    => isset($json['expires_in']) ? (int) $json['expires_in'] : 7200,
        ];
    }

    // --------------------------------------------------
    // Private: fetch the authenticated user's profile
    // --------------------------------------------------
    private static function fetchUserProfile(string $accessToken): ?array
    {
        $url = self::USERS_ME_URL . '?user.fields=id,name,username,profile_image_url,public_metrics';

        $response = wp_remote_get($url, [
            'timeout' => 15,
            'headers' => [
                'Authorization' => 'Bearer ' . $accessToken,
            ],
        ]);

        if (is_wp_error($response)) {
            return null;
        }

        $httpCode = (int) wp_remote_retrieve_response_code($response);
        $json     = json_decode(wp_remote_retrieve_body($response), true);

        if ($httpCode !== 200 || !is_array($json) || !isset($json['data']['id'])) {
            return null;
        }

        $data = $json['data'];

        return [
            'x_user_id'        => (string) $data['id'],
            'username'          => (string) ($data['username'] ?? ''),
            'display_name'      => (string) ($data['name'] ?? ''),
            'profile_image_url' => (string) ($data['profile_image_url'] ?? ''),
            'followers_count'   => (int) ($data['public_metrics']['followers_count'] ?? 0),
        ];
    }

    // --------------------------------------------------
    // Private: save or update the connection row
    // --------------------------------------------------
    private static function saveConnection(array $tokenData, array $userInfo): bool
    {
        global $wpdb;

        $table    = $wpdb->prefix . 'amai_x_connection';
        $tenantId = self::getTenantId();

        $accessTokenEnc  = KeyVault::encrypt($tokenData['access_token']);
        $refreshTokenEnc = $tokenData['refresh_token'] !== null
            ? KeyVault::encrypt($tokenData['refresh_token'])
            : null;

        $expiresAt = gmdate('Y-m-d H:i:s', time() + ($tokenData['expires_in'] ?? 7200));

        // Check for existing row
        $existing = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM {$table} WHERE tenant_id = %s LIMIT 1",
                $tenantId
            )
        );

        $data = [
            'x_user_id'         => $userInfo['x_user_id'],
            'username'           => $userInfo['username'],
            'display_name'       => $userInfo['display_name'],
            'profile_image_url'  => $userInfo['profile_image_url'],
            'followers_count'    => $userInfo['followers_count'],
            'access_token_enc'   => $accessTokenEnc,
            'refresh_token_enc'  => $refreshTokenEnc,
            'token_expires_at'   => $expiresAt,
            'connected_at'       => gmdate('Y-m-d H:i:s'),
            'last_verified_at'   => gmdate('Y-m-d H:i:s'),
            'status'             => 'active',
        ];

        $formats = ['%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s'];

        if ($existing !== null) {
            $wpdb->update($table, $data, ['tenant_id' => $tenantId], $formats, ['%s']);
        } else {
            $data['tenant_id'] = $tenantId;
            $formats[] = '%s';
            $wpdb->insert($table, $data, $formats);
        }

        return $wpdb->last_error === '';
    }

    // --------------------------------------------------
    // Private: revoke an access token
    // --------------------------------------------------
    private static function revokeToken(string $token): void
    {
        wp_remote_post(self::REVOKE_URL, [
            'timeout' => 10,
            'headers' => [
                'Content-Type'  => 'application/x-www-form-urlencoded',
                'Authorization' => 'Basic ' . base64_encode(self::getClientId() . ':' . self::getClientSecret()),
            ],
            'body' => [
                'token'           => $token,
                'token_type_hint' => 'access_token',
            ],
        ]);
    }

    // --------------------------------------------------
    // Private: mark connection as error/expired
    // --------------------------------------------------
    private static function markConnectionError(string $reason): void
    {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_x_connection';

        $wpdb->update(
            $table,
            ['status' => 'expired'],
            ['tenant_id' => self::getTenantId()],
            ['%s'],
            ['%s']
        );
    }

    // --------------------------------------------------
    // Private: generate a PKCE code verifier
    // 43-128 chars, unreserved URI characters only
    // --------------------------------------------------
    private static function generateCodeVerifier(): string
    {
        return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
    }

    // --------------------------------------------------
    // Private: derive S256 code challenge from verifier
    // --------------------------------------------------
    private static function deriveCodeChallenge(string $verifier): string
    {
        $hash = hash('sha256', $verifier, true);
        return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
    }

    // --------------------------------------------------
    // Private: redirect with error message
    // --------------------------------------------------
    private static function redirectWithError(string $message): void
    {
        wp_safe_redirect(
            admin_url('admin.php?page=automarketer-ai-social&channel=x&amai_x_error=' . rawurlencode($message))
        );
        exit;
    }

    // --------------------------------------------------
    // Private: get callback URL
    // --------------------------------------------------
    private static function getCallbackUrl(): string
    {
        return home_url('/x/callback');
    }

    // --------------------------------------------------
    // Private: get Client ID from wp-config constant
    // --------------------------------------------------
    private static function getClientId(): string
    {
        if (!defined('AMAI_X_CLIENT_ID') || !is_string(AMAI_X_CLIENT_ID) || AMAI_X_CLIENT_ID === '') {
            return '';
        }

        return AMAI_X_CLIENT_ID;
    }

    // --------------------------------------------------
    // Private: get Client Secret from wp-config constant
    // --------------------------------------------------
    private static function getClientSecret(): string
    {
        if (!defined('AMAI_X_CLIENT_SECRET') || !is_string(AMAI_X_CLIENT_SECRET) || AMAI_X_CLIENT_SECRET === '') {
            return '';
        }

        return AMAI_X_CLIENT_SECRET;
    }

    // --------------------------------------------------
    // Private: get tenant ID
    // --------------------------------------------------
    private static function getTenantId(): string
    {
        return get_option('amai_tenant_id', 'default');
    }
}