<?php

namespace EasyLinkAutomations\Auth;

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

use EasyLinkAutomations\Auth\Contracts\OAuth2PlatformAuthInterface;
use EasyLinkAutomations\Models\Connection;

/**
 * Handles the full OAuth2 Authorization Code flow.
 *
 * - Generates authorization URLs with CSRF state tokens
 * - Exchanges authorization codes for access + refresh tokens
 * - Auto-refreshes expired tokens
 */
final class OAuth2Handler
{
    /**
     * Transient prefix for OAuth2 state tokens.
     */
    private const STATE_PREFIX = 'easylink_automations_oauth2_state_';

    /**
     * Build the authorization URL for a platform.
     *
     * Stores a state token in a WordPress transient for CSRF verification.
     *
     * @param string $platform     The platform slug
     * @param int    $connectionId The connection ID to associate
     *
     * @return string|null The full authorization URL, or null on failure
     */
    public static function getAuthorizationUrl(string $platform, int $connectionId): ?string
    {
        $platformClass = PlatformAuthRegistry::get($platform);
        if (!$platformClass || !is_subclass_of($platformClass, OAuth2PlatformAuthInterface::class)) {
            return null;
        }

        $connection = Connection::find($connectionId);
        if (!$connection) {
            return null;
        }

        $credentials  = self::withFieldDefaults($platformClass, Connection::getCredentials($connection));
        $oauth2Config = $platformClass::getOAuth2Config();

        // Generate CSRF state token
        $state = wp_generate_password(32, false);
        $stateData = [
            'connection_id' => $connectionId,
            'platform'      => $platform,
        ];
        set_transient(self::STATE_PREFIX . $state, $stateData, 600); // 10 min expiry

        // Build the authorization URL
        $params = [
            'client_id'     => $credentials['client_id'] ?? '',
            'redirect_uri'  => self::getCallbackUrl(),
            'response_type' => 'code',
            'scope'         => self::interpolateConfigValue($oauth2Config['scope'] ?? '', $credentials),
            'state'         => $state,
        ];

        // Merge any platform-specific auth params (e.g. access_type=offline for Google)
        if (!empty($oauth2Config['auth_params'])) {
            $params = array_merge($params, $oauth2Config['auth_params']);
        }

        $authUrl             = $oauth2Config['auth_url'];
        $authUrlByCredential = $oauth2Config['auth_url_by_credential'] ?? [];
        $authUrlField        = $authUrlByCredential['field'] ?? '';
        $authUrlMap          = $authUrlByCredential['urls'] ?? [];
        $authUrlFieldValue   = $credentials[$authUrlField] ?? '';

        if ($authUrlFieldValue !== '' && isset($authUrlMap[$authUrlFieldValue])) {
            $authUrl = $authUrlMap[$authUrlFieldValue];
        }

        return self::interpolateConfigValue($authUrl, $credentials) . '?' . http_build_query($params);
    }

