/* ============================================================
 * Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com
 * Fase 5 — página pública de verificação (escopo §13)
 *
 * Sem tenant, sem UsuarioRef: é a única leitura do sistema que não passa
 * por RLS nem por identidade — ver db/migrations/006_verificacao_publica.sql
 * para como o banco garante que só os campos públicos saem daqui.
 *
 * O que a página pública NUNCA mostra: descrição do pagamento, endereços,
 * telefones, e-mails, documento completo, dados bancários, assinatura. Por
 * isso este serviço lê o resultado já reduzido da função do banco e nem
 * chega a tocar em credor_snapshot/devedor_snapshot inteiros — só nos dois
 * campos que a função devolve.
 * ============================================================ */

import type { Pool } from '../db/client.ts';
import { ErroDeNegocio } from '../core/errors.ts';
import { mascararNome, mascararDocumento } from '../core/canonical.ts';
import type { RateLimitService } from './rate-limit.service.ts';
import type { StorageAdapter } from './storage.ts';

export interface ReciboPublico {
  numeroFormatado: string;
  direcao: 'RECEBIMENTO' | 'PAGAMENTO';
  valorCentavos: number;
  dataPagamento: string;
  emitidoEm: string;
  emitenteNomeFantasia: string;
  logoUrl: string | null;
  contraparteNomeMascarado: string;
  contraparteDocumentoMascarado: string;
  status: 'VALIDO' | 'CANCELADO';
  motivoCancelamento: string | null;
  canceladoEm: string | null;
}

const FORMATO_CODIGO = /^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/;

export class VerificacaoService {
  constructor(
    private readonly pool: Pool,
    private readonly rateLimit: RateLimitService,
    /** Sem storage configurado (docs/decisoes.md §16, ainda em provisionamento), logoUrl vem sempre null. */
    private readonly storage?: StorageAdapter,
  ) {}

  /**
   * `ip` é obrigatório de propósito: não existe caminho para chamar isto
   * sem passar pelo rate limit — ver docs/escopo.md §13 e
   * db/migrations/007_rate_limit_publico.sql.
   */
  async publica(codigo: string, ip: string): Promise<ReciboPublico> {
    await this.rateLimit.exigirDentroDoLimite(ip);

    const normalizado = codigo.trim().toUpperCase();
    if (!FORMATO_CODIGO.test(normalizado)) {
      throw new ErroDeNegocio('Código de verificação inválido.', 'CODIGO_INVALIDO', 422);
    }

    const conn = await this.pool.connect();
    let linha: any;
    try {
      const { rows } = await conn.query(
        'SELECT * FROM recibos.buscar_recibo_publico($1)', [normalizado],
      );
      linha = rows[0];
    } finally {
      conn.release();
    }

    if (!linha || !linha.numero_formatado) {
      throw new ErroDeNegocio(
        'Nenhum recibo encontrado para esse código de verificação.', 'RECIBO_NAO_ENCONTRADO', 404,
      );
    }

    // A contraparte é o lado que NÃO emitiu o documento — o outro lado do
    // emitente_papel. É esse lado que fica mascarado; o emitente é público
    // por definição (é quem assina o documento).
    const contraparteNome = linha.emitente_papel === 'CREDOR' ? linha.devedor_nome : linha.credor_nome;
    const contraparteDocumento =
      linha.emitente_papel === 'CREDOR' ? linha.devedor_documento : linha.credor_documento;

    return {
      numeroFormatado: linha.numero_formatado,
      direcao: linha.direcao,
      valorCentavos: Number(linha.valor_centavos),
      dataPagamento: linha.data_pagamento,
      emitidoEm: linha.emitido_em,
      emitenteNomeFantasia: linha.emitente_nome_fantasia,
      logoUrl: linha.logo_storage_key && this.storage ? this.storage.urlPublica(linha.logo_storage_key) : null,
      contraparteNomeMascarado: mascararNome(contraparteNome),
      contraparteDocumentoMascarado: mascararDocumento(contraparteDocumento),
      status: linha.cancelado ? 'CANCELADO' : 'VALIDO',
      motivoCancelamento: linha.motivo_cancelamento ?? null,
      canceladoEm: linha.cancelado_em ?? null,
    };
  }
}
