<?php
/**
 * Teleship OAuth Manager
 *
 * Handles OAuth authentication with Teleship API including:
 * - Client ID/Secret storage (encrypted)
 * - Bearer token exchange
 * - Automatic token refresh
 * - Connection status tracking
 *
 * @package Teleship
 * @since 2.0.0
 */

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

/**
 * Class Teleship_OAuth
 *
 * OAuth helper for Teleship Integration - mirrors Magento Helper/OAuth.php
 */
class Teleship_OAuth {

    /**
     * Option keys for OAuth data
     */
    const OPTION_CLIENT_ID = 'teleship_client_id';
    const OPTION_CLIENT_SECRET = 'teleship_client_secret';
    const OPTION_ACCOUNT_ID = 'teleship_account_id';
    const OPTION_ACCESS_TOKEN = 'teleship_access_token';
    const OPTION_REFRESH_TOKEN = 'teleship_refresh_token';
    const OPTION_TOKEN_EXPIRES_AT = 'teleship_token_expires_at';
    const OPTION_BEARER_TOKEN = 'teleship_bearer_token';
    const OPTION_BEARER_TOKEN_EXPIRES_AT = 'teleship_bearer_token_expires_at';
    const OPTION_CONNECTED_AT = 'teleship_connected_at';
    const OPTION_WEBHOOK_ID = 'teleship_webhook_id';
    const OPTION_WEBHOOK_SECRET = 'teleship_webhook_secret';

    /**
     * Buffer time before expiration to trigger refresh (5 minutes)
     */
    const REFRESH_BUFFER_SECONDS = 300;

    /**
     * Flag to prevent recursive refresh attempts
     *
     * @var bool
     */
    private static $is_refreshing = false;

    /**
     * Singleton instance
     *
     * @var Teleship_OAuth
     */
    private static $instance = null;

    /**
     * Get singleton instance
     *
     * @return Teleship_OAuth
     */
    public static function instance() {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor
     */
    private function __construct() {
        // Private constructor for singleton
    }

    /**
     * Check if store is connected to Teleship
     *
     * @return bool
     */
    public function is_connected() {
        $client_id = $this->get_client_id();
        $client_secret = $this->get_client_secret();
        $account_id = $this->get_account_id();

        return !empty($client_id) && !empty($client_secret) && !empty($account_id);
    }

    /**
     * Get client ID (decrypted)
     *
     * @return string|null
     */
    public function get_client_id() {
        $encrypted = get_option(self::OPTION_CLIENT_ID);
        if (empty($encrypted)) {
            return null;
        }
        return $this->decrypt($encrypted);
    }

    /**
     * Get client secret (decrypted)
     *
     * @return string|null
     */
    public function get_client_secret() {
        $encrypted = get_option(self::OPTION_CLIENT_SECRET);
        if (empty($encrypted)) {
            return null;
        }
        return $this->decrypt($encrypted);
    }

    /**
     * Get Teleship account ID
     *
     * @return string|null
     */
    public function get_account_id() {
        return get_option(self::OPTION_ACCOUNT_ID);
    }

    /**
     * Get access token (with auto-refresh if needed)
     *
     * @param bool $auto_refresh Whether to auto-refresh if expired.
     * @return string|null
     */
    public function get_access_token($auto_refresh = true) {
        $encrypted = get_option(self::OPTION_ACCESS_TOKEN);
        if (empty($encrypted)) {
            return null;
        }

        // Check if token needs refresh
        if ($auto_refresh && !self::$is_refreshing && $this->should_refresh_token()) {
            teleship_log('[OAuth] Access token needs refresh, attempting auto-refresh');
            if ($this->refresh_access_token()) {
                // Re-read the newly saved token
                $encrypted = get_option(self::OPTION_ACCESS_TOKEN);
            }
        }

        return $this->decrypt($encrypted);
    }

    /**
     * Get refresh token (decrypted)
     *
     * @return string|null
     */
    public function get_refresh_token() {
        $encrypted = get_option(self::OPTION_REFRESH_TOKEN);
        if (empty($encrypted)) {
            return null;
        }
        return $this->decrypt($encrypted);
    }

    /**
     * Get bearer token for API calls (exchanges clientId + clientSecret for JWT)
     *
     * This is the primary method to get a token for API calls.
     * It caches the bearer token and auto-refreshes when expired.
     *
     * @return string|null Bearer token (JWT)
     */
    public function get_bearer_token() {
        // Check if we have a cached bearer token that's still valid
        $cached_token = $this->get_cached_bearer_token();
        if ($cached_token && !$this->is_bearer_token_expired()) {
            return $cached_token;
        }

        // No cached token or expired - fetch a new one
        teleship_log('[OAuth] Bearer token expired or missing, fetching new token');

        $client_id = $this->get_client_id();
        $client_secret = $this->get_client_secret();

        if (empty($client_id) || empty($client_secret)) {
            teleship_log('[OAuth] Missing client credentials for bearer token');
            return null;
        }

        // Get token URL
        $token_url = teleship_get_api_url() . '/oauth/token';

        teleship_log('[OAuth] Fetching bearer token from: ' . $token_url);

        $response = wp_remote_post($token_url, array(
            'timeout' => 30,
            'headers' => array(
                'Content-Type' => 'application/json',
            ),
            'body' => wp_json_encode(array(
                'clientId' => $client_id,
                'clientSecret' => $client_secret,
            )),
        ));

        if (is_wp_error($response)) {
            teleship_log('[OAuth] Bearer token fetch failed: ' . $response->get_error_message());
            return null;
        }

        $http_code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);

        if ($http_code !== 200) {
            teleship_log('[OAuth] Bearer token fetch failed with HTTP ' . $http_code . ': ' . $body);
            return null;
        }

        $token_data = json_decode($body, true);

        if (!isset($token_data['accessToken'])) {
            teleship_log('[OAuth] Invalid token response - no accessToken');
            return null;
        }

        // Cache the bearer token
        update_option(self::OPTION_BEARER_TOKEN, $this->encrypt($token_data['accessToken']));

        // Save expiration time (convert milliseconds to seconds if needed)
        if (isset($token_data['expirationTime'])) {
            $expires_at = is_numeric($token_data['expirationTime']) && $token_data['expirationTime'] > time() * 10
                ? gmdate('Y-m-d H:i:s', intval($token_data['expirationTime'] / 1000))
                : gmdate('Y-m-d H:i:s', intval($token_data['expirationTime']));
            update_option(self::OPTION_BEARER_TOKEN_EXPIRES_AT, $expires_at);
        }

        teleship_log('[OAuth] Bearer token fetched successfully');

        return $token_data['accessToken'];
    }