    /**
     * Handle the OAuth2 callback.
     *
     * Verifies the state token, exchanges the authorization code for tokens,
     * encrypts and stores them, and updates the connection status.
     *
     * @param string $code  The authorization code from the provider
     * @param string $state The state token for CSRF verification
     *
     * @return array{success: bool, message: string, connection_id?: int}
     */
    public static function handleCallback(string $code, string $state, array $callbackParams = []): array
    {
        // Verify state token
        $stateData = get_transient(self::STATE_PREFIX . $state);
        if (!$stateData) {
            return ['success' => false, 'message' => __('Invalid or expired state token', 'easylink-automations')];
        }
        delete_transient(self::STATE_PREFIX . $state);

        $connectionId = $stateData['connection_id'];
        $platform     = $stateData['platform'];

        $platformClass = PlatformAuthRegistry::get($platform);
        if (!$platformClass || !is_subclass_of($platformClass, OAuth2PlatformAuthInterface::class)) {
            return ['success' => false, 'message' => __('Unknown platform', 'easylink-automations')];
        }

        $connection = Connection::find($connectionId);
        if (!$connection) {
            return ['success' => false, 'message' => __('Connection not found', 'easylink-automations')];
        }

        $credentials  = self::withFieldDefaults($platformClass, Connection::getCredentials($connection));
        $oauth2Config = $platformClass::getOAuth2Config();

        // Exchange authorization code for tokens
        $tokenParams = [
            'grant_type'    => 'authorization_code',
            'code'          => $code,
            'redirect_uri'  => self::getCallbackUrl(),
            'client_id'     => $credentials['client_id'] ?? '',
            'client_secret' => $credentials['client_secret'] ?? '',
        ];

        if (!empty($oauth2Config['token_params'])) {
            $tokenParams = array_merge($tokenParams, $oauth2Config['token_params']);
        }
        if (!empty($oauth2Config['include_scope_in_token_request'])) {
            $tokenParams['scope'] = self::interpolateConfigValue($oauth2Config['scope'] ?? '', $credentials);
        }

        $tokenUrl = self::getTokenUrl($oauth2Config, $credentials, $callbackParams);
        $tokenHeaders = ['Accept' => 'application/json'];

        if (($oauth2Config['token_client_auth'] ?? '') === 'basic') {
            $tokenHeaders['Authorization'] = 'Basic ' . base64_encode(
                ($credentials['client_id'] ?? '') . ':' . ($credentials['client_secret'] ?? '')
            );
            unset($tokenParams['client_id'], $tokenParams['client_secret']);
        }

        $response = wp_remote_post($tokenUrl, [
            'body'    => $tokenParams,
            'headers' => $tokenHeaders,
            'timeout' => 30,
        ]);

        if (is_wp_error($response)) {
            return ['success' => false, 'message' => sprintf(
                /* translators: %s: HTTP error returned by WordPress. */
                __('Token exchange failed: %s', 'easylink-automations'),
                $response->get_error_message()
            )];
        }

        $statusCode = wp_remote_retrieve_response_code($response);
        $body       = json_decode(wp_remote_retrieve_body($response), true);

        if ($statusCode < 200 || $statusCode >= 300 || empty($body['access_token'])) {
            $error = $body['error_description'] ?? $body['error'] ?? 'Unknown error';

            return ['success' => false, 'message' => sprintf(
                /* translators: %s: error message returned by the OAuth provider. */
                __('Token exchange failed: %s', 'easylink-automations'),
                $error
            )];
        }

        // Store encrypted tokens
        $tokenData = [
            'access_token'  => $body['access_token'],
            'refresh_token' => $body['refresh_token'] ?? '',
            'token_type'    => $body['token_type'] ?? 'Bearer',
            'scope'         => $body['scope'] ?? '',
            'obtained_at'   => time(),
            'expires_in'    => $body['expires_in'] ?? 0,
        ];

        // Store extra fields that some platforms return (e.g. Salesforce instance_url)
        $extraFields = ['instance_url', 'id', 'issued_at', 'api_domain'];
        foreach ($extraFields as $field) {
            if (!empty($body[$field])) {
                $tokenData[$field] = $body[$field];
            }
        }

        $tokenData['token_url'] = $tokenUrl;

        // Slack OAuth v2 returns a separate user token for user_scope.
        if (!empty($body['authed_user']['access_token'])) {
            $tokenData['user_access_token']  = $body['authed_user']['access_token'];
            $tokenData['user_refresh_token'] = $body['authed_user']['refresh_token'] ?? '';
            $tokenData['user_token_type']    = $body['authed_user']['token_type'] ?? 'user';
            $tokenData['user_scope']         = $body['authed_user']['scope'] ?? '';
            $tokenData['user_obtained_at']   = time();
            $tokenData['user_expires_in']    = $body['authed_user']['expires_in'] ?? 0;
            $tokenData['authed_user_id']     = $body['authed_user']['id'] ?? '';
        }

        $expiresAt = null;
        if (!empty($body['expires_in'])) {
            $expiresAt = gmdate('Y-m-d H:i:s', time() + (int) $body['expires_in']);
        }

        Connection::update($connectionId, [
            'token_data' => EncryptionService::encrypt($tokenData),
            'status'     => 'active',
            'expires_at' => $expiresAt,
        ]);

        return [
            'success'       => true,
            'message'       => __('Connected successfully', 'easylink-automations'),
            'connection_id' => $connectionId,
        ];
    }

    /**
     * Get a valid access token for a connection, auto-refreshing if expired.
     *
     * @param int $connectionId The connection ID
     *
     * @return string A valid access token, or empty string on failure
     */
    public static function getValidToken(int $connectionId): string
    {
        $connection = Connection::find($connectionId);
        if (!$connection) {
            return '';
        }

        $tokenData = Connection::getTokenData($connection);
        if (empty($tokenData['access_token'])) {
            return '';
        }

        // Check if token is expired (with 60-second buffer)
        $expiresIn  = (int) ($tokenData['expires_in'] ?? 0);
        $obtainedAt = (int) ($tokenData['obtained_at'] ?? 0);

        if ($expiresIn > 0 && $obtainedAt > 0) {
            $expiresAt = $obtainedAt + $expiresIn;
            if (time() >= ($expiresAt - 60)) {
                // Token is expired or about to expire — try to refresh
                $refreshed = self::refreshToken($connectionId);
                if ($refreshed) {
                    return $refreshed['access_token'];
                }

                // Refresh failed, return existing token (might still work briefly)
                return $tokenData['access_token'];
            }
        }

        return $tokenData['access_token'];
    }

