/* ============================================================
 * Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com
 * Fase 3 — rascunho, selo e cancelamento
 *
 * O rascunho é a única superfície editável do sistema. Selar congela
 * tudo: número, snapshots, extenso, código público e hash encadeado.
 * Depois do selo a única escrita possível é o cancelamento.
 * ============================================================ */

import { emTransacao, type Pool, type Executor } from '../db/client.ts';
import { exigir, exigirSerie, type UsuarioRef } from '../core/identity.ts';
import { DadosInvalidos, NaoEncontrado } from '../core/errors.ts';
import { valorPorExtenso } from '../core/extenso.ts';
import {
  canonicalizar, gerarCodigoVerificacao, VERSAO_PAYLOAD, type PayloadRecibo,
} from '../core/canonical.ts';
import type { LicencaService } from './licenca.service.ts';
import type { Direcao } from './cadastros.service.ts';
import { DESCRICAO_MAXIMA } from '../render/template.ts';

export interface DadosRascunho {
  seriesId: string;
  counterpartyId?: string | null;
  valorCentavos: number;
  descricao: string;
  natureza?: string | null;
  formaPagamento: string;
  dataPagamento: string;          // YYYY-MM-DD
  localPagamento: string;
  referencia?: string | null;
  assinaturaAssetId?: string | null;
  signatarioNome?: string | null;
  signatarioDocumento?: string | null;
  externalSource?: string | null;
  externalRef?: string | null;
}

interface Pessoa { nome: string; documento: string; tipo: 'PF' | 'PJ'; endereco?: string | null; }

export interface FiltroRecibos {
  seriesId?: string;
  direcao?: Direcao;
  busca?: string;
  limite?: number;
}

/**
 * Quem é credor e quem é devedor sai da direção da série, não de um campo
 * digitado. No recebimento a empresa é credora; no pagamento é a contraparte.
 * Gravar os dois lados explicitamente deixa o documento legível daqui a cinco
 * anos sem precisar rodar regra de negócio nenhuma.
 */
export function definirPartes(direcao: Direcao, empresa: Pessoa, contraparte: Pessoa) {
  return direcao === 'RECEBIMENTO'
    ? { credor: empresa, devedor: contraparte, emitentePapel: 'CREDOR' as const }
    : { credor: contraparte, devedor: empresa, emitentePapel: 'DEVEDOR' as const };
}

/** PJ dá quitação no plural, PF no singular. */
export function verboDeQuitacao(credor: Pessoa): 'Recebemos' | 'Recebi' {
  return credor.tipo === 'PJ' ? 'Recebemos' : 'Recebi';
}

export function corpoDoRecibo(
  direcao: Direcao, empresa: Pessoa, contraparte: Pessoa, valorCentavos: number, descricao: string,
): string {
  const { credor, devedor } = definirPartes(direcao, empresa, contraparte);
  return `${verboDeQuitacao(credor)} de ${devedor.nome}, inscrito sob o nº ${devedor.documento}, ` +
    `a importância de ${valorPorExtenso(valorCentavos)}, referente a ${descricao}, ` +
    'dando plena e geral quitação do valor ora recebido.';
}

export class RecibosService {
  constructor(
    private readonly pool: Pool,
    private readonly licenca: LicencaService,
  ) {}

  // ---------------- rascunho ----------------

