<?php

namespace EasyLinkAutomations\Auth;

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

use EasyLinkAutomations\Models\Connection;

/**
 * Tests credentials against a platform's test endpoint.
 *
 * Uses the platform's getTestRequest() to make a real API call and
 * verify that the stored credentials are valid.
 */
final class CredentialTester
{
    /**
     * Test credentials for a connection.
     *
     * @param int $connectionId The connection to test
     *
     * @return array{success: bool, message: string}
     */
    public static function testConnection(int $connectionId): array
    {
        $connection = Connection::find($connectionId);
        if (!$connection) {
            return ['success' => false, 'message' => __('Connection not found', 'easylink-automations')];
        }

        $platform = $connection['platform'] ?? '';

        return self::test($platform, $connectionId);
    }

    /**
     * Test credentials for a platform using a connection's stored credentials.
     *
     * @param string $platform     The platform slug
     * @param int    $connectionId The connection ID
     *
     * @return array{success: bool, message: string}
     */
    public static function test(string $platform, int $connectionId): array
    {
        $platformClass = PlatformAuthRegistry::get($platform);
        if (!$platformClass) {
            return ['success' => false, 'message' => sprintf(
                /* translators: %s: platform slug. */
                __('Unknown platform: %s', 'easylink-automations'),
                $platform
            )];
        }

        $testRequest = $platformClass::getTestRequest();
        if ($testRequest === null) {
            return ['success' => true, 'message' => __('No test endpoint configured — credentials saved', 'easylink-automations')];
        }

        // Build authentication headers. Some APIs authenticate through URL/body
        // parameters in the test request, so an empty header set can be valid.
        $authHeaders = Connection::getAuthHeaders($connectionId);

        $headers = array_merge(
            ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            $authHeaders,
            $testRequest['headers'] ?? []
        );

        // Resolve URL placeholders like {api_url} from credentials
        $connection = Connection::find($connectionId);
        $creds      = $connection ? Connection::getCredentials($connection) : [];
        $creds      = self::withFieldDefaults($platformClass, $creds);
        $url        = self::resolveUrlPlaceholders($testRequest['url'], $creds);

        $requestArgs = [
            'method'  => $testRequest['method'] ?? 'GET',
            'headers' => $headers,
            'timeout' => 15,
        ];

        if (array_key_exists('body', $testRequest)) {
            $body = self::resolveValuePlaceholders($testRequest['body'], $creds);
            $requestArgs['body'] = is_array($body) ? wp_json_encode($body) : (string) $body;
        }

        $response = wp_remote_request($url, $requestArgs);

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

        $statusCode = wp_remote_retrieve_response_code($response);
        $rawBody    = wp_remote_retrieve_body($response);

        $verdict = self::askPlatform($platformClass, (int) $statusCode, $rawBody);
        if ($verdict !== null) {
            return $verdict;
        }

        $body = json_decode($rawBody, true);

        // Check for platform-specific error patterns
        if (is_array($body) && !empty($body['error'])) {
            $error = is_string($body['error']) ? $body['error'] : ($body['error']['message'] ?? 'Unknown error');

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

        // APIs such as Slack and Telegram report application errors with ok=false.
        if (is_array($body) && isset($body['ok']) && $body['ok'] === false) {
            return [
                'success' => false,
                'message' => sprintf(
                    /* translators: %s: error message returned by the remote service. */
                    __('Authentication failed: %s', 'easylink-automations'),
                    $body['error'] ?? $body['description'] ?? __('Invalid credentials', 'easylink-automations')
                ),
            ];
        }

        if ($statusCode === 401 || $statusCode === 403) {
            return [
                'success' => false,
                'message' => sprintf(
                    /* translators: %d: HTTP status code. */
                    __('Authentication failed: Invalid or expired credentials (HTTP %d)', 'easylink-automations'),
                    $statusCode
                ),
            ];
        }

        if ($statusCode >= 200 && $statusCode < 300) {
            return [
                'success' => true,
                'message' => __('Connection successful', 'easylink-automations'),
            ];
        }

        return [
            'success' => false,
            'message' => sprintf(
                /* translators: %d: HTTP status code. */
                __('Connection test returned HTTP %d', 'easylink-automations'),
                $statusCode
            ),
        ];
    }

    /**
     * Test raw credentials before saving (for non-OAuth2 flows).
     *
     * @param string $platform    The platform slug
     * @param array  $credentials Raw credential data (not yet stored)
     *
     * @return array{success: bool, message: string}
     */
    public static function testRaw(string $platform, array $credentials): array
    {
        $platformClass = PlatformAuthRegistry::get($platform);
        if (!$platformClass) {
            return ['success' => false, 'message' => sprintf(
                /* translators: %s: platform slug. */
                __('Unknown platform: %s', 'easylink-automations'),
                $platform
            )];
        }

        $testRequest = $platformClass::getTestRequest();
        if ($testRequest === null) {
            return ['success' => true, 'message' => __('No test endpoint configured — credentials accepted', 'easylink-automations')];
        }

        $credentials = self::withFieldDefaults($platformClass, $credentials);

        // Build headers directly from the raw credentials
        $authHeaders = $platformClass::authenticate($credentials);

        $headers = array_merge(
            ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            $authHeaders,
            $testRequest['headers'] ?? []
        );

        // Resolve URL placeholders like {api_url} from raw credentials
        $url = self::resolveUrlPlaceholders($testRequest['url'], $credentials);

        $requestArgs = [
            'method'  => $testRequest['method'] ?? 'GET',
            'headers' => $headers,
            'timeout' => 15,
        ];

        if (array_key_exists('body', $testRequest)) {
            $body = self::resolveValuePlaceholders($testRequest['body'], $credentials);
            $requestArgs['body'] = is_array($body) ? wp_json_encode($body) : (string) $body;
        }

        $response = wp_remote_request($url, $requestArgs);

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

        $statusCode = wp_remote_retrieve_response_code($response);
        $rawBody    = wp_remote_retrieve_body($response);

        $verdict = self::askPlatform($platformClass, (int) $statusCode, $rawBody);
        if ($verdict !== null) {
            return $verdict;
        }

        $body = json_decode($rawBody, true);

        if (is_array($body) && !empty($body['error'])) {
            $error = is_string($body['error']) ? $body['error'] : ($body['error']['message'] ?? 'Unknown error');

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

        if (is_array($body) && isset($body['ok']) && $body['ok'] === false) {
            return [
                'success' => false,
                'message' => sprintf(
                    /* translators: %s: error message returned by the remote service. */
                    __('Authentication failed: %s', 'easylink-automations'),
                    $body['error'] ?? $body['description'] ?? __('Invalid credentials', 'easylink-automations')
                ),
            ];
        }

        if ($statusCode === 401 || $statusCode === 403) {
            return ['success' => false, 'message' => sprintf(
                /* translators: %d: HTTP status code. */
                __('Authentication failed (HTTP %d)', 'easylink-automations'),
                $statusCode
            )];
        }

        if ($statusCode >= 200 && $statusCode < 300) {
            return ['success' => true, 'message' => __('Connection successful', 'easylink-automations')];
        }

        return ['success' => false, 'message' => sprintf(
            /* translators: %d: HTTP status code. */
            __('Test returned HTTP %d', 'easylink-automations'),
            $statusCode
        )];
    }

    /**
     * Let a connector judge its own test response.
     *
     * Most APIs signal failure through the status code or a JSON error field,
     * which the generic checks below already cover. APIs that answer with plain
     * text under HTTP 200 cannot be judged that way, so a connector may define
     * validateTestResponse() to decide for itself. Returning null from that
     * method — or not defining it at all — keeps the generic handling.
     *
     * @param class-string $platformClass
     *
     * @return array{success: bool, message: string}|null
     */
    private static function askPlatform(string $platformClass, int $statusCode, string $rawBody): ?array
    {
        if (!method_exists($platformClass, 'validateTestResponse')) {
            return null;
        }

        $verdict = $platformClass::validateTestResponse($statusCode, $rawBody);

        return is_array($verdict) ? $verdict : null;
    }

    /**
     * Replace {field_name} placeholders in a URL with credential values.
     *
     * @param string $url         The URL potentially containing {field_name} placeholders
     * @param array  $credentials Credential key-value pairs
     *
     * @return string The URL with placeholders resolved
     */
    private static function resolveUrlPlaceholders(string $url, array $credentials): string
    {
        return (string) preg_replace_callback(
            '/\{(\w+)\}/',
            static function (array $matches) use ($credentials): string {
                $key   = $matches[1];
                $value = $credentials[$key] ?? $matches[0];

                return is_string($value) ? rtrim($value, '/') : $matches[0];
            },
            $url
        );
    }

    /**
     * Add connector field defaults for credentials that predate new optional fields.
     *
     * @param class-string $platformClass
     */
    private static function withFieldDefaults(string $platformClass, array $credentials): array
    {
        foreach ($platformClass::getFields() as $key => $field) {
            if (!array_key_exists($key, $credentials) && array_key_exists('default', $field)) {
                $credentials[$key] = $field['default'];
            }
        }

        return $credentials;
    }

    /**
     * Resolve credential placeholders in scalar or structured test payloads.
     */
    private static function resolveValuePlaceholders(mixed $value, array $credentials): mixed
    {
        if (is_array($value)) {
            $resolved = [];
            foreach ($value as $key => $item) {
                $resolved[$key] = self::resolveValuePlaceholders($item, $credentials);
            }

            return $resolved;
        }

        if (!is_string($value)) {
            return $value;
        }

        return (string) preg_replace_callback(
            '/\{(\w+)\}/',
            static function (array $matches) use ($credentials): string {
                $key   = $matches[1];
                $found = $credentials[$key] ?? $matches[0];

                return is_scalar($found) ? (string) $found : $matches[0];
            },
            $value
        );
    }
}
