<?php

/**
 * MassPost Authorization Manager.
 *
 * Provides a session-based authorization workflow for the MassPost plugin.
 *
 * @package masspost
 */

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

if (! defined('MASSPOST_LICENSE_OPTION')) {
    define('MASSPOST_LICENSE_OPTION', 'masspost_license_data');
}

/**
 * Handles MassPost authorization state.
 */
class MassPost_License
{
    const META_LAST_REFRESH     = 'last_verified';
    const DEFAULT_REFRESH_TTL   = HOUR_IN_SECONDS;
    const SESSION_GRACE_PERIOD  = 120; // seconds.

    /**
     * Singleton instance.
     *
     * @var self|null
     */
    private static $instance = null;

    /**
     * REST API base URL.
     *
     * @var string
     */
    private $api_base;

    private function __construct()
    {
        $this->api_base = apply_filters(
            'masspost_authorization_api_base',
            'https://masspost.net/wp-json/masspost-auz/v1'
        );

        add_action('rest_api_init', [$this, 'register_rest_routes']);
    }

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

    /**
     * Register REST API endpoints used for remote authorization management.
     *
     * @return void
     */
    public function register_rest_routes(): void
    {
        register_rest_route(
            'masspost/v1',
            '/authorization/revoke',
            [
                'methods'             => \WP_REST_Server::CREATABLE,
                'callback'            => [$this, 'rest_handle_remote_revoke'],
                'permission_callback' => '__return_true',
            ]
        );
    }

