<?php

namespace App\Services\Sefaz;

use App\Models\Company;
use App\Models\DfePendente;
use App\Models\NotaEvento;
use App\Models\NotaFiscal;
use Illuminate\Support\Facades\Log;

/**
 * Persistência dos documentos vindos do NFeDistribuicaoDFe.
 *
 * Concentra as regras fiscais num lugar só — o monitor e a consulta por chave
 * usam o mesmo caminho, então cancelamento, manifestação e arquivamento não
 * podem divergir entre eles.
 */
class ArquivadorDfe
{
    public function __construct(
        private readonly DfeParser $parser,
        private readonly XmlStorage $storage,
    ) {}

    /**
     * Arquiva um documento do lote.
     *
     * @param  array{nsu: int, schema: string, xml: string}  $documento
     * @return array{tipo: 'nota'|'evento'|'pendente', novo: bool, nota: ?NotaFiscal}
     */
    public function arquivar(Company $company, array $documento): array
    {
        $parsed = $this->parser->parse($documento['xml'], $documento['nsu'], $documento['schema']);

        if ($parsed === null) {
            $this->registrarPendente($company, $documento, 'Documento em formato não reconhecido pelo sistema.');

            return ['tipo' => 'pendente', 'novo' => true, 'nota' => null];
        }

        if ($parsed['kind'] === 'nota') {
            if (strlen((string) $parsed['data']['chave']) !== 44) {
                $this->registrarPendente($company, $documento, 'Documento sem chave de acesso válida.');

                return ['tipo' => 'pendente', 'novo' => true, 'nota' => null];
            }

            return $this->salvarNota($company, $parsed['data'], $documento['xml']);
        }

        if (strlen((string) $parsed['data']['chave']) !== 44) {
            $this->registrarPendente($company, $documento, 'Evento sem chave de acesso válida.');

            return ['tipo' => 'pendente', 'novo' => true, 'nota' => null];
        }

        return $this->salvarEvento($company, $parsed['data'], $documento['xml']);
    }

    /**
     * @return array{tipo: 'nota', novo: bool, nota: NotaFiscal}
     */
    public function salvarNota(Company $company, array $dados, string $xml): array
    {
        $nota = NotaFiscal::withoutGlobalScopes()->firstOrNew([
            'company_id' => $company->id,
            'chave' => $dados['chave'],
        ]);

        $novo = ! $nota->exists;

        // Um resumo que chega depois nunca sobrescreve o XML completo já arquivado.
        if (! $novo && $nota->tem_xml_completo && ! $dados['tem_xml_completo']) {
            return ['tipo' => 'nota', 'novo' => false, 'nota' => $nota];
        }

        $dados['dest_cnpj'] ??= $company->cnpj;

        if ($dados['tem_xml_completo']) {
            $dados['xml_path'] = $this->storage->putNota($company, $dados['chave'], $xml, (string) ($dados['data_emissao'] ?? ''));
            $dados['xml_size'] = strlen($xml);
        }

        // Cancelamento já registrado não é revertido por uma reentrega do
        // documento original, que ainda traz o protocolo de autorização.
        if (! $novo && $nota->situacao === 'cancelada') {
            unset($dados['situacao']);
        }

        $nota->fill($dados)->save();

        return ['tipo' => 'nota', 'novo' => $novo, 'nota' => $nota];
    }

    /**
     * @return array{tipo: 'evento', novo: bool, nota: ?NotaFiscal}
     */
    public function salvarEvento(Company $company, array $dados, string $xml): array
    {
        $nota = NotaFiscal::withoutGlobalScopes()
            ->where('company_id', $company->id)
            ->where('chave', $dados['chave'])
            ->first();

        $evento = NotaEvento::withoutGlobalScopes()->firstOrNew([
            'company_id' => $company->id,
            'chave' => $dados['chave'],
            'tipo_evento' => $dados['tipo_evento'],
            'sequencia' => $dados['sequencia'],
        ]);

        $novo = ! $evento->exists;

        $evento->fill($dados);
        $evento->nota_fiscal_id = $nota?->id;
        $evento->xml_path = $this->storage->putEvento(
            $company,
            $dados['chave'],
            $dados['tipo_evento'],
            $dados['sequencia'],
            $xml,
            (string) ($dados['data_evento'] ?? ''),
        );
        $evento->save();

        if ($nota) {
            $this->refletirEventoNaNota($nota, $evento);
        }

        return ['tipo' => 'evento', 'novo' => $novo, 'nota' => $nota];
    }

    /**
     * Eventos mudam o estado fiscal da nota: cancelamento tira o documento de
     * circulação, e a manifestação registrada na SEFAZ vale mesmo quando foi
     * feita fora deste sistema.
     */
    private function refletirEventoNaNota(NotaFiscal $nota, NotaEvento $evento): void
    {
        $mudancas = [];

        if (in_array($evento->tipo_evento, NotaEvento::CANCELAMENTOS, true)) {
            $mudancas['situacao'] = 'cancelada';
        }

        if ($manifestacao = NotaEvento::manifestacaoPara($evento->tipo_evento)) {
            // Vale a manifestação mais recente: a empresa pode dar ciência e,
            // depois, confirmar ou desconhecer a operação.
            if ($nota->manifestacao_at === null
                || $evento->data_evento === null
                || $evento->data_evento->gte($nota->manifestacao_at)) {
                $mudancas['manifestacao'] = $manifestacao;
                $mudancas['manifestacao_at'] = $evento->data_evento ?? now();
                $mudancas['manifestacao_protocolo'] = $evento->protocolo;
            }
        }

        if ($mudancas) {
            $nota->forceFill($mudancas)->save();
        }
    }

    /**
     * Guarda o documento bruto e registra a falha. Nada some por não termos
     * sabido interpretar.
     */
    public function registrarPendente(Company $company, array $documento, string $motivo): void
    {
        $raiz = null;
        $chave = null;

        if ($sx = @simplexml_load_string($documento['xml'])) {
            $raiz = $sx->getName();

            if (preg_match('/\b(\d{44})\b/', $documento['xml'], $m)) {
                $chave = $m[1];
            }
        }

        Log::warning('Documento DFe não interpretado', [
            'company_id' => $company->id,
            'nsu' => $documento['nsu'],
            'schema' => $documento['schema'] ?? null,
            'raiz' => $raiz,
        ]);

        DfePendente::withoutGlobalScopes()->updateOrCreate(
            ['company_id' => $company->id, 'nsu' => $documento['nsu']],
            [
                'schema_dfe' => $documento['schema'] ?? null,
                'raiz' => $raiz,
                'chave' => $chave,
                'motivo' => mb_substr($motivo, 0, 200),
                'xml_path' => $this->storage->putBruto($company, $documento['nsu'], $documento['xml']),
            ],
        );
    }
}