  async salvarRascunho(u: UsuarioRef, dados: DadosRascunho, rascunhoId?: string) {
    exigir(u, 'emitir');
    exigirSerie(u, dados.seriesId);
    this.validar(dados);

    return emTransacao(this.pool, async (x) => {
      const serie = await this.carregarSerie(x, dados.seriesId);
      if (rascunhoId) {
        const { rows } = await x.query(
          `UPDATE recibos.receipt_drafts SET
             series_id=$2, counterparty_id=$3, direcao=$4, valor_centavos=$5, descricao=$6,
             natureza=$7, forma_pagamento=$8, data_pagamento=$9, local_pagamento=$10,
             referencia=$11, assinatura_asset_id=$12, signatario_nome=$13,
             signatario_documento=$14, atualizado_em=now()
           WHERE id=$1 RETURNING *`,
          [rascunhoId, dados.seriesId, dados.counterpartyId ?? null, serie.direcao,
           dados.valorCentavos, dados.descricao, dados.natureza ?? null, dados.formaPagamento,
           dados.dataPagamento, dados.localPagamento, dados.referencia ?? null,
           dados.assinaturaAssetId ?? null, dados.signatarioNome ?? null,
           dados.signatarioDocumento ?? null],
        );
        if (!rows[0]) throw new NaoEncontrado('Rascunho');
        return rows[0];
      }
      const { rows } = await x.query(
        `INSERT INTO recibos.receipt_drafts
           (tenant_id, company_id, series_id, counterparty_id, direcao, valor_centavos,
            descricao, natureza, forma_pagamento, data_pagamento, local_pagamento,
            referencia, assinatura_asset_id, signatario_nome, signatario_documento, criado_por)
         VALUES (current_setting('app.tenant_id')::uuid, $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
         RETURNING *`,
        [serie.company_id, dados.seriesId, dados.counterpartyId ?? null, serie.direcao,
         dados.valorCentavos, dados.descricao, dados.natureza ?? null, dados.formaPagamento,
         dados.dataPagamento, dados.localPagamento, dados.referencia ?? null,
         dados.assinaturaAssetId ?? null, dados.signatarioNome ?? null,
         dados.signatarioDocumento ?? null, u.id],
      );
      return rows[0];
    });
  }

  // ---------------- selo ----------------

  /**
   * Um único caminho para criar documento selado. A aplicação nem tem
   * INSERT em recibos.receipts — só esta função do banco cria linha lá.
   */
  async selar(u: UsuarioRef, dados: DadosRascunho, rascunhoId?: string) {
    exigir(u, 'emitir');
    exigirSerie(u, dados.seriesId);
    this.validar(dados);

    const licenca = await this.licenca.exigirPodeSelar();
    await this.licenca.exigirDentroDoLimiteMensal(licenca);

    return emTransacao(this.pool, async (x) => {
      const serie = await this.carregarSerie(x, dados.seriesId);
      const empresa = await this.carregarEmpresa(x, serie.company_id);
      const contraparte = await this.carregarContraparte(x, dados.counterpartyId);

      const { credor, devedor, emitentePapel } =
        definirPartes(serie.direcao, empresa, contraparte);

      // O número real só é atribuído dentro da função do banco, com o lock
      // na mão. Aqui ele entra no payload como placeholder e é substituído
      // pelo valor definitivo antes do hash — ver nota abaixo.
      const { rows: prox } = await x.query(
        'SELECT codigo, proximo_numero FROM recibos.series WHERE id = $1 FOR UPDATE', [dados.seriesId],
      );
      const numeroFormatado =
        `${prox[0].codigo}-${String(prox[0].proximo_numero).padStart(6, '0')}`;

      const codigoVerificacao = gerarCodigoVerificacao();
      const emitidoEm = new Date().toISOString();

      const payload: PayloadRecibo = {
        direcao: serie.direcao,
        numero_formatado: numeroFormatado,
        valor_centavos: dados.valorCentavos,
        descricao: dados.descricao,
        natureza: dados.natureza ?? null,
        forma_pagamento: dados.formaPagamento,
        data_pagamento: dados.dataPagamento,
        local_pagamento: dados.localPagamento,
        referencia: dados.referencia ?? null,
        credor_snapshot: credor as unknown as Record<string, unknown>,
        devedor_snapshot: devedor as unknown as Record<string, unknown>,
        emitente_papel: emitentePapel,
        logo_sha256: empresa.logo_sha256 ?? null,
        assinatura_sha256: dados.assinaturaAssetId ? await this.hashDoAsset(x, dados.assinaturaAssetId) : null,
        signatario_nome: dados.signatarioNome ?? credor.nome,
        external_source: dados.externalSource ?? null,
        external_ref: dados.externalRef ?? null,
        emitido_por: u.id,
        emitido_em: emitidoEm,
      };

      const canonico = canonicalizar(payload);

      const { rows } = await x.query(
        `SELECT * FROM recibos.selar_recibo($1, $2, $3::jsonb, $4, $5::smallint)`,
        [dados.seriesId, u.id, JSON.stringify({
          codigo_verificacao: codigoVerificacao,
          valor_centavos: dados.valorCentavos,
          valor_extenso: valorPorExtenso(dados.valorCentavos),
          descricao: dados.descricao,
          natureza: dados.natureza ?? null,
          forma_pagamento: dados.formaPagamento,
          data_pagamento: dados.dataPagamento,
          local_pagamento: dados.localPagamento,
          referencia: dados.referencia ?? null,
          credor_snapshot: credor,
          devedor_snapshot: devedor,
          emitente_papel: emitentePapel,
          counterparty_id: dados.counterpartyId ?? null,
          logo_asset_id: empresa.logo_asset_id ?? null,
          logo_sha256: empresa.logo_sha256 ?? null,
          assinatura_asset_id: dados.assinaturaAssetId ?? null,
          assinatura_sha256: payload.assinatura_sha256,
          signatario_nome: payload.signatario_nome,
          signatario_documento: dados.signatarioDocumento ?? null,
          external_source: dados.externalSource ?? null,
          external_ref: dados.externalRef ?? null,
        }), canonico, VERSAO_PAYLOAD],
      );

      const recibo = rows[0];

      // Se o número atribuído não for o que entrou no payload, o hash não
      // representa este documento. Aborta e devolve o número — melhor não
      // emitir do que emitir com hash que não fecha.
      if (recibo.numero_formatado !== numeroFormatado) {
        throw new Error(
          `corrida na numeração: payload assinado como ${numeroFormatado}, ` +
          `banco atribuiu ${recibo.numero_formatado}`,
        );
      }

      if (rascunhoId) {
        await x.query('DELETE FROM recibos.receipt_drafts WHERE id = $1', [rascunhoId]);
      }
      return { recibo, payload, canonico };
    });
  }