    /**
     * Get cached bearer token (without auto-refresh)
     *
     * @return string|null
     */
    private function get_cached_bearer_token() {
        $encrypted = get_option(self::OPTION_BEARER_TOKEN);
        if (empty($encrypted)) {
            return null;
        }
        return $this->decrypt($encrypted);
    }

    /**
     * Check if bearer token is expired or expiring soon
     *
     * @return bool
     */
    public function is_bearer_token_expired() {
        $expires_at = get_option(self::OPTION_BEARER_TOKEN_EXPIRES_AT);

        if (empty($expires_at)) {
            // No expiration set, assume expired to force refresh
            return true;
        }

        $expiration_time = strtotime($expires_at);
        $current_time = time();

        // Check if expired or will expire within buffer time (5 minutes)
        return ($expiration_time - self::REFRESH_BUFFER_SECONDS) < $current_time;
    }

    /**
     * Check if access token should be refreshed
     *
     * @return bool
     */
    public function should_refresh_token() {
        // Only refresh if we have a refresh token
        if (empty($this->get_refresh_token())) {
            return false;
        }

        $expires_at = get_option(self::OPTION_TOKEN_EXPIRES_AT);

        if (empty($expires_at)) {
            return false;
        }

        $expiration_time = strtotime($expires_at);
        $current_time = time();

        // Refresh if expired or will expire within buffer time
        return ($expiration_time - self::REFRESH_BUFFER_SECONDS) < $current_time;
    }

    /**
     * Refresh the access token using the refresh token
     *
     * @return bool True if refresh was successful
     */
    public function refresh_access_token() {
        // Prevent recursive refresh attempts
        if (self::$is_refreshing) {
            return false;
        }

        self::$is_refreshing = true;

        try {
            $refresh_token = $this->get_refresh_token();

            if (empty($refresh_token)) {
                teleship_log('[OAuth] No refresh token available for auto-refresh');
                return false;
            }

            $refresh_url = teleship_get_api_url() . '/oauth/refresh';

            teleship_log('[OAuth] Attempting token refresh');

            $response = wp_remote_post($refresh_url, array(
                'timeout' => 30,
                'headers' => array(
                    'Content-Type' => 'application/json',
                ),
                'body' => wp_json_encode(array(
                    'refreshToken' => $refresh_token,
                )),
            ));

            if (is_wp_error($response)) {
                teleship_log('[OAuth] Token refresh failed: ' . $response->get_error_message());
                return false;
            }

            $http_code = wp_remote_retrieve_response_code($response);
            $body = wp_remote_retrieve_body($response);

            if ($http_code !== 200) {
                teleship_log('[OAuth] Token refresh failed with HTTP ' . $http_code);
                return false;
            }

            $token_data = json_decode($body, true);

            if (!isset($token_data['accessToken'])) {
                teleship_log('[OAuth] Invalid refresh response - no access token');
                return false;
            }

            // Save new tokens
            update_option(self::OPTION_ACCESS_TOKEN, $this->encrypt($token_data['accessToken']));

            if (isset($token_data['refreshToken'])) {
                update_option(self::OPTION_REFRESH_TOKEN, $this->encrypt($token_data['refreshToken']));
            }

            if (isset($token_data['expirationTime'])) {
                $expires_at = gmdate('Y-m-d H:i:s', intval($token_data['expirationTime'] / 1000));
                update_option(self::OPTION_TOKEN_EXPIRES_AT, $expires_at);
            }

            teleship_log('[OAuth] Token refresh successful');

            return true;

        } finally {
            self::$is_refreshing = false;
        }
    }

