<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use RuntimeException;

/**
 * Reusable HTTP client for Karvi Partner API (cURL).
 * Automatically attaches Bearer token and X-Request-Datetime on every request.
 */
final class ApiService
{
    private static ?self $instance = null;

    private array $config;

    /** @var callable(): ?User|null */
    private $userResolver;

    public function __construct(?array $config = null, ?callable $userResolver = null)
    {
        $this->config = $config ?? (require dirname(__DIR__) . '/config/api.php');
        $this->userResolver = $userResolver;
    }

    public static function instance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public static function setInstance(self $instance): void
    {
        self::$instance = $instance;
    }

    public function config(): array
    {
        return $this->config;
    }

    /**
     * ISO 8601 UTC datetime with milliseconds — required by Karvi API.
     */
    public static function requestDatetime(): string
    {
        $micro = microtime(true);
        $seconds = (int)$micro;
        $ms = (int)round(($micro - $seconds) * 1000);

        return gmdate('Y-m-d\TH:i:s', $seconds) . '.' . str_pad((string)$ms, 3, '0', STR_PAD_LEFT) . 'Z';
    }

    public function baseUrl(): string
    {
        return rtrim((string)($this->config['base_url'] ?? ''), '/');
    }

    public function buildUrl(string $path): string
    {
        return $this->baseUrl() . '/' . ltrim($path, '/');
    }

    /**
     * POST http://karviapi1.de/karvipartner/login
     * Body: {"email":"...","password":"..."}
     *
     * @return array{success:bool,message:string,data:?array,http_status:int,raw:?string}
     */
    public function login(string $email, string $password): array
    {
        return $this->request('POST', '/karvipartner/login', [
            'email' => $email,
            'password' => $password,
        ], withAuth: false);
    }

    /**
     * @param array<string,mixed>|null $body
     * @return array{success:bool,message:string,data:?array,http_status:int,raw:?string}
     */
    public function request(string $method, string $path, ?array $body = null, bool $withAuth = true): array
    {
        if (!function_exists('curl_init')) {
            return [
                'success' => false,
                'message' => 'PHP cURL extension is not enabled on this server.',
                'data' => null,
                'http_status' => 503,
                'raw' => null,
            ];
        }

        $url = str_starts_with($path, 'http') ? $path : $this->buildUrl($path);
        $method = strtoupper($method);
        $headers = $this->buildHeaders($withAuth);
        $jsonBody = $body !== null ? json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;

        $started = microtime(true);
        $ch = curl_init($url);
        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => (int)($this->config['timeout'] ?? 30),
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_CUSTOMREQUEST => $method,
        ];

        if ($jsonBody !== null) {
            $options[CURLOPT_POSTFIELDS] = $jsonBody;
        }

        if (empty($this->config['verify_ssl'])) {
            $options[CURLOPT_SSL_VERIFYPEER] = false;
            $options[CURLOPT_SSL_VERIFYHOST] = 0;
        }

        curl_setopt_array($ch, $options);

        $raw = curl_exec($ch);
        $httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

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

        $this->logRequest([
            'method' => $method,
            'url' => $url,
            'http_status' => $httpStatus,
            'duration_ms' => $durationMs,
            'curl_error' => $curlError !== '' ? $curlError : null,
            'request_body' => $this->sanitizeLogBody($body),
            'response_snippet' => is_string($raw) ? substr($raw, 0, 2000) : null,
        ]);

        if ($raw === false) {
            return [
                'success' => false,
                'message' => 'API connection failed: ' . $curlError,
                'data' => null,
                'http_status' => 502,
                'raw' => null,
            ];
        }

        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return [
                'success' => false,
                'message' => 'Invalid API response format.',
                'data' => null,
                'http_status' => $httpStatus,
                'raw' => $raw,
            ];
        }

        $hasSuccessFlag = array_key_exists('success', $decoded);
        $success = $httpStatus < 400 && (!$hasSuccessFlag || !empty($decoded['success']));
        $message = (string)($decoded['message'] ?? ($success ? 'OK' : 'API request failed.'));

        if ($httpStatus >= 400 && $message === 'OK') {
            $message = 'API request failed.';
        }

        return [
            'success' => $success,
            'message' => $message,
            'data' => $decoded,
            'http_status' => $httpStatus,
            'raw' => $raw,
        ];
    }

    public function get(string $path, bool $withAuth = true): array
    {
        return $this->request('GET', $path, null, $withAuth);
    }

    public function post(string $path, array $body = [], bool $withAuth = true): array
    {
        return $this->request('POST', $path, $body, $withAuth);
    }

    public function put(string $path, array $body = [], bool $withAuth = true): array
    {
        return $this->request('PUT', $path, $body, $withAuth);
    }

    public function patch(string $path, array $body = [], bool $withAuth = true): array
    {
        return $this->request('PATCH', $path, $body, $withAuth);
    }

    public function delete(string $path, bool $withAuth = true): array
    {
        return $this->request('DELETE', $path, null, $withAuth);
    }

    /** @return list<string> */
    private function buildHeaders(bool $withAuth): array
    {
        $headers = [
            'Accept: application/json',
            'Content-Type: application/json',
            'X-Request-Datetime: ' . self::requestDatetime(),
        ];

        if ($withAuth) {
            $token = $this->resolveBearerToken();
            if ($token !== null && $token !== '') {
                $headers[] = 'Authorization: Bearer ' . $token;
            }
        }

        return $headers;
    }

    private function resolveBearerToken(): ?string
    {
        if ($this->userResolver !== null) {
            $user = ($this->userResolver)();
            if ($user instanceof User && $user->token !== '') {
                return $user->token;
            }
        }

        if (function_exists('auth_api_token')) {
            $token = auth_api_token();
            if (is_string($token) && $token !== '') {
                return $token;
            }
        }

        $apiKey = trim((string)($this->config['api_key'] ?? ''));

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

    /** @param array<string,mixed>|null $body */
    private function sanitizeLogBody(?array $body): ?array
    {
        if ($body === null) {
            return null;
        }

        $safe = $body;
        if (isset($safe['password'])) {
            $safe['password'] = '***';
        }

        return $safe;
    }

    /** @param array<string,mixed> $entry */
    private function logRequest(array $entry): void
    {
        if (!function_exists('app_log_dir')) {
            return;
        }

        $line = json_encode(array_merge(['ts' => gmdate('c')], $entry), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($line === false) {
            return;
        }

        @file_put_contents(app_log_dir() . '/api.log', $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
}
