<?php

namespace io\billdesk\client\hmacsha256;

use DateTime;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use io\billdesk\client\BillDeskClient;
use io\billdesk\client\Response;
use io\billdesk\client\Constants;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use DateTimeImmutable;
use DateTimeZone;

class BillDeskJWEHS256Client implements BillDeskClient
{
    private Logger $logger;
    private Config $config;
    private JWESymmetricHelper $jweHelper;
    private string $pgBaseUrl;
    private string $clientId;

    public function __construct(Config $config, string $pgBaseUrl)
    {
        $this->config = $config;
        $this->pgBaseUrl = $pgBaseUrl;

        $this->initializeLogger();
        $this->initializeJweHelper();
    }

    private function initializeLogger(): void
    {
        $this->logger = new Logger('billdesk-jose-client-hs256');
        $this->logger->pushHandler(new StreamHandler('php://stdout', Logger::DEBUG));
    }

    private function initializeJweHelper(): void
    {
        $encryptionKey = $this->config->getEncryptionKeyBase64();
        $keyId = $this->config->getEncryptionKeyId();
        $clientId = $this->config->getClientId();
        $signingKey = $this->config->getSigningKeyBase64();
        $signingKeyId = $this->config->getSigningKeyId();

        $decodedEncryptionKey = base64_decode($encryptionKey);
        if (strlen($decodedEncryptionKey) !== 32) {
            throw new \InvalidArgumentException("Encryption key must decode to exactly 32 bytes.");
        }

        $decodedSigningKey = base64_decode($signingKey);
        if (strlen($decodedSigningKey) < 32) {
            throw new \InvalidArgumentException("Signing key must decode to at least 32 bytes.");
        }

        $this->jweHelper = new JWESymmetricHelper(
            $encryptionKey,
            $clientId,
            $keyId,
            $this->logger,
            $signingKey,
            $signingKeyId
        );
    }

    private function callPGApi(string $url, array $requestData, array $headers = []): Response
    {
        // Ensure trace ID
        if (empty($headers[Constants::HEADER_BD_TRACE_ID])) {
            $headers[Constants::HEADER_BD_TRACE_ID] = uniqid();
        }
        $bdTraceId = $headers[Constants::HEADER_BD_TRACE_ID];

        // Ensure timestamp (UTC)
        if (empty($headers[Constants::HEADER_BD_TIMESTAMP])) {
            $bdTimestamp = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('YmdHis');
            $headers[Constants::HEADER_BD_TIMESTAMP] = $bdTimestamp;
        } else {
            $bdTimestamp = $headers[Constants::HEADER_BD_TIMESTAMP];
        }

        // Set JOSE content type headers
        $headers["Content-Type"] = "application/jose";
        $headers["Accept"] = "application/jose";

        // Encode input payload
        $encryptedPayload = $this->jweHelper->encryptPayload($requestData, [$this->config->getClientId()]);
        $this->logger->info("Encrypted JWE token", ["jweToken" => $encryptedPayload]);

        $signedPayload = $this->jweHelper->sign($encryptedPayload);
        $this->logger->info("Signed JWS token", ["jwsToken" => $signedPayload]);

        $client = new Client();
        $httpResponse = null;

        try {
            $request = new Request("POST", $url, $headers, $signedPayload);
            $this->logger->info("callPGApi Request", [
                "url" => (string) $request->getUri(),
                "headers" => $request->getHeaders(),
                "body" => (string) $request->getBody()
            ]);

            $httpResponse = $client->send($request);
        } catch (RequestException $e) {
            $this->logger->error("Request to PG failed", [
                "message" => $e->getMessage(),
                "request_url" => (string) $e->getRequest()?->getUri(),
                "request_headers" => $e->getRequest()?->getHeaders(),
                "request_body" => (string) $e->getRequest()?->getBody(),
                "response" => $e->hasResponse() ? (string) $e->getResponse()->getBody() : null
            ]);

            $httpResponse = $e->getResponse();
        }

        if (!$httpResponse) {
            throw new \Exception("No response received from PG.");
        }

        $statusCode = $httpResponse->getStatusCode();
        $responseHeaders = $httpResponse->getHeaders();

        $body = (string) $httpResponse->getBody();
        $httpResponse->getBody()->rewind(); // In case it is read again downstream

        $this->logger->info("BDResponse Status Code: {$statusCode}");
        $this->logger->info("BDResponse Headers: " . json_encode($responseHeaders));
        $this->logger->info("BDRaw Response Body: {$body}");

        if (empty($body)) {
            $this->logger->error("Received empty response body from PG");
            throw new \Exception("Received empty response body from PG");
        }

        try {
            $decryptedResponse = $this->jweHelper->verifyAndDecrypt($body);
            $this->logger->info("Decrypted response from PG", ["body" => $decryptedResponse]);
        } catch (\Exception $e) {
            $this->logger->error("Decryption failed", [
                "error" => $e->getMessage(),
                "response_token" => $body
            ]);
            throw new \Exception("Decryption failed: " . $e->getMessage(), 0, $e);
        }

        $decodedPayload = json_decode($decryptedResponse, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $this->logger->error("Failed to decode decrypted JSON", ["error" => json_last_error_msg()]);
            throw new \Exception("Failed to decode decrypted JSON: " . json_last_error_msg());
        }

        return new Response($statusCode, $decodedPayload, $bdTraceId, $bdTimestamp);
    }

    public function createOrder($request, $headers = [])
    {
        return $this->callPGApi(Constants::createOrderURL($this->pgBaseUrl), $request, $headers);
    }

    public function createTransaction($request, $headers = [])
    {
        return $this->callPGApi(Constants::createTransactionURL($this->pgBaseUrl), $request, $headers);
    }

    public function refundTransaction($request, $headers = [])
    {
        return $this->callPGApi(Constants::refundTransactionURL($this->pgBaseUrl), $request, $headers);
    }

    public function setLogger($logger)
    {
        $this->logger = $logger;
    }
}