    /**
     * Save OAuth connection data
     *
     * @param array $data Connection data from Teleship.
     * @return bool
     */
    public function save_connection($data) {
        if (empty($data['clientId']) || empty($data['clientSecret']) || empty($data['accountId'])) {
            teleship_log('[OAuth] Missing required connection data');
            return false;
        }

        update_option(self::OPTION_CLIENT_ID, $this->encrypt($data['clientId']));
        update_option(self::OPTION_CLIENT_SECRET, $this->encrypt($data['clientSecret']));
        update_option(self::OPTION_ACCOUNT_ID, $data['accountId']);
        update_option(self::OPTION_CONNECTED_AT, current_time('mysql'));

        if (isset($data['accessToken'])) {
            update_option(self::OPTION_ACCESS_TOKEN, $this->encrypt($data['accessToken']));
        }

        if (isset($data['refreshToken'])) {
            update_option(self::OPTION_REFRESH_TOKEN, $this->encrypt($data['refreshToken']));
        }

        if (isset($data['expirationTime'])) {
            $expires_at = gmdate('Y-m-d H:i:s', intval($data['expirationTime'] / 1000));
            update_option(self::OPTION_TOKEN_EXPIRES_AT, $expires_at);
        }

        teleship_log('[OAuth] Connection saved successfully');

        return true;
    }

    /**
     * Clear all OAuth data (disconnect)
     *
     * @return void
     */
    public function disconnect() {
        delete_option(self::OPTION_CLIENT_ID);
        delete_option(self::OPTION_CLIENT_SECRET);
        delete_option(self::OPTION_ACCOUNT_ID);
        delete_option(self::OPTION_ACCESS_TOKEN);
        delete_option(self::OPTION_REFRESH_TOKEN);
        delete_option(self::OPTION_TOKEN_EXPIRES_AT);
        delete_option(self::OPTION_BEARER_TOKEN);
        delete_option(self::OPTION_BEARER_TOKEN_EXPIRES_AT);
        delete_option(self::OPTION_CONNECTED_AT);
        delete_option(self::OPTION_WEBHOOK_ID);
        delete_option(self::OPTION_WEBHOOK_SECRET);

        // Clear session validation cache
        delete_transient('teleship_session_valid');

        teleship_log('[OAuth] Disconnected and cleared all OAuth data');
    }

    /**
     * Get connection timestamp
     *
     * @return string|null
     */
    public function get_connected_at() {
        return get_option(self::OPTION_CONNECTED_AT);
    }

    /**
     * Get token expiration timestamp
     *
     * @return string|null
     */
    public function get_token_expires_at() {
        return get_option(self::OPTION_TOKEN_EXPIRES_AT);
    }

    /**
     * Check if token is expired
     *
     * @return bool
     */
    public function is_token_expired() {
        $expires_at = $this->get_token_expires_at();

        if (empty($expires_at)) {
            return false;
        }

        return strtotime($expires_at) < time();
    }

    /**
     * Get number of days until token expires
     *
     * @return int|null
     */
    public function get_days_until_expiration() {
        $expires_at = $this->get_token_expires_at();

        if (empty($expires_at)) {
            return null;
        }

        $expiration_time = strtotime($expires_at);
        $current_time = time();
        $seconds_remaining = $expiration_time - $current_time;

        if ($seconds_remaining < 0) {
            return 0; // Already expired
        }

        return (int) ceil($seconds_remaining / 86400);
    }

    /**
     * Check if token will expire soon
     *
     * @param int $days Number of days threshold.
     * @return bool
     */
    public function is_token_expiring_soon($days = 7) {
        $days_until_expiration = $this->get_days_until_expiration();

        if (null === $days_until_expiration) {
            return false;
        }

        return $days_until_expiration <= $days && $days_until_expiration > 0;
    }

    /**
     * Get Authorization header value for API requests
     *
     * @return string|null
     */
    public function get_authorization_header() {
        $token = $this->get_bearer_token();

        if (empty($token)) {
            return null;
        }

        return 'Bearer ' . $token;
    }

    /**
     * Validate connection and return error message if not valid
     *
     * @return string|null Error message, or null if connection is valid
     */
    public function validate_connection() {
        if (!$this->is_connected()) {
            return __('Not connected to Teleship. Please connect in the Teleship settings.', 'teleship-global-shipping');
        }

        // Check if token is expired AND we don't have a refresh token
        if ($this->is_token_expired() && empty($this->get_refresh_token())) {
            return __('Teleship connection has expired. Please reconnect in the settings.', 'teleship-global-shipping');
        }

        // If we have a refresh token, the connection is effectively permanent
        if (!empty($this->get_refresh_token())) {
            return null;
        }

        // Legacy check for tokens without refresh capability
        if ($this->is_token_expiring_soon(7)) {
            $days_remaining = $this->get_days_until_expiration();
            return sprintf(
                /* translators: %d is the number of days until expiration */
                __('Teleship connection will expire in %d day(s). Please reconnect soon.', 'teleship-global-shipping'),
                $days_remaining
            );
        }

        return null;
    }

    /**
     * Get connection status as array for display
     *
     * @return array
     */
    public function get_connection_status() {
        return array(
            'is_connected'          => $this->is_connected(),
            'is_expired'            => $this->is_token_expired(),
            'is_expiring_soon'      => $this->is_token_expiring_soon(7),
            'has_refresh_token'     => !empty($this->get_refresh_token()),
            'can_auto_refresh'      => !empty($this->get_refresh_token()),
            'account_id'            => $this->get_account_id(),
            'connected_at'          => $this->get_connected_at(),
            'expires_at'            => $this->get_token_expires_at(),
            'days_until_expiration' => $this->get_days_until_expiration(),
            'validation_message'    => $this->validate_connection(),
        );
    }

    /**
     * Encrypt a value for secure storage
     *
     * @param string $value Value to encrypt.
     * @return string Encrypted value
     */
    private function encrypt($value) {
        if (empty($value)) {
            return '';
        }

        // Use WordPress auth key as encryption key
        $key = defined('AUTH_KEY') ? AUTH_KEY : 'teleship-default-key';

        // Simple encryption using openssl if available
        if (function_exists('openssl_encrypt')) {
            $iv = substr(hash('sha256', $key), 0, 16);
            $encrypted = openssl_encrypt($value, 'AES-256-CBC', $key, 0, $iv);
            return base64_encode($encrypted);
        }

        // Fallback: base64 encode (not secure, but better than plain text)
        return base64_encode($value);
    }

    /**
     * Decrypt a stored value
     *
     * @param string $encrypted Encrypted value.
     * @return string Decrypted value
     */
    private function decrypt($encrypted) {
        if (empty($encrypted)) {
            return '';
        }

        $key = defined('AUTH_KEY') ? AUTH_KEY : 'teleship-default-key';

        // Decrypt using openssl if available
        if (function_exists('openssl_decrypt')) {
            $iv = substr(hash('sha256', $key), 0, 16);
            $decrypted = openssl_decrypt(base64_decode($encrypted), 'AES-256-CBC', $key, 0, $iv);
            return $decrypted !== false ? $decrypted : '';
        }

        // Fallback: base64 decode
        return base64_decode($encrypted);
    }

    /**
     * Save webhook data
     *
     * @param string $webhook_id Webhook ID from Teleship.
     * @param string $webhook_secret Webhook secret for signature verification.
     * @return void
     */
    public function save_webhook_data($webhook_id, $webhook_secret) {
        update_option(self::OPTION_WEBHOOK_ID, $webhook_id);
        update_option(self::OPTION_WEBHOOK_SECRET, $webhook_secret);
    }

    /**
     * Get webhook ID
     *
     * @return string|null
     */
    public function get_webhook_id() {
        return get_option(self::OPTION_WEBHOOK_ID);
    }

    /**
     * Get webhook secret
     *
     * @return string|null
     */
    public function get_webhook_secret() {
        return get_option(self::OPTION_WEBHOOK_SECRET);
    }
}

/**
 * Get Teleship OAuth instance
 *
 * @return Teleship_OAuth
 */
function teleship_oauth() {
    return Teleship_OAuth::instance();
}