    /**
     * Get a valid Slack-style nested user token, refreshing it when needed.
     */
    public static function getValidUserToken(int $connectionId): string
    {
        $connection = Connection::find($connectionId);
        if (!$connection) {
            return '';
        }

        $tokenData = Connection::getTokenData($connection);
        if (empty($tokenData['user_access_token'])) {
            return '';
        }

        $expiresIn  = (int) ($tokenData['user_expires_in'] ?? 0);
        $obtainedAt = (int) ($tokenData['user_obtained_at'] ?? 0);

        if ($expiresIn <= 0 || $obtainedAt <= 0 || time() < ($obtainedAt + $expiresIn - 60)) {
            return $tokenData['user_access_token'];
        }

        if (empty($tokenData['user_refresh_token'])) {
            return $tokenData['user_access_token'];
        }

        $platform      = $connection['platform'] ?? '';
        $platformClass = PlatformAuthRegistry::get($platform);
        if (!$platformClass || !is_subclass_of($platformClass, OAuth2PlatformAuthInterface::class)) {
            return $tokenData['user_access_token'];
        }

        $credentials  = self::withFieldDefaults($platformClass, Connection::getCredentials($connection));
        $oauth2Config = $platformClass::getOAuth2Config();
        $tokenUrl     = $tokenData['token_url'] ?? self::getTokenUrl($oauth2Config, $credentials);
        $refreshBody  = [
            'grant_type'    => 'refresh_token',
            'refresh_token' => $tokenData['user_refresh_token'],
            'client_id'     => $credentials['client_id'] ?? '',
            'client_secret' => $credentials['client_secret'] ?? '',
        ];

        $response = wp_remote_post($tokenUrl, [
            'body'    => $refreshBody,
            'headers' => ['Accept' => 'application/json'],
            'timeout' => 30,
        ]);

        if (is_wp_error($response)) {
            return $tokenData['user_access_token'];
        }

        $statusCode = wp_remote_retrieve_response_code($response);
        $body       = json_decode(wp_remote_retrieve_body($response), true);
        if ($statusCode < 200 || $statusCode >= 300 || empty($body['access_token'])) {
            return $tokenData['user_access_token'];
        }

        $tokenData['user_access_token']  = $body['access_token'];
        $tokenData['user_refresh_token'] = $body['refresh_token'] ?? $tokenData['user_refresh_token'];
        $tokenData['user_token_type']    = $body['token_type'] ?? $tokenData['user_token_type'] ?? 'user';
        $tokenData['user_scope']         = $body['scope'] ?? $tokenData['user_scope'] ?? '';
        $tokenData['user_obtained_at']   = time();
        $tokenData['user_expires_in']    = $body['expires_in'] ?? $tokenData['user_expires_in'] ?? 0;

        Connection::update($connectionId, [
            'token_data' => EncryptionService::encrypt($tokenData),
            'status'     => 'active',
        ]);

        return $tokenData['user_access_token'];
    }

