<?php

namespace App\Services\Providers;

use App\Models\Transaction;
use App\Models\Vendor;
use App\Models\VendorTransactionLog;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Throwable;

abstract class AbstractProvider
{
    public function __construct(protected Vendor $vendor)
    {
    }

    abstract protected function headers(): array;

    protected function headersForMethod(string $method): array
    {
        return $this->headers();
    }

    protected function defaultBaseUrl(): ?string
    {
        return null;
    }

    protected function baseUrl(): string
    {
        return rtrim($this->vendor->base_url ?: (string) $this->defaultBaseUrl(), '/');
    }

    protected function http(?string $method = null): PendingRequest
    {
        return Http::acceptJson()
            ->contentType('application/json')
            ->timeout((int) ($this->vendor->timeout_seconds ?: 30))
            ->baseUrl($this->baseUrl())
            ->withHeaders($method ? $this->headersForMethod($method) : $this->headers());
    }

    protected function request(
        string $method,
        string $endpoint,
        array $payload = [],
        ?Transaction $transaction = null,
        array $options = []
    ): array {
        $started = microtime(true);
        $response = null;
        $body = [];
        $httpCode = null;
        $hardFailure = false;

        try {
            $pending = $this->http($method);

            if (($options['form'] ?? false) === true) {
                $pending = $pending->asForm();
            }

            $response = match (strtoupper($method)) {
                'GET' => $pending->get($endpoint, $payload),
                'POST' => $pending->post($endpoint, $payload),
                'PUT' => $pending->put($endpoint, $payload),
                'PATCH' => $pending->patch($endpoint, $payload),
                'DELETE' => $pending->delete($endpoint, $payload),
                default => throw new \InvalidArgumentException("Unsupported HTTP method [{$method}]"),
            };

            $httpCode = $response->status();
            $body = $this->decodeResponseBody($response);
        } catch (ConnectionException $e) {
            $hardFailure = true;
            $body = [
                'error' => 'connection_exception',
                'message' => $e->getMessage(),
            ];
        } catch (RequestException $e) {
            $response = $e->response;
            $httpCode = $response?->status();
            $body = $response ? $this->decodeResponseBody($response) : [
                'error' => 'request_exception',
                'message' => $e->getMessage(),
            ];
        } catch (Throwable $e) {
            $hardFailure = true;
            $body = [
                'error' => 'unexpected_exception',
                'message' => $e->getMessage(),
                'exception' => class_basename($e),
            ];
        }

        $durationMs = (int) round((microtime(true) - $started) * 1000);

        $this->logRequest(
            transaction: $transaction,
            endpoint: $endpoint,
            requestPayload: $payload,
            responsePayload: $body,
            httpCode: $httpCode,
            durationMs: $durationMs,
        );

        return [
            'response' => $response,
            'body' => $body,
            'http_code' => $httpCode,
            'duration_ms' => $durationMs,
            'hard_failure' => $hardFailure,
            'endpoint' => $endpoint,
        ];
    }

    protected function logRequest(
        ?Transaction $transaction,
        string $endpoint,
        array $requestPayload,
        array $responsePayload,
        ?int $httpCode,
        ?int $durationMs
    ): void {
        if (! $transaction) {
            return;
        }

        VendorTransactionLog::create([
            'transaction_id' => $transaction->id,
            'vendor_id' => $this->vendor->id,
            'endpoint' => $endpoint,
            'request_json' => $requestPayload,
            'response_json' => $responsePayload,
            'http_code' => $httpCode,
            'duration_ms' => $durationMs,
        ]);
    }

    protected function decodeResponseBody(Response $response): array
    {
        $decoded = json_decode($response->body(), true);

        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            return $decoded;
        }

        return [
            'raw' => Str::limit($response->body(), 5000, ''),
        ];
    }

    protected function bodyValue(array $body, array $paths, mixed $default = null): mixed
    {
        foreach ($paths as $path) {
            $value = data_get($body, $path);

            if ($value !== null && $value !== '') {
                return $value;
            }
        }

        return $default;
    }

    protected function normalizeStatusString(array $body): ?string
    {
        $status = $this->bodyValue($body, [
            'status',
            'Status',
            'transaction.status',
            'data.status',
            'api_response',
            'message.status',
        ]);

        return $status !== null ? Str::lower((string) $status) : null;
    }

    protected function normalizeVendorReference(array $body): ?string
    {
        $value = $this->bodyValue($body, [
            'transaction.reference',
            'transaction.id',
            'reference',
            'id',
            'data.reference',
            'data.id',
            'result.reference',
        ]);

        return $value !== null ? (string) $value : null;
    }

    protected function normalizeVendorCost(array $body): ?float
    {
        $balanceBefore = $this->numericValue($this->bodyValue($body, [
            'balance_before',
            'balanceBefore',
            'data.balance_before',
            'data.balanceBefore',
            'transaction.balance_before',
            'transaction.balanceBefore',
        ]));
        $balanceAfter = $this->numericValue($this->bodyValue($body, [
            'balance_after',
            'balanceAfter',
            'data.balance_after',
            'data.balanceAfter',
            'transaction.balance_after',
            'transaction.balanceAfter',
        ]));

        if ($balanceBefore !== null && $balanceAfter !== null && $balanceBefore >= $balanceAfter) {
            $delta = round($balanceBefore - $balanceAfter, 2);
            if ($delta > 0) {
                return $delta;
            }
        }

        $value = $this->bodyValue($body, [
            'vendor_cost',
            'cost',
            'transaction.vendor_cost',
            'transaction.cost',
            'data.vendor_cost',
            'data.cost',
            'price',
            'transaction.price',
            'data.price',
            'content.transactions.price',
            'paid_amount',
            'data.paid_amount',
            'transaction.paid_amount',
            'plan_amount',
            'data.plan_amount',
            'transaction.plan_amount',
            'api_charge',
            'charge',
            'content.transactions.amount',
            'content.amount',
            'amount',
        ]);

        return $this->numericValue($value);
    }

    protected function numericValue(mixed $value): ?float
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (is_numeric($value)) {
            return (float) $value;
        }

        if (is_string($value)) {
            $normalized = preg_replace('/[^0-9.\-]/', '', $value) ?? '';
            if ($normalized !== '' && is_numeric($normalized)) {
                return (float) $normalized;
            }
        }

        return null;
    }

    protected function normalizeMessage(array $body): ?string
    {
        $message = $this->bodyValue($body, [
            'message',
            'detail',
            'response',
            'response_description',
            'memo',
            'error',
            'errors',
            'content.message',
            'content.error',
            'transaction.memo',
        ]);

        return $this->stringifyMessageValue($message);
    }

    protected function stringifyMessageValue(mixed $message): ?string
    {
        if ($message === null) {
            return null;
        }

        if (is_scalar($message)) {
            $value = trim((string) $message);

            return $value !== '' ? $value : null;
        }

        if (! is_array($message)) {
            return null;
        }

        $parts = [];

        array_walk_recursive($message, function ($value) use (&$parts) {
            if (! is_scalar($value)) {
                return;
            }

            $text = trim((string) $value);
            if ($text !== '') {
                $parts[] = $text;
            }
        });

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

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

    protected function normalizePurchaseResponse(array $result): ProviderResponse
    {
        $body = Arr::get($result, 'body', []);
        $status = $this->normalizeStatusString($body);
        $httpCode = Arr::get($result, 'http_code');
        $durationMs = Arr::get($result, 'duration_ms');
        $hardFailure = (bool) Arr::get($result, 'hard_failure', false);
        $endpoint = Arr::get($result, 'endpoint');
        $message = $this->normalizeMessage($body);
        $vendorRef = $this->normalizeVendorReference($body);
        $vendorCost = $this->normalizeVendorCost($body);

        if ($hardFailure) {
            return ProviderResponse::failed(
                message: $message ?? 'Vendor connection failed',
                raw: $body,
                httpCode: $httpCode,
                durationMs: $durationMs,
                hardFailure: true,
                vendorReference: $vendorRef,
                vendorCost: $vendorCost,
                endpoint: $endpoint
            );
        }

        if ($this->matchesStatus($status, ['success', 'successful', 'completed', 'delivered', 'ok'])) {
            return ProviderResponse::success(
                message: $message,
                vendorReference: $vendorRef,
                vendorCost: $vendorCost,
                raw: $body,
                httpCode: $httpCode,
                durationMs: $durationMs,
                endpoint: $endpoint
            );
        }

        if ($this->matchesStatus($status, ['pending', 'processing', 'queued', 'in_progress'])) {
            return ProviderResponse::pending(
                message: $message,
                vendorReference: $vendorRef,
                vendorCost: $vendorCost,
                raw: $body,
                httpCode: $httpCode,
                durationMs: $durationMs,
                hardFailure: false,
                endpoint: $endpoint
            );
        }

        if (($httpCode !== null && $httpCode >= 500) || ($httpCode !== null && $httpCode === 429)) {
            return ProviderResponse::failed(
                message: $message ?? 'Vendor temporary error',
                raw: $body,
                httpCode: $httpCode,
                durationMs: $durationMs,
                hardFailure: true,
                vendorReference: $vendorRef,
                vendorCost: $vendorCost,
                endpoint: $endpoint
            );
        }

        return ProviderResponse::failed(
            message: $message ?? 'Vendor request failed',
            raw: $body,
            httpCode: $httpCode,
            durationMs: $durationMs,
            hardFailure: false,
            vendorReference: $vendorRef,
            vendorCost: $vendorCost,
            endpoint: $endpoint
        );
    }

    protected function matchesStatus(?string $status, array $needles): bool
    {
        if (! $status) {
            return false;
        }

        foreach ($needles as $needle) {
            if ($status === $needle || str_contains($status, $needle)) {
                return true;
            }
        }

        return false;
    }

    protected function unsupported(string $feature): ProviderResponse
    {
        return ProviderResponse::failed(
            message: "{$feature} is not supported for vendor type {$this->vendor->type}",
            raw: ['unsupported' => $feature],
            hardFailure: false
        );
    }
}