  async cancelar(u: UsuarioRef, receiptId: string, motivo: string) {
    exigir(u, 'cancelar');
    if (motivo.trim().length < 5) {
      throw new DadosInvalidos('O motivo do cancelamento precisa de ao menos 5 caracteres — ele fica no documento.');
    }
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        'SELECT * FROM recibos.cancelar_recibo($1, $2, $3)', [receiptId, motivo.trim(), u.id],
      );
      return rows[0];
    });
  }

  // ---------------- leitura (talão, detalhe) ----------------

  /** Talão: lista paginada, mais recente primeiro. RLS já restringe ao tenant. */
  async listar(u: UsuarioRef, filtros: FiltroRecibos = {}) {
    exigir(u, 'ler');
    return emTransacao(this.pool, async (x) => {
      const condicoes: string[] = [];
      const valores: unknown[] = [];
      if (filtros.seriesId) {
        valores.push(filtros.seriesId);
        condicoes.push(`r.series_id = $${valores.length}`);
      }
      if (filtros.direcao) {
        valores.push(filtros.direcao);
        condicoes.push(`r.direcao = $${valores.length}`);
      }
      if (filtros.busca?.trim()) {
        valores.push(`%${filtros.busca.trim()}%`);
        const p = valores.length;
        condicoes.push(
          `(r.numero_formatado ILIKE $${p} OR r.descricao ILIKE $${p} ` +
          `OR r.credor_snapshot ->> 'nome' ILIKE $${p} OR r.devedor_snapshot ->> 'nome' ILIKE $${p})`,
        );
      }
      const onde = condicoes.length ? `WHERE ${condicoes.join(' AND ')}` : '';
      const limite = Math.min(Math.max(filtros.limite ?? 50, 1), 200);
      valores.push(limite);

      const { rows } = await x.query(
        `SELECT r.id, r.numero_formatado, r.direcao, r.valor_centavos, r.descricao,
                r.data_pagamento, r.emitido_em, r.credor_snapshot, r.devedor_snapshot,
                r.emitente_papel, (rc.receipt_id IS NOT NULL) AS cancelado
           FROM recibos.receipts r
           LEFT JOIN recibos.receipt_cancellations rc ON rc.receipt_id = r.id
           ${onde}
           ORDER BY r.emitido_em DESC
           LIMIT $${valores.length}`,
        valores,
      );
      return rows;
    });
  }

  /** Detalhe: o recibo com status de cancelamento já resolvido. */
  async buscarPorId(u: UsuarioRef, id: string) {
    exigir(u, 'ler');
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `SELECT r.*, us.nome AS emitido_por_nome,
                (rc.receipt_id IS NOT NULL) AS cancelado,
                rc.motivo AS motivo_cancelamento, rc.cancelado_em, rc.cancelado_por
           FROM recibos.receipts r
           LEFT JOIN recibos.users us ON us.id = r.emitido_por
           LEFT JOIN recibos.receipt_cancellations rc ON rc.receipt_id = r.id
          WHERE r.id = $1`,
        [id],
      );
      if (!rows[0]) throw new NaoEncontrado('Recibo');
      return rows[0];
    });
  }

  /** "O que aconteceu" — a trilha de eventos que o painel de detalhe mostra. */
  async eventos(u: UsuarioRef, receiptId: string) {
    exigir(u, 'ler');
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `SELECT e.tipo, e.ocorrido_em, e.metadata, e.ip, us.nome AS usuario_nome
           FROM recibos.receipt_events e
           LEFT JOIN recibos.users us ON us.id = e.usuario_id
          WHERE e.receipt_id = $1
          ORDER BY e.ocorrido_em ASC`,
        [receiptId],
      );
      return rows;
    });
  }

  // ---------------- apoio ----------------

  private validar(d: DadosRascunho) {
    if (!Number.isInteger(d.valorCentavos) || d.valorCentavos <= 0) {
      throw new DadosInvalidos('O valor precisa ser um inteiro de centavos maior que zero.');
    }
    if (!d.descricao?.trim()) {
      throw new DadosInvalidos('A descrição é obrigatória: é ela que identifica a natureza da dívida quitada.');
    }
    if (d.descricao.trim().length > DESCRICAO_MAXIMA) {
      throw new DadosInvalidos(
        `A descrição tem ${d.descricao.trim().length} caracteres e o limite é ${DESCRICAO_MAXIMA}. ` +
        'Acima disso o texto não cabe na meia folha A4 e a via sai cortada na impressão.',
      );
    }
    if (!/^\d{4}-\d{2}-\d{2}$/.test(d.dataPagamento)) {
      throw new DadosInvalidos('Data do pagamento deve estar em AAAA-MM-DD.');
    }
    if (!d.localPagamento?.trim()) {
      throw new DadosInvalidos('O local do pagamento é obrigatório na quitação.');
    }
  }

  private async carregarSerie(x: Executor, id: string) {
    const { rows } = await x.query(
      'SELECT id, company_id, codigo, direcao, ativo FROM recibos.series WHERE id = $1', [id],
    );
    if (!rows[0]) throw new NaoEncontrado('Série');
    if (!rows[0].ativo) throw new DadosInvalidos(`A série ${rows[0].codigo} está inativa.`);
    return rows[0];
  }

  private async carregarEmpresa(x: Executor, id: string): Promise<Pessoa & any> {
    const { rows } = await x.query(
      `SELECT c.*, a.bytes_sha256 AS logo_sha256
         FROM recibos.companies c
         LEFT JOIN recibos.assets a ON a.id = c.logo_asset_id
        WHERE c.id = $1`, [id],
    );
    if (!rows[0]) throw new NaoEncontrado('Empresa');
    const r = rows[0];
    return Object.assign(r, {
      nome: r.razao_social,
      documento: r.cnpj_cpf,
      tipo: r.tipo_pessoa,
      endereco: [r.logradouro, r.numero, r.bairro, r.cidade, r.uf].filter(Boolean).join(', ') || null,
    });
  }

  private async carregarContraparte(x: Executor, id?: string | null): Promise<Pessoa> {
    if (!id) throw new DadosInvalidos('Selecione a contraparte antes de selar.');
    const { rows } = await x.query('SELECT * FROM recibos.counterparties WHERE id = $1', [id]);
    if (!rows[0]) throw new NaoEncontrado('Contraparte');
    const r = rows[0];
    return {
      nome: r.nome_razao_social,
      documento: r.cpf_cnpj,
      tipo: r.tipo_pessoa,
      endereco: [r.logradouro, r.numero, r.bairro, r.cidade, r.uf].filter(Boolean).join(', ') || null,
    };
  }

  private async hashDoAsset(x: Executor, id: string): Promise<string | null> {
    const { rows } = await x.query('SELECT bytes_sha256 FROM recibos.assets WHERE id = $1', [id]);
    return rows[0]?.bytes_sha256 ?? null;
  }
}