    /**
     * Refresh an OAuth2 access token using the refresh token.
     *
     * @param int $connectionId The connection ID
     *
     * @return array|null Updated token data on success, null on failure
     */
    public static function refreshToken(int $connectionId): ?array
    {
        $connection = Connection::find($connectionId);
        if (!$connection) {
            return null;
        }

        $tokenData = Connection::getTokenData($connection);
        if (empty($tokenData['refresh_token'])) {
            return null;
        }

        $platform      = $connection['platform'] ?? '';
        $platformClass = PlatformAuthRegistry::get($platform);
        if (!$platformClass || !is_subclass_of($platformClass, OAuth2PlatformAuthInterface::class)) {
            return null;
        }

        $credentials  = self::withFieldDefaults($platformClass, Connection::getCredentials($connection));
        $oauth2Config = $platformClass::getOAuth2Config();

        $tokenUrl = $tokenData['token_url'] ?? self::getTokenUrl($oauth2Config, $credentials);

        $refreshBody = [
            'grant_type'    => 'refresh_token',
            'refresh_token' => $tokenData['refresh_token'],
            'client_id'     => $credentials['client_id'] ?? '',
            'client_secret' => $credentials['client_secret'] ?? '',
        ];
        $refreshHeaders = ['Accept' => 'application/json'];

        if (!empty($oauth2Config['include_scope_in_token_request'])) {
            $refreshBody['scope'] = self::interpolateConfigValue($oauth2Config['scope'] ?? '', $credentials);
        }

        if (($oauth2Config['refresh_client_auth'] ?? '') === 'basic') {
            $refreshHeaders['Authorization'] = 'Basic ' . base64_encode(
                ($credentials['client_id'] ?? '') . ':' . ($credentials['client_secret'] ?? '')
            );
            unset($refreshBody['client_id'], $refreshBody['client_secret']);
        }

        $response = wp_remote_post($tokenUrl, [
            'body'    => $refreshBody,
            'headers' => $refreshHeaders,
            'timeout' => 30,
        ]);

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

        $body = json_decode(wp_remote_retrieve_body($response), true);
        if (empty($body['access_token'])) {
            // Token refresh failed — mark connection as error
            Connection::update($connectionId, ['status' => 'error']);

            return null;
        }

        // Update stored token data
        $newTokenData = [
            'access_token'  => $body['access_token'],
            'refresh_token' => $body['refresh_token'] ?? $tokenData['refresh_token'], // Keep old if not returned
            'token_type'    => $body['token_type'] ?? $tokenData['token_type'] ?? 'Bearer',
            'scope'         => $body['scope'] ?? $tokenData['scope'] ?? '',
            'obtained_at'   => time(),
            'expires_in'    => $body['expires_in'] ?? $tokenData['expires_in'] ?? 0,
            'token_url'     => $tokenUrl,
        ];

        // Preserve provider-specific routing/identity metadata when refresh
        // responses omit fields returned during the initial token exchange.
        $extraFields = [
            'instance_url',
            'id',
            'issued_at',
            'api_domain',
            'user_access_token',
            'user_refresh_token',
            'user_token_type',
            'user_scope',
            'user_obtained_at',
            'user_expires_in',
            'authed_user_id',
        ];
        foreach ($extraFields as $field) {
            if (!empty($body[$field]) || !empty($tokenData[$field])) {
                $newTokenData[$field] = $body[$field] ?? $tokenData[$field];
            }
        }

        $expiresAt = null;
        if (!empty($body['expires_in'])) {
            $expiresAt = gmdate('Y-m-d H:i:s', time() + (int) $body['expires_in']);
        }

        Connection::update($connectionId, [
            'token_data' => EncryptionService::encrypt($newTokenData),
            'status'     => 'active',
            'expires_at' => $expiresAt,
        ]);

        return $newTokenData;
    }

    /**
     * Resolve the token URL, including providers that return a regional token host.
     */
    private static function getTokenUrl(array $oauth2Config, array $credentials, array $callbackParams = []): string
    {
        $tokenUrl = self::interpolateConfigValue($oauth2Config['token_url'], $credentials);

        $callbackParam = $oauth2Config['token_url_from_callback_param'] ?? '';
        if ($callbackParam && !empty($callbackParams[$callbackParam])) {
            $tokenUrl = rtrim(esc_url_raw((string) $callbackParams[$callbackParam]), '/')
                . ($oauth2Config['token_url_callback_path'] ?? '/oauth/v2/token');
        }

        return $tokenUrl;
    }

    /**
     * Replace simple {credential_key} placeholders in OAuth config values.
     */
    private static function interpolateConfigValue(string $value, array $credentials): string
    {
        foreach ($credentials as $key => $credentialValue) {
            if (is_scalar($credentialValue)) {
                $value = str_replace(
                    '{' . $key . '}/',
                    rtrim((string) $credentialValue, '/') . '/',
                    $value
                );
                $value = str_replace('{' . $key . '}', (string) $credentialValue, $value);
            }
        }

        return $value;
    }

    /**
     * Apply connector field defaults to credentials saved before a field existed.
     *
     * @param class-string<OAuth2PlatformAuthInterface> $platformClass
     */
    private static function withFieldDefaults(string $platformClass, array $credentials): array
    {
        foreach ($platformClass::getFields() as $key => $field) {
            if (
                (!array_key_exists($key, $credentials) || $credentials[$key] === '')
                && array_key_exists('default', $field)
            ) {
                $credentials[$key] = $field['default'];
            }
        }

        return $credentials;
    }

    /**
     * Get the OAuth2 callback URL.
     */
    public static function getCallbackUrl(): string
    {
        return rest_url('easylink-automations/v1/auth/callback');
    }
}
