<?php

namespace Controllers;

use Libs\ResponseLib;
use Models\CommodityModel;
use Models\CprModel;
use Models\PaymentModel;
use Psr\Http\Message\ServerRequestInterface;
use Services\B3CprService;
use Services\TokenCreateService;

class PaymentConfirmController
{
    private PaymentModel $paymentModel;
    private CprModel $cprModel;
    private B3CprService $b3Service;
    private CommodityModel $commodityModel;
    private TokenCreateService $tokenCreateService;
    private \PDO $pdo;

    public function __construct()
    {
        if (!isset($GLOBALS['pdo']) || !$GLOBALS['pdo'] instanceof \PDO) {
            throw new \RuntimeException('Global PDO connection not initialized');
        }

        $this->pdo = $GLOBALS['pdo'];
        $this->paymentModel = new PaymentModel();
        $this->cprModel = new CprModel();
        $this->b3Service = new B3CprService();
        $this->commodityModel = new CommodityModel();
        $this->tokenCreateService = new TokenCreateService();
    }

    public function __invoke(ServerRequestInterface $request)
    {
        $body = json_decode((string)$request->getBody(), true) ?? [];
        $paymentId = isset($body['payment_id']) ? (int)$body['payment_id'] : 0;

        if ($paymentId <= 0) {
            return ResponseLib::sendFail('payment_id inválido', [], 'E_VALIDATE')->withStatus(400);
        }

        $payment = $this->paymentModel->findById($paymentId);
        if (!$payment) {
            return ResponseLib::sendFail('Pagamento não encontrado', [], 'E_NOT_FOUND')->withStatus(404);
        }

        $statusId = (int)($payment['status_id'] ?? 0);
        if ($statusId === 0) {
            return ResponseLib::sendFail('Pagamento ainda não confirmado', ['payment_id' => $paymentId], 'E_PAYMENT_PENDING')->withStatus(409);
        }

        if ($statusId !== 1) {
            return ResponseLib::sendFail('Pagamento em status inválido', ['status_id' => $statusId], 'E_PAYMENT_STATUS')->withStatus(409);
        }

        $cpr = $this->cprModel->findByPaymentId($paymentId);
        if (!$cpr) {
            return ResponseLib::sendFail('Nenhuma CPR vinculada ao pagamento', [], 'E_CPR_NOT_FOUND')->withStatus(404);
        }

        try {
            $payload = $this->b3Service->mapToB3($cpr);
        } catch (\Throwable $e) {
            return ResponseLib::sendFail('Falha ao montar payload para a B3: ' . $e->getMessage(), [], 'E_B3_MAP')->withStatus(500);
        }

        try {
            $token = $this->resolveB3Token($request, $body);
        } catch (\Throwable $e) {
            return ResponseLib::sendFail('Falha ao obter token de acesso da B3: ' . $e->getMessage(), [], 'E_B3_TOKEN')->withStatus(502);
        }

        try {
            $result = $this->b3Service->postCpr($token, $payload);
        } catch (\Throwable $e) {
            return ResponseLib::sendFail('Falha ao enviar CPR à B3: ' . $e->getMessage(), [], 'E_EXTERNAL')->withStatus(502);
        }

        if (isset($result['error'])) {
            return ResponseLib::sendFail('cURL error during B3 CPR request', ['error' => $result['error']], 'E_EXTERNAL')->withStatus(502);
        }

        $tickerSymbol = $result['json']['data']['tickerSymbol'] ?? null;

        try {
            $tokenResult = $this->createTokenFromCpr($cpr);
        } catch (\Throwable $e) {
            return ResponseLib::sendFail(
                'Falha ao gerar token: ' . $e->getMessage(),
                [],
                'E_TOKEN_CREATE'
            )->withStatus(500);
        }

        try {
            $this->cprModel->updateTokenId((int)$cpr['cpr_id'], (int)$tokenResult['token_id']);
            if ($tickerSymbol !== null) {
                $this->cprModel->updateTicker((int)$cpr['cpr_id'], (string)$tickerSymbol);
            }
        } catch (\Throwable $e) {
            return ResponseLib::sendFail(
                'Falha ao atualizar CPR: ' . $e->getMessage(),
                [],
                'E_CPR_UPDATE'
            )->withStatus(500);
        }

        return ResponseLib::sendOk([
            'message' => 'CPR enviada e token criado com sucesso',
            'payment_id' => $paymentId,
            'b3_response' => $result['json'] ?? ($result['raw'] ?? null),
            'token_id' => $tokenResult['token_id'],
            'token_external_id' => $tokenResult['token_external_id'],
            'tx_hash' => $tokenResult['tx_hash'],
        ], 'S_CPR_SENT');
    }

    private function resolveB3Token(ServerRequestInterface $request, array $body): string
    {
        $token = $body['b3_access_token'] ?? ($body['access_token'] ?? null);
        if (!$token) {
            $b3Auth = $request->getHeaderLine('X-B3-Authorization') ?: '';
            if (stripos($b3Auth, 'Bearer ') === 0) {
                $token = trim(substr($b3Auth, 7));
            }
        }

        if (!$token) {
            $token = $request->getHeaderLine('X-B3-Access-Token') ?: null;
        }

        if (!$token) {
            $token = $this->b3Service->getAccessToken();
        }

        return $token;
    }

