<?php

namespace App\Services\Sefaz;

use App\Models\Company;
use App\Models\SyncLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * Monitor de XML: consulta o serviço NFeDistribuicaoDFe da SEFAZ Ambiente
 * Nacional e baixa todos os documentos destinados ao CNPJ da empresa.
 *
 * O serviço funciona como uma fila: cada documento tem um NSU sequencial e a
 * consulta parte do último NSU já lido (ultNSU), avançando de lote em lote —
 * cada lote traz no máximo 50 documentos — até alcançar o maxNSU informado
 * pela própria SEFAZ.
 *
 * Códigos de retorno relevantes:
 *   137 — nenhum documento localizado (fim da fila)
 *   138 — documentos localizados
 *   656 — consumo indevido (o CNPJ está bloqueado temporariamente)
 */
class DistribuicaoDfeService
{
    /** Fim da fila: não há documento novo. */
    private const CSTAT_SEM_DOCUMENTOS = '137';

    /** Lote devolvido com documentos. */
    private const CSTAT_COM_DOCUMENTOS = '138';

    /** Bloqueio por consumo indevido — precisa recuar o intervalo. */
    private const CSTAT_CONSUMO_INDEVIDO = '656';

    public function __construct(
        private readonly SefazToolsFactory $tools,
        private readonly ArquivadorDfe $arquivador,
    ) {}

    /**
     * Executa um ciclo completo de sincronização para a empresa.
     */
    public function sync(Company $company, string $origem = 'cron'): SyncLog
    {
        $iniciado = microtime(true);

        $log = SyncLog::withoutGlobalScopes()->create([
            'company_id' => $company->id,
            'origem' => $origem,
            'status' => 'running',
            'nsu_inicial' => $company->ult_nsu,
        ]);

        $novasNotas = 0;
        $novosEventos = 0;
        $pendentes = 0;
        $requisicoes = 0;
        $ultNsu = (int) $company->ult_nsu;
        $maxNsu = (int) $company->max_nsu;
        $cstat = null;

        try {
            $tools = $this->tools->make($company);
            $limite = (int) config('nfe.max_requisicoes_por_sync');
            $pausa = (int) config('nfe.pausa_entre_requisicoes_ms');

            do {
                $resposta = $tools->sefazDistDFe($ultNsu);
                $requisicoes++;

                $lote = $this->lerResposta($resposta);
                $cstat = $lote['cstat'];

                if ($cstat === self::CSTAT_CONSUMO_INDEVIDO) {
                    throw new ConsumoIndevidoException($lote['motivo']);
                }

                if (! in_array($cstat, [self::CSTAT_COM_DOCUMENTOS, self::CSTAT_SEM_DOCUMENTOS], true)) {
                    throw new SefazException("SEFAZ retornou {$cstat}: {$lote['motivo']}");
                }

                $maxNsu = max($maxNsu, $lote['max_nsu']);
                $ultNsu = max($ultNsu, $lote['ult_nsu']);

                foreach ($lote['documentos'] as $documento) {
                    $resultado = $this->arquivador->arquivar($company, $documento);

                    if ($resultado['novo']) {
                        match ($resultado['tipo']) {
                            'nota' => $novasNotas++,
                            'evento' => $novosEventos++,
                            'pendente' => $pendentes++,
                        };
                    }
                }

                // Grava o cursor a cada lote: se o processo cair no meio, a
                // próxima execução retoma exatamente de onde parou.
                $company->forceFill(['ult_nsu' => $ultNsu, 'max_nsu' => $maxNsu])->save();

                $continua = $cstat === self::CSTAT_COM_DOCUMENTOS
                    && $ultNsu < $maxNsu
                    && $requisicoes < $limite;

                if ($continua && $pausa > 0) {
                    usleep($pausa * 1000);
                }
            } while ($continua);

            $status = ($novasNotas + $novosEventos) > 0 ? 'success' : 'empty';
            $mensagem = $ultNsu < $maxNsu
                ? sprintf('Parcial: %d documento(s) restantes na fila da SEFAZ.', $maxNsu - $ultNsu)
                : 'Fila da SEFAZ sincronizada.';

            if ($pendentes > 0) {
                $mensagem .= sprintf(' %d documento(s) não reconhecido(s) foram arquivados para conferência.', $pendentes);
            }

            $this->finalizar($company, $log, $status, $mensagem, compact('novasNotas', 'novosEventos', 'pendentes', 'requisicoes', 'ultNsu', 'maxNsu', 'cstat'), $iniciado);
        } catch (Throwable $e) {
            Log::warning('Falha na sincronização DFe', [
                'company_id' => $company->id,
                'erro' => $e->getMessage(),
            ]);

            $this->finalizar($company, $log, 'error', $e->getMessage(), compact('novasNotas', 'novosEventos', 'pendentes', 'requisicoes', 'ultNsu', 'maxNsu', 'cstat'), $iniciado);
        }

        return $log->refresh();
    }

    /**
     * Extrai cStat, cursores e documentos (base64 + gzip) do retDistDFeInt.
     *
     * @return array{cstat: string, motivo: string, ult_nsu: int, max_nsu: int, documentos: array<int, array{nsu: int, schema: string, xml: string}>}
     */
    private function lerResposta(string $resposta): array
    {
        $sx = @simplexml_load_string($resposta);

        if ($sx === false) {
            throw new SefazException('Resposta da SEFAZ em formato inesperado.');
        }

        // A resposta pode vir envelopada no SOAP ou já desembrulhada.
        $ret = $sx->getName() === 'retDistDFeInt' ? $sx : ($sx->xpath('//*[local-name()="retDistDFeInt"]')[0] ?? null);

        if ($ret === null) {
            throw new SefazException('Resposta da SEFAZ sem retDistDFeInt.');
        }

        $documentos = [];

        foreach ($ret->loteDistDFeInt->docZip ?? [] as $doc) {
            $xml = @gzdecode(base64_decode((string) $doc, true) ?: '');

            if ($xml === false) {
                continue;
            }

            $documentos[] = [
                'nsu' => (int) $doc['NSU'],
                'schema' => (string) $doc['schema'],
                'xml' => $xml,
            ];
        }

        return [
            'cstat' => (string) $ret->cStat,
            'motivo' => (string) $ret->xMotivo,
            'ult_nsu' => (int) $ret->ultNSU,
            'max_nsu' => (int) $ret->maxNSU,
            'documentos' => $documentos,
        ];
    }

    private function finalizar(Company $company, SyncLog $log, string $status, string $mensagem, array $ctx, float $iniciado): void
    {
        DB::transaction(function () use ($company, $log, $status, $mensagem, $ctx, $iniciado) {
            $log->forceFill([
                'status' => $status,
                'nsu_final' => $ctx['ultNsu'],
                'max_nsu' => $ctx['maxNsu'],
                'docs_novos' => $ctx['novasNotas'],
                'eventos_novos' => $ctx['novosEventos'],
                'requisicoes' => $ctx['requisicoes'],
                'cstat' => $ctx['cstat'],
                'mensagem' => mb_substr($mensagem, 0, 2000),
                'duracao_ms' => (int) ((microtime(true) - $iniciado) * 1000),
            ])->save();

            $company->forceFill([
                'ult_nsu' => $ctx['ultNsu'],
                'max_nsu' => $ctx['maxNsu'],
                'last_sync_at' => now(),
                'last_sync_status' => $status,
                'last_sync_message' => mb_substr($mensagem, 0, 2000),
            ])->save();
        });
    }
}
