<?php
/**
 * HTTP Service Class
 *
 * @version     1.0.$Revision:$
 * @version     SVN: $Id:$
 * @package     bplan-base/globals
 * @subpackage  Services
 * @author      Emilio Cannarozzo <emilio.cannarozzo@bplan-solutions.de>
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 * @copyright   Copyright (C) 2025, 2026 bplan-solutions GmbH & Co. KG <https://www.bplan-solutions.de/>
 * /Δ\
 */

namespace BplanBase\Globals\Services;


use Exception;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;


/**
 * HTTP Service Class
 *
 * @version     1.4.0 / 2026-05-15
 * @author      Emilio Cannarozzo <emilio.cannarozzo@bplan-solutions.de>
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 */
class HttpService
{


/* +++ TRAITS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */


/* +++ CLASS CONSTANTS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++ */


/* +++ OBJECT MEMBERS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * @var     array
     */
    protected array $_config;


    /**
     * @var     array
     */
    protected array $_defaults;


    /**
     * @var     integer
     */
    protected int $_logLevel;


    /**
     * @var     array
     */
    protected array $_logMask;


    /**
     * @var     PendingRequest
     */
    protected PendingRequest $_PendingRequest;


/* +++ CLASS MEMBERS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * Fallback-Defaults wenn config/http-service.php im Projekt nicht vorhanden ist.
     *
     * @var     array
     */
    protected static array $_configDefaults = [
        'auth' => [
            'type' => 'bearer',
        ],
        'connect-timeout' => 5,
        'headers' => [
            'accept' => 'application/json',
        ],
        'log' => [
            'incoming-request' => false,
            'level'            => 0,
            'map'              => [
                'Authorization',
                'password',
                'token',
            ],
        ],
        'retry' => [
            'sleep' => 500,
            'times' => 3,
        ],
        'timeout' => 10,
        'verify'  => true,
    ];


    /**
     *
     * @var     string
     */
    protected static string $_logPrefix = 'HttpService';


/* +++ OBJECT METHODS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * @param       string     $profile
     *              Name des Profils in config/http-service.php
     *
     * @param       array|null $config
     *              Direkte Konfiguration; überspringt den Profil-Lookup wenn übergeben
     *
     * @version     2.0.0 / 2026-05-14
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function __construct(string $profile = 'default', ?array $config = null)
    {
        $this->_config   = $config ?? config('http-service.profiles.'.$profile, []);
        $this->_defaults = config('http-service.defaults', static::$_configDefaults);
        $this->_logLevel = $this->_config['log']['level'] ?? $this->_defaults['log']['level'];
        $this->_logMask  = $this->_config['log']['mask']  ?? ['Authorization', 'password', 'token'];

        $this->_PendingRequest = $this->_buildPendingRequest();

    } // __construct()


    /**
     *
     *
     * @return      PendingRequest
     *
     * @version     1.1.0 / 2026-03-06
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _buildPendingRequest(): PendingRequest
    {
        $Request = Http::baseUrl($this->_config['base-url'] ?? '');

        if (!empty($this->_config['headers'])) {
            $Request = $Request->withHeaders($this->_config['headers']);
        }

        if (!empty($this->_config['connect-timeout'])) {
            $Request = $Request->connectTimeout($this->_config['connect-timeout']);
        }

        if (!empty($this->_config['timeout'])) {
            $Request = $Request->timeout($this->_config['timeout']);
        }

        if (!empty($this->_config['retry'])) {
            $times        = $this->_config['retry']['times'] ?? $this->_defaults['retry']['times'];
            $initialSleep = $this->_config['retry']['sleep'] ?? $this->_defaults['retry']['sleep'];

            $Request = $Request->retry(
                $times,
                function (int $attempt) use ($initialSleep) {
                    return (int) ($initialSleep * pow(2, $attempt - 1)); // 500, 1000, 2000, ...
                }
            );
        }

        if (isset($this->_config['verify']) && $this->_config['verify'] === false) {
            $Request = $Request->withoutVerifying();
        }

        if (!empty($this->_config['auth'])) {
            $auth = $this->_config['auth'];

            $Request = match ($auth['type'] ?? 'bearer') {
                'basic'  => $Request->withBasicAuth($auth['username'], $auth['password']),
                'digest' => $Request->withDigestAuth($auth['username'], $auth['password']),
                default  => $Request->withToken($auth['token'], 'Bearer'),
            };
        }
        return $Request;

    } // _buildPendingRequest()


    /**
     *
     *
     * @param       string $method
     *
     * @param       string $url
     *
     * @param       array $payload
     *
     * @return      Response
     *
     * @version     1.2.0 / 2026-05-07
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _execute(string $method, string $url, array $payload = []): Response
    {
        $resolvedUrl = $this->_resolveUrl($url);
        $label = strtoupper($method).' '.$url;
        $start = microtime(true);

        $Request = $this->_PendingRequest;

        /*
        **  Content-Type nur setzen bei Methoden mit Body und wenn er explizit konfiguriert ist. */
        $contentType = $this->_config['headers']['content-Type']
            ?? $this->_config['headers']['accept']
            ?? null;