    private function createTokenFromCpr(array $cpr): array
    {
        $inputs = $this->prepareTokenInputs($cpr);

        return $this->tokenCreateService->createToken(
            $inputs['token_commodities_amount'],
            $inputs['token_commodities_value'],
            $inputs['token_uf'],
            $inputs['token_city'],
            $inputs['token_content'],
            $inputs['token_flag'],
            $inputs['wallet_id'],
            $inputs['chain_id'],
            $inputs['commodities_id'],
            $inputs['cpr_id'],
            $inputs['user_id']
        );
    }

    /**
     * @return array{
     *     token_commodities_amount:int,
     *     token_commodities_value:int,
     *     token_uf:string,
     *     token_city:string,
     *     token_content:string,
     *     token_flag:string,
     *     wallet_id:int,
     *     chain_id:int,
     *     commodities_id:int,
     *     cpr_id:int,
     *     user_id:int
     * }
     */
    private function prepareTokenInputs(array $cpr): array
    {
        $cprId = (int)($cpr['cpr_id'] ?? 0);
        if ($cprId <= 0) {
            throw new \InvalidArgumentException('CPR sem identificador válido.');
        }

        $userId = (int)($cpr['user_id'] ?? 0);
        if ($userId <= 0) {
            throw new \InvalidArgumentException('CPR sem usuário associado.');
        }

        $companyId = 2;
        $wallet = $this->findWalletByCompanyId($companyId);

        $commoditiesName = $this->requireStringField($cpr, ['cpr_product_name'], 'cpr_product_name');
        $commoditiesId = $this->resolveCommodityId($commoditiesName);

        $tokenCommoditiesAmount = $this->requireNumericField(
            $cpr,
            ['cpr_product_quantity', 'cpr_issue_quantity'],
            'quantidade do produto'
        );
        $tokenCommoditiesValue = $this->requireNumericField(
            $cpr,
            ['cpr_issue_value', 'cpr_issue_financial_value'],
            'valor do produto'
        );
        $tokenUf = $this->requireStringField(
            $cpr,
            ['cpr_deliveryPlace_state_acronym', 'cpr_issuers_state_acronym'],
            'UF'
        );
        $tokenCity = $this->requireStringField(
            $cpr,
            ['cpr_deliveryPlace_city_name', 'cpr_issuers_city_name'],
            'cidade'
        );

        return [
            'token_commodities_amount' => $tokenCommoditiesAmount,
            'token_commodities_value' => $tokenCommoditiesValue,
            'token_uf' => $tokenUf,
            'token_city' => $tokenCity,
            'token_content' => (string)$cprId,
            'token_flag' => '',
            'wallet_id' => $wallet['wallet_id'],
            'chain_id' => $wallet['chain_id'],
            'commodities_id' => $commoditiesId,
            'cpr_id' => $cprId,
            'user_id' => $userId,
        ];
    }

    private function findWalletByCompanyId(int $companyId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT wallet_id, chain_id
             FROM "wallet"
             WHERE company_id = :company_id
             ORDER BY wallet_id ASC
             LIMIT 1'
        );
        $stmt->execute(['company_id' => $companyId]);
        $wallet = $stmt->fetch(\PDO::FETCH_ASSOC);

        if (!$wallet) {
            throw new \RuntimeException('Nenhuma carteira encontrada para a empresa informada.');
        }

        return [
            'wallet_id' => (int)$wallet['wallet_id'],
            'chain_id' => (int)$wallet['chain_id'],
        ];
    }

    private function resolveCommodityId(string $name): int
    {
        $commodityId = $this->commodityModel->getIdByName($name);
        if ($commodityId === null) {
            throw new \RuntimeException('Commodity não encontrada para o produto: ' . $name);
        }

        return $commodityId;
    }

    private function requireStringField(array $cpr, array $candidates, string $label): string
    {
        foreach ($candidates as $field) {
            if (!array_key_exists($field, $cpr)) {
                continue;
            }
            $value = $this->normalizeStringValue($cpr[$field]);
            if ($value !== '') {
                return $value;
            }
        }

        throw new \InvalidArgumentException("Campo {$label} ausente ou inválido na CPR.");
    }

    private function requireNumericField(array $cpr, array $candidates, string $label): int
    {
        foreach ($candidates as $field) {
            if (!array_key_exists($field, $cpr)) {
                continue;
            }
            $value = $this->normalizeNumericValue($cpr[$field]);
            if ($value !== null) {
                return $value;
            }
        }

        throw new \InvalidArgumentException("Campo {$label} ausente ou inválido na CPR.");
    }

    private function normalizeStringValue($value): string
    {
        if (is_array($value)) {
            $value = reset($value);
        }

        if (!is_scalar($value)) {
            return '';
        }

        $stringValue = trim((string)$value);
        if ($stringValue === '') {
            return '';
        }

        $parts = preg_split('/\s*;\s*/', $stringValue) ?: [];
        $first = $parts[0] ?? $stringValue;

        return trim((string)$first);
    }

    private function normalizeNumericValue($value): ?int
    {
        if (is_array($value)) {
            $value = reset($value);
        }

        if (is_string($value)) {
            $value = str_replace([' ', ','], ['', '.'], $value);
        }

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

        return null;
    }
}