<?php

namespace EasyLinkAutomations\Actions\Sendy;

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

use EasyLinkAutomations\Models\Connection;

/**
 * Shared HTTP plumbing for Sendy actions in both the free and pro tiers.
 *
 * Sendy runs on the user's own server and exposes a form-encoded POST API that
 * answers with plain text for most endpoints (JSON only for the brand and list
 * lookups), almost always under HTTP 200. Success therefore has to be decided
 * from the body rather than the status code.
 */
final class SendyRequestHelper
{
    private const TIMEOUT = 30;

    /**
     * Bodies that mean "your credentials or installation URL are wrong",
     * whichever endpoint returned them.
     */
    private const CREDENTIAL_ERRORS = [
        'no data passed',
        'api key not passed',
        'invalid api key',
    ];

    /**
     * Resolve and validate the stored credentials for a Sendy connection.
     *
     * @return array{url: string, api_key: string}|string Credentials, or an error message
     */
    public static function credentials(int $connectionId): array|string
    {
        if ($connectionId <= 0) {
            return 'No Sendy connection selected';
        }

        $connection = Connection::find($connectionId);
        if (!$connection || ($connection['platform'] ?? '') !== 'sendy') {
            return 'Invalid Sendy connection';
        }

        $creds  = Connection::getCredentials($connection);
        $url    = rtrim(trim((string) ($creds['url'] ?? '')), '/');
        $apiKey = trim((string) ($creds['api_key'] ?? ''));

        if ($url === '' || $apiKey === '') {
            return 'Sendy connection is missing its installation URL or API key';
        }

        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            return 'Sendy installation URL is not a valid URL';
        }

        return ['url' => $url, 'api_key' => $apiKey];
    }

    /**
     * POST a form-encoded request to a Sendy endpoint.
     *
     * The api_key is added here so no caller has to handle credentials.
     *
     * @param array<string, scalar> $fields Body fields to send alongside the API key
     *
     * @return array{success: bool, message: string, body: string}
     */
    public static function post(int $connectionId, string $path, array $fields): array
    {
        $creds = self::credentials($connectionId);
        if (is_string($creds)) {
            return ['success' => false, 'message' => $creds, 'body' => ''];
        }

        $fields['api_key'] = $creds['api_key'];

        $response = wp_remote_post($creds['url'] . '/' . ltrim($path, '/'), [
            'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
            'body'    => $fields,
            'timeout' => self::TIMEOUT,
        ]);

        if (is_wp_error($response)) {
            return [
                'success' => false,
                'message' => 'Request to Sendy failed: ' . $response->get_error_message(),
                'body'    => '',
            ];
        }

        $statusCode = (int) wp_remote_retrieve_response_code($response);
        $body       = trim(wp_remote_retrieve_body($response));

        if ($statusCode < 200 || $statusCode >= 300) {
            return [
                'success' => false,
                'message' => 'Sendy returned HTTP ' . $statusCode . ($body === '' ? '' : ': ' . self::truncate($body)),
                'body'    => $body,
            ];
        }

        return ['success' => true, 'message' => '', 'body' => $body];
    }

    /**
     * Whether a plain-text Sendy body signals success.
     *
     * The current API documents "true" as the affirmative response, while older
     * installations answer "1" for the same endpoints, so both are accepted.
     */
    public static function isAffirmative(string $body): bool
    {
        return in_array(strtolower(trim($body)), ['true', '1'], true);
    }

    /**
     * Build a failure result from a plain-text Sendy body.
     *
     * @return array{success: bool, message: string, data: array}
     */
    public static function failure(string $body, string $fallback): array
    {
        $body = trim($body);

        if ($body === '') {
            return ['success' => false, 'message' => $fallback, 'data' => []];
        }

        if (in_array(strtolower($body), self::CREDENTIAL_ERRORS, true)) {
            return [
                'success' => false,
                'message' => 'Sendy rejected the credentials: ' . $body,
                'data'    => [],
            ];
        }

        return ['success' => false, 'message' => 'Sendy: ' . self::truncate($body), 'data' => []];
    }

    /**
     * Decode a JSON response body from the brand and list lookup endpoints.
     *
     * @return array<mixed>|string Decoded payload, or an error message
     */
    public static function decodeList(string $body): array|string
    {
        $body = trim($body);

        if ($body === '') {
            return 'Sendy returned an empty response';
        }

        $decoded = json_decode($body, true);
        if (!is_array($decoded)) {
            // These endpoints report every failure as a plain-text sentence.
            return $body;
        }

        return $decoded;
    }

    /**
     * Normalise a toggle config value into a boolean.
     */
    public static function toBool(mixed $value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_string($value)) {
            return in_array(strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true);
        }

        return (bool) $value;
    }

    /**
     * Collapse a comma-separated ID list into a clean comma-separated string.
     */
    public static function normaliseIdList(mixed $value): string
    {
        if (is_array($value)) {
            $parts = $value;
        } else {
            $parts = explode(',', (string) $value);
        }

        $parts = array_filter(array_map(
            static fn(mixed $part): string => trim((string) $part),
            $parts
        ), static fn(string $part): bool => $part !== '');

        return implode(',', $parts);
    }

    private static function truncate(string $value): string
    {
        return strlen($value) > 200 ? substr($value, 0, 200) . '…' : $value;
    }
}