    /**
     * Handle remote revocation requests initiated from the authorization server.
     *
     * @param WP_REST_Request $request Incoming REST request.
     * @return WP_REST_Response
     */
    public function rest_handle_remote_revoke(WP_REST_Request $request)
    {
        $raw_payload = $request->get_json_params();
        if (! is_array($raw_payload)) {
            $raw_payload = [];
        }

        // Ensure expected fields are available even if sent as form data.
        foreach (['site_identifier', 'auz_token', 'timestamp', 'status', 'signature'] as $field) {
            if (array_key_exists($field, $raw_payload)) {
                continue;
            }

            $value = $request->get_param($field);
            if (is_scalar($value)) {
                $raw_payload[$field] = $value;
            }
        }

        $signature = isset($raw_payload['signature']) && is_scalar($raw_payload['signature'])
            ? (string) $raw_payload['signature']
            : '';

        if ('' === $signature) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'code'    => 'masspost_missing_signature',
                    'message' => __('Missing signature.', 'masspost'),
                ],
                400
            );
        }

        $signature_payload = [];
        foreach ($raw_payload as $key => $value) {
            if ('signature' === $key || ! is_scalar($value)) {
                continue;
            }

            $signature_payload[$key] = (string) $value;
        }

        $site_identifier = isset($signature_payload['site_identifier']) ? sanitize_text_field($signature_payload['site_identifier']) : '';
        $auz_token       = isset($signature_payload['auz_token']) ? sanitize_text_field($signature_payload['auz_token']) : '';
        $timestamp       = isset($signature_payload['timestamp']) ? (int) $signature_payload['timestamp'] : 0;
        $status_flag     = isset($signature_payload['status']) ? sanitize_key($signature_payload['status']) : '';

        if ('' === $site_identifier || '' === $auz_token) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'code'    => 'masspost_missing_fields',
                    'message' => __('Authorization details are incomplete.', 'masspost'),
                ],
                400
            );
        }

        if ($timestamp && abs(time() - $timestamp) > DAY_IN_SECONDS) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'code'    => 'masspost_request_expired',
                    'message' => __('Authorization request expired. Please try again.', 'masspost'),
                ],
                400
            );
        }

        $license = $this->get_license();

        $secret = '';
        if (isset($license['authorization']['auz_secret']) && '' !== $license['authorization']['auz_secret']) {
            $secret = (string) $license['authorization']['auz_secret'];
        } elseif (isset($license['site_identifier']) && '' !== $license['site_identifier']) {
            $secret = (string) $license['site_identifier'];
        } else {
            $secret = $site_identifier;
        }

        if (! $this->verify_request_signature($signature_payload, $secret, $signature)) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'code'    => 'masspost_invalid_signature',
                    'message' => __('Invalid signature.', 'masspost'),
                ],
                401
            );
        }

        if (isset($license['site_identifier']) && $license['site_identifier'] && ! hash_equals((string) $license['site_identifier'], $site_identifier)) {
            return new WP_REST_Response(
                [
                    'success' => true,
                    'status'  => 'ignored',
                    'message' => __('Authorization payload ignored: site identifier mismatch.', 'masspost'),
                ],
                200
            );
        }

        $now           = current_time('timestamp', true);
        if (! $now) {
            $now = time();
        }
        $status_value  = ('disabled' === $status_flag) ? 'disabled' : 'revoked';
        $status_message = ('disabled' === $status_flag)
            ? __('Access to MassPost was disabled by support. Please contact support to restore access.', 'masspost')
            : __('Authorization was revoked. Please reconnect your account to resume service.', 'masspost');

        unset($license['authorization'], $license['session']);
        $license['status']           = $status_value;
        $license['remote_status']    = $status_value;
        $license['remote_updated']   = $now;
        $license['site_identifier']  = $license['site_identifier'] ?? $site_identifier;
        $license['last_error']       = [
            'code'    => ('disabled' === $status_flag) ? 'masspost_remote_disabled' : 'masspost_remote_revoked',
            'message' => $status_message,
        ];

        $this->update_license($license);

        /**
         * Fires when authorization is revoked remotely from the MassPost authorization server.
         *
         * @param array $license           Updated license payload.
         * @param array $signature_payload Raw request payload without the signature.
         */
        do_action('masspost_authorization_revoked_remotely', $license, $signature_payload);

        return new WP_REST_Response(
            [
                'success'   => true,
                'status'    => $status_value,
                'message'   => $status_message,
                'updatedAt' => $now,
            ],
            200
        );
    }

    /**
     * Return current license data.
     *
     * @return array
     */
    public function get_license(): array
    {
        $raw = get_option(MASSPOST_LICENSE_OPTION, []);
        return is_array($raw) ? $raw : [];
    }

    /**
     * Persist license data.
     *
     * @param array $data License payload.
     * @return void
     */
    public function update_license(array $data): void
    {
        update_option(MASSPOST_LICENSE_OPTION, $data);
    }

    /**
     * Remove stored license.
     */
    public function clear_license(): void
    {
        delete_option(MASSPOST_LICENSE_OPTION);
    }

    /**
     * Check whether the plugin has a valid authorization.
     *
     * @return bool
     */
    public function is_authorized(): bool
    {
        $license = $this->get_license();
        return isset($license['status']) && 'authorized' === $license['status']
            && ! empty($license['authorization']['auz_token'])
            && ! empty($license['authorization']['auz_secret']);
    }

    /**
     * Determine whether a session is pending finalization.
     *
     * @return bool
     */
    public function has_pending_session(): bool
    {
        $license = $this->get_license();
        if (empty($license['session']['id']) || ! isset($license['status'])) {
            return false;
        }

        if ('pending' !== $license['status']) {
            return false;
        }

        $expires_at = isset($license['session']['expires_at']) ? (int) $license['session']['expires_at'] : 0;
        if ($expires_at && ($expires_at + self::SESSION_GRACE_PERIOD) < time()) {
            return false;
        }

        return true;
    }

    /**
     * Start authorization by requesting a session from the MassPost authorization server.
     *
     * @param string $email Account email.
     * @return array|WP_Error
     */
    public function start_authorization(string $email)
    {
        $email = sanitize_email($email);
        if ('' === $email) {
            return new WP_Error(
                'masspost_auth_invalid_email',
                __('Please enter a valid email address.', 'masspost')
            );
        }

        $site_identifier = $this->compute_site_identifier();
        $payload         = [
            'email'           => $email,
            'site_url'        => home_url('/'),
            'site_name'       => get_bloginfo('name'),
            'callback_url'    => $this->get_callback_url(),
            'site_identifier' => $site_identifier,
            'admin_email'     => get_bloginfo('admin_email'),
            'plugin_version'  => defined('MASSPOST_VERSION') ? MASSPOST_VERSION : '1.0.0',
            'wp_version'      => get_bloginfo('version'),
        ];

        $response = $this->remote_post('/authorization/session/start', $payload);
        if (is_wp_error($response)) {
            $this->record_error($response);
            return $response;
        }

        if (empty($response['session_id']) || empty($response['authorize_url'])) {
            $err = new WP_Error(
                'masspost_auth_invalid_response',
                __('Unexpected response from authorization server.', 'masspost')
            );
            $this->record_error($err);
            return $err;
        }

        $license = $this->get_license();
        $license = array_merge(
            $license,
            [
                'status'          => 'pending',
                'email'           => $email,
                'site_identifier' => $site_identifier,
                'session'         => [
                    'id'            => $response['session_id'],
                    'authorize_url' => $response['authorize_url'],
                    'expires_at'    => isset($response['expires_at']) ? (int) $response['expires_at'] : (time() + (int) ($response['expires_in'] ?? 900)),
                    'started_at'    => time(),
                ],
                'last_error'      => null,
            ]
        );

        $this->update_license($license);

        return $license['session'];
    }

    /**
     * Finalize authorization after the user approves on masspost.net.
     *
     * @param string $session_id Session identifier.
     * @param string $auz_token  Authorization token.
     * @param string $request_id Optional request identifier.
     * @return array|WP_Error
     */
    public function finalize_authorization(string $session_id, string $auz_token, string $request_id = '')
    {
        $session_id = sanitize_text_field($session_id);
        $auz_token  = sanitize_text_field($auz_token);

        if ('' === $session_id || '' === $auz_token) {
            return new WP_Error(
                'masspost_auth_missing',
                __('Authorization data is incomplete.', 'masspost')
            );
        }

        $license = $this->get_license();
        if (empty($license['site_identifier'])) {
            return new WP_Error(
                'masspost_auth_no_site',
                __('Site identifier is missing. Please start a new authorization.', 'masspost')
            );
        }

        $params = [
            'auz_token'       => $auz_token,
            'site_identifier' => $license['site_identifier'],
            'request_id'      => $request_id ? $request_id : $session_id,
        ];
        $params['signature'] = $this->build_signature($params, $license['site_identifier']);

        $response = $this->remote_post('/verify', $params);
        if (is_wp_error($response)) {
            $this->record_error($response);
            return $response;
        }

        if (empty($response['auz_secret'])) {
            $err = new WP_Error(
                'masspost_auth_invalid_token',
                __('Authorization token could not be validated.', 'masspost')
            );
            $this->record_error($err);
            return $err;
        }

        $license['status'] = 'authorized';
        $license['authorization'] = [
            'auz_token'          => $response['auz_token'],
            'auz_secret'         => $response['auz_secret'],
            'user_id'            => isset($response['user_id']) ? (int) $response['user_id'] : 0,
            'subscription_status' => $response['subscription_status'] ?? '',
            'subscription_type'  => isset($response['subscription_type']) ? (int) $response['subscription_type'] : 0,
            'expires_at'         => $response['expires_at'] ?? '',
            self::META_LAST_REFRESH => time(),
            'raw'                => $response,
        ];
        unset($license['session']);
        $license['last_error'] = null;

        $this->update_license($license);

        return $license['authorization'];
    }

    /**
     * Revoke current authorization remotely.
     *
     * @return bool|WP_Error
     */
    public function revoke_authorization()
    {
        $license = $this->get_license();
        if (empty($license['authorization']['auz_token']) || empty($license['site_identifier'])) {
            return new WP_Error(
                'masspost_revoke_missing',
                __('No active authorization to revoke.', 'masspost')
            );
        }

        $params = [
            'auz_token'       => $license['authorization']['auz_token'],
            'site_identifier' => $license['site_identifier'],
            'user_id'         => isset($license['authorization']['user_id']) ? (int) $license['authorization']['user_id'] : 0,
        ];

        $secret = $license['authorization']['auz_secret'] ?? $license['site_identifier'];
        $params['signature'] = $this->build_signature($params, $secret);

        $response = $this->remote_post('/revoke', $params);
        if (is_wp_error($response)) {
            $this->record_error($response);
            return $response;
        }

        $this->clear_license();
        return true;
    }

    /**
     * Update authorization data if the cached status is stale.
     *
     * @param bool $force Force refresh.
     * @return void
     */
    public function maybe_refresh_authorization(bool $force = false): void
    {
        if (! $this->is_authorized()) {
            return;
        }

        $license   = $this->get_license();
        $auth_data = $license['authorization'] ?? [];
        $last      = isset($auth_data[self::META_LAST_REFRESH]) ? (int) $auth_data[self::META_LAST_REFRESH] : 0;

        if (! $force && $last && (time() - $last) < self::DEFAULT_REFRESH_TTL) {
            return;
        }

        // Re-run verification using stored secret.
        $params = [
            'auz_token'       => $auth_data['auz_token'],
            'site_identifier' => $license['site_identifier'],
            'request_id'      => wp_generate_uuid4(),
        ];
        $params['signature'] = $this->build_signature($params, $auth_data['auz_secret']);

        $response = $this->remote_post('/verify', $params);

        // Handle revoked/disabled status from server - this catches 403 responses
        if (is_wp_error($response)) {
            $error_code = $response->get_error_code();
            $error_message = $response->get_error_message();

            // Check if this is a revocation/disabled response
            if (stripos($error_message, 'revoked') !== false || stripos($error_message, 'disabled') !== false) {
                // Clear authorization and set revoked status
                unset($license['authorization'], $license['session']);
                $license['status'] = 'revoked';
                $license['remote_status'] = 'revoked';
                $license['remote_updated'] = time();
                $license['last_error'] = [
                    'code'    => $error_code,
                    'message' => $error_message,
                ];
                $this->update_license($license);
                return;
            }

            $this->record_error($response);
            return;
        }

        // Check for revoked_at or inactive subscription status in successful response
        // These statuses should clear authorization and force re-authentication
        $inactive_statuses = ['revoked', 'disabled', 'expired', 'inactive', 'cancelled'];
        if (!empty($response['revoked_at']) || (isset($response['subscription_status']) && in_array($response['subscription_status'], $inactive_statuses, true))) {
            $status = $response['subscription_status'] ?? 'revoked';
            unset($license['authorization'], $license['session']);
            $license['status'] = $status;
            $license['remote_status'] = $status;
            $license['remote_updated'] = time();

            // Set appropriate error message based on status
            if (in_array($status, ['expired', 'inactive', 'cancelled'], true)) {
                $message = __('Your subscription has expired or is inactive. Please renew to continue using MassPost.', 'masspost');
            } else {
                $message = __('Authorization was revoked. Please reconnect your account to resume service.', 'masspost');
            }

            $license['last_error'] = [
                'code'    => 'masspost_subscription_' . $status,
                'message' => $response['message'] ?? $message,
            ];
            $this->update_license($license);
            return;
        }

        $license['authorization']['subscription_status'] = $response['subscription_status'] ?? $auth_data['subscription_status'] ?? '';
        $license['authorization']['subscription_type']   = isset($response['subscription_type']) ? (int) $response['subscription_type'] : ($auth_data['subscription_type'] ?? 0);
        $license['authorization']['expires_at']          = $response['expires_at'] ?? $auth_data['expires_at'] ?? '';
        $license['authorization'][self::META_LAST_REFRESH] = time();
        $license['authorization']['raw'] = $response;

        $this->update_license($license);
    }

    /**
     * Compute a stable identifier for the current site.
     *
     * @param string $url Optional URL override.
     * @return string
     */
    public function compute_site_identifier(string $url = ''): string
    {
        $raw = $url ? $url : home_url('/');
        $parts = wp_parse_url(trim((string) $raw));

        if (empty($parts['host'])) {
            return md5($raw);
        }

        $host = strtolower($parts['host']);
        if (0 === strpos($host, 'www.')) {
            $host = substr($host, 4);
        }

        $path = isset($parts['path']) ? untrailingslashit($parts['path']) : '';

        return $host . $path;
    }

    /**
     * Compute callback URL for authorization flow.
     *
     * @return string
     */
    public function get_callback_url(): string
    {
        $args = [
            'page'                    => 'masspost',
            'masspost_auth_callback'  => '1',
        ];

        $url = is_network_admin()
            ? network_admin_url('admin.php')
            : admin_url('admin.php');

        return apply_filters('masspost_authorization_callback_url', add_query_arg($args, $url));
    }

    /**
     * Store an error on the license payload for UI consumption.
     *
     * @param WP_Error $error Error details.
     * @return void
     */
    private function record_error(WP_Error $error): void
    {
        $license = $this->get_license();
        $license['last_error'] = [
            'code'    => $error->get_error_code(),
            'message' => $error->get_error_message(),
        ];
        $this->update_license($license);
    }

    /**
     * Send JSON request to authorization API.
     *
     * @param string $path API path.
     * @param array  $body Request payload.
     * @param array  $args Extra wp_remote_post args.
     * @return array|WP_Error
     */
    private function remote_post(string $path, array $body, array $args = [])
    {
        $endpoint = trailingslashit($this->api_base) . ltrim($path, '/');

        $request_args = wp_parse_args(
            [
                'timeout' => 15,
                'headers' => [
                    'Content-Type' => 'application/json',
                ],
                'body'    => wp_json_encode($body),
            ],
            $args
        );

        $response = wp_remote_post($endpoint, $request_args);

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

        $code = wp_remote_retrieve_response_code($response);
        $data = json_decode(wp_remote_retrieve_body($response), true);

        if ($code < 200 || $code >= 300) {
            $message = $data['message'] ?? __('Authorization server returned an error.', 'masspost');
            return new WP_Error(
                'masspost_auth_http_error',
                wp_strip_all_tags($message)
            );
        }

        return is_array($data) ? $data : [];
    }

    /**
     * Validate a signed payload received from the authorization server.
     *
     * @param array  $payload   Request payload without the signature.
     * @param string $secret    Shared secret.
     * @param string $signature Provided HMAC signature.
     * @return bool
     */
    private function verify_request_signature(array $payload, string $secret, string $signature): bool
    {
        if ('' === $secret || '' === $signature) {
            return false;
        }

        $expected = $this->build_signature($payload, $secret);

        return hash_equals($expected, $signature);
    }

    /**
     * Build HMAC signature.
     *
     * @param array  $params Request parameters.
     * @param string $secret Shared secret.
     * @return string
     */
    private function build_signature(array $params, string $secret): string
    {
        $payload = $params;
        unset($payload['signature']);
        ksort($payload);

        $string = '';
        foreach ($payload as $key => $value) {
            if (is_array($value)) {
                continue;
            }
            $string .= $key . '=' . $value . '&';
        }
        $string = rtrim($string, '&');

        return hash_hmac('sha256', $string, $secret);
    }
}

/**
 * Helper accessor.
 *
 * @return MassPost_License
 */
function masspost_license(): MassPost_License
{
    return MassPost_License::instance();
}

/**
 * Determine activation state.
 *
 * @return bool
 */
function masspost_is_activated(): bool
{
    // Bypass activation for masspost.net and its subdomains
    $host = wp_parse_url(home_url(), PHP_URL_HOST);
    if ($host && (str_ends_with($host, '.masspost.net') || 'masspost.net' === $host)) {
        return true;
    }

    return masspost_license()->is_authorized();
}

/**
 * Retrieve stored authorization payload.
 *
 * @return array
 */
function masspost_get_license_data(): array
{
    return masspost_license()->get_license();
}