        if ($contentType !== null && !in_array($method, ['get', 'head']) && !empty($payload)) {
            $Request = $Request->contentType($contentType);
        }
        try {
            $Response = $this->_PendingRequest->{$method}($resolvedUrl, $payload);
            $duration = round((microtime(true) - $start) * 1000);

            $this->_log($label, $Response, $duration, $payload);

            return $Response;

        } catch (\Throwable $E) {
            $context = [
                'request' => $this->_formatRequest($method, $resolvedUrl, $payload),
            ];

            if ($E instanceof RequestException && $E->response !== null) {
                $context['response'] = $this->_formatResponse($E->response);
            }
            \AppLog::error(
                message: $E,
                prefix: self::$_logPrefix,
                context: $context,
            );
            throw $E;
        }
    } // _execute()


    /**
     *
     *
     * @param       string $method
     *
     * @param       string $url
     *
     * @param       array $payload
     *
     * @return      array
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _formatRequest(string $method, string $url, array $payload): array
    {
        $headers = $this->_PendingRequest->mergeOptions([])['headers'] ?? [];

        return [
            'body' => $this->_maskSensitiveData($payload),
            'headers' => $this->_maskSensitiveData($headers),
            'method' => strtoupper($method),
            'url' => $url,
        ];
    } // _formatRequest()


    /**
     *
     *
     * @param       Response $Response
     *
     * @return      array
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _formatResponse(Response $Response): array
    {
        return [
            'body' => $Response->json() ?? $Response->body(),
            'headers' => $Response->headers(),
            'status' => $Response->status(),
        ];
    } // _formatResponse()


    /**
     *
     *
     * @param       string $label
     *
     * @param       Response $Response
     *
     * @param       integer $duration
     *
     * @param       array $payload
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _log(string $label, Response $Response, int $duration, array $payload): void
    {
        $method         = strtolower(explode(' ', $label)[0]);
        $url            = explode(' ', $label, 2)[1];
        $isFailed       = $Response->failed();
        $isClientErr    = $Response->clientError();
        $logMethod      = $isClientErr ? 'warning' : 'error';
        $correlationId  = Str::random(6);
        $requestPrefix  = 'HttpService ['.$correlationId.'/Request ';
        $responsePrefix = 'HttpService ['.$correlationId.'/Response';

        /*
        **  Query-Parameter: bei GET/HEAD aus Payload, sonst aus der URL. */
        if (in_array($method, ['get', 'head'])) {
            $outgoingQuery = $payload;

        } else {
            $outgoingQuery = [];
            $parsedUrl = parse_url($this->_resolveUrl($url));

            if (!empty($parsedUrl['query'])) {
                parse_str($parsedUrl['query'], $outgoingQuery);
            }
        }

        $incomingRequestData = [
            'method' => request()->method(),
            'url'    => request()->path(),
            'query'  => request()->query(),
        ];
        $logIncoming = $this->_config['log']['incoming-request'] ?? false;

        /*
        **  Fehlerfall: immer alles loggen, zwei Einträge. */
        if ($isFailed === true) {
            \AppLog::{$logMethod}(
                message: $label,
                prefix:  $requestPrefix,
                context: [
                    'method'           => strtoupper($method),
                    'status'           => $Response->status(),
                    'url'              => $url,
                    'query'            => $outgoingQuery,
                    'headers'          => $this->_maskSensitiveData(
                        $this->_PendingRequest->mergeOptions([])['headers'] ?? []
                    ),
                    'request_body'     => in_array($method, ['get', 'head']) ? [] : $this->_maskSensitiveData($payload),
                    'incoming_request' => $incomingRequestData,
                ],
            );
            \AppLog::{$logMethod}(
                message: $label,
                prefix:  $responsePrefix,
                context: [
                    'method'           => strtoupper($method),
                    'status'           => $Response->status(),
                    'duration_ms'      => $duration,
                    'response'         => $Response->json() ?? $Response->body(),
                    'incoming_request' => $incomingRequestData,
                ],
            );
            return;
        }
        /*
        **   Erfolgsfall: abhängig vom Log-Level. */
        if ($this->_logLevel === 0) {
            return;
        }
        /*
        **  Request-Eintrag. */
        $requestContext = [
            'method' => strtoupper($method),
            'status' => $Response->status(),
            'url'    => $url,
            'query'  => $outgoingQuery,
        ];

        if ($this->_logLevel >= 2) {
            $requestContext['headers']      = $this->_maskSensitiveData(
                $this->_PendingRequest->mergeOptions([])['headers'] ?? []
            );
            $requestContext['request_body'] = in_array($method, ['get', 'head']) ? [] : $this->_maskSensitiveData($payload);
        }

        if ($logIncoming) {
            $requestContext['incoming_request'] = $incomingRequestData;
        }
        \AppLog::info(
            message: $label,
            prefix:  $requestPrefix,
            context: $requestContext,
        );
        /*
        **  Response-Eintrag ab Level 3. */
        if ($this->_logLevel >= 3) {
            $responseContext = [
                'method'      => strtoupper($method),
                'status'      => $Response->status(),
                'duration_ms' => $duration,
                'response'    => $Response->json() ?? $Response->body(),
            ];

            if ($logIncoming == true) {
                $responseContext['incoming_request'] = $incomingRequestData;
            }
            \AppLog::info(
                message: $label,
                prefix:  $responsePrefix,
                context: $responseContext,
            );
        }
    } // _log()


    /**
     *
     *
     * @param       array $data
     *
     * @return      array
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _maskSensitiveData(array $data): array
    {
        return collect($data)
            ->mapWithKeys(function ($value, $key) {
                $masked = collect($this->_logMask)
                    ->contains(fn($field) => strtolower($field) === strtolower($key));

                return [$key => $masked ? '***' : $value];
            })
            ->toArray();

    } // _maskSensitiveData()


    /**
     *
     *
     * @param       string $url
     *
     * @return      string
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _resolveUrl(string $url): string
    {
        if (filter_var($url, FILTER_VALIDATE_URL)) {
            return $url;
        }
        $baseUrl = rtrim($this->_config['base-url'] ?? '', '/');
        $path = ltrim($url, '/');

        return $baseUrl.'/'.$path;

    } // _resolveUrl()


    /**
     *
     *
     * @param       string $contentType
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function accept(string $contentType): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->accept($contentType);

        return $Clone;

    } // accept()


    /**
     *
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function acceptJson(): static
    {
        return $this->accept('application/json');

    } // acceptJson()


    /**
     *
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function asForm(): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->asForm();

        return $Clone;

    } // asForm()


    /**
     *
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function asJson(): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->asJson();

        return $Clone;

    } // asJson()


    /**
     *
     *
     * @param       string $url
     *
     * @param       array $data
     *
     * @return      Response
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function delete(string $url, array $data = []): Response
    {
        return $this->_execute('delete', $url, $data);

    } // delete()


    /**
     *
     *
     * @param       string $url
     *
     * @param       array $query
     *
     * @return      Response
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function get(string $url, array $query = []): Response
    {
        return $this->_execute('get', $url, $query);

    } // get()


    /**
     *
     *
     * @param       string $url
     *
     * @param       array $query
     *
     * @return      Response
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function head(string $url, array $query = []): Response
    {
        return $this->_execute('head', $url, $query);

    } // head()


    /**
     *
     *
     * @param       string $url
     *
     * @param       array $data
     *
     * @return      Response
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function patch(string $url, array $data = []): Response
    {
        return $this->_execute('patch', $url, $data);

    } // patch()


    /**
     *
     *
     * @param       string $url
     *
     * @param       array $data
     *
     * @param       array $files
     *
     * @return      Response
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function post(string $url, array $data = [], array $files = []): Response
    {
        if (!empty($files)) {
            $Clone = clone $this;

            $Request = $Clone->_PendingRequest->asMultipart();

            foreach ($files as $name => $file) {
                $Request = $Request->attach($name, $file['contents'], $file['filename'] ?? $name);
            }
            $Clone->_PendingRequest = $Request;

            return $Clone->_execute('post', $url, $data);
        }
        return $this->_execute('post', $url, $data);

    } // post()


    /**
     *
     *
     * @param       string $profile
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function profile(string $profile): static
    {
        return new static($profile);

    } // profile()


    /**
     *
     *
     * @param       string $url
     *
     * @param       array $data
     *
     * @return      Response
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function put(string $url, array $data = []): Response
    {
        return $this->_execute('put', $url, $data);

    } // put()


    /**
     *
     *
     * @return      PendingRequest
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function raw(): PendingRequest
    {
        return $this->_PendingRequest;

    } // raw()


    /**
     *
     *
     * @param       integer $times
     *
     * @param       integer $sleepMilliseconds
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function retry(int $times, int $sleepMilliseconds = 0): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->retry($times, $sleepMilliseconds);

        return $Clone;

    } // retry()


    /**
     *
     *
     * @param       integer $seconds
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function timeout(int $seconds): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->timeout($seconds);

        return $Clone;

    } // timeout()


    /**
     *
     *
     * @param       string $username
     *
     * @param       string $password
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function withBasicAuth(string $username, string $password): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->withBasicAuth($username, $password);

        return $Clone;

    } // withBasicAuth()


    /**
     *
     *
     * @param       string $username
     *
     * @param       string $password
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function withDigestAuth(string $username, string $password): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->withDigestAuth($username, $password);

        return $Clone;

    } // withDigestAuth()


    /**
     *
     *
     * @param       array $headers
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function withHeaders(array $headers): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->withHeaders($headers);

        return $Clone;

    } // withHeaders()


    /**
     * Undocumented function
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function withoutRedirecting(): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->withoutRedirecting();

        return $Clone;

    } // withoutRedirecting()


    /**
     *
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function withoutVerifying(): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->withoutVerifying();

        return $Clone;

    } // withoutVerifying()


    /**
     *
     *
     * @param       string $token
     *
     * @param       string $type
     *
     * @return      static
     *
     * @version     1.0.0 / 2026-02-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function withToken(string $token, string $type = 'Bearer'): static
    {
        $Clone = clone $this;

        $Clone->_PendingRequest = $this->_PendingRequest->withToken($token, $type);

        return $Clone;

    } // withToken()


/* +++ CLASS METHODS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++++ */


} // class HttpService {}
