/* ============================================================
 * Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com
 * Fase 1 — empresas, séries e contrapartes
 * ============================================================ */

import { emTransacao, type Pool } from '../db/client.ts';
import { exigir, type UsuarioRef } from '../core/identity.ts';
import { DadosInvalidos, NaoEncontrado } from '../core/errors.ts';

export type Direcao = 'RECEBIMENTO' | 'PAGAMENTO';

export interface NovaEmpresa {
  razaoSocial: string;
  nomeFantasia?: string;
  tipoPessoa?: 'PF' | 'PJ';
  cnpjCpf: string;
  cidade?: string;
  uf?: string;
}

export interface NovaSerie {
  companyId: string;
  codigo: string;
  descricao?: string;
  direcao: Direcao;
  reservadaPara?: string;
}

export interface NovaContraparte {
  tipoPessoa: 'PF' | 'PJ';
  nomeRazaoSocial: string;
  cpfCnpj: string;
  cidade?: string;
  uf?: string;
  email?: string;
  telefone?: string;
  pisNit?: string;
  papeis?: string[];
}

/** Só os dígitos. Mesma normalização do índice único no banco. */
export function digitos(doc: string): string {
  return String(doc).replace(/\D/g, '');
}

/** Valida CPF/CNPJ pelos dígitos verificadores. */
export function documentoValido(doc: string): boolean {
  const d = digitos(doc);
  if (d.length === 11) return cpfValido(d);
  if (d.length === 14) return cnpjValido(d);
  return false;
}

function cpfValido(c: string): boolean {
  if (/^(\d)\1{10}$/.test(c)) return false;
  for (const tamanho of [9, 10]) {
    let soma = 0;
    for (let i = 0; i < tamanho; i++) soma += Number(c[i]) * (tamanho + 1 - i);
    const dv = (soma * 10) % 11 % 10;
    if (dv !== Number(c[tamanho])) return false;
  }
  return true;
}

function cnpjValido(c: string): boolean {
  if (/^(\d)\1{13}$/.test(c)) return false;
  const calcular = (base: string, pesos: number[]) => {
    const soma = base.split('').reduce((s, n, i) => s + Number(n) * pesos[i], 0);
    const r = soma % 11;
    return r < 2 ? 0 : 11 - r;
  };
  const dv1 = calcular(c.slice(0, 12), [5,4,3,2,9,8,7,6,5,4,3,2]);
  const dv2 = calcular(c.slice(0, 13), [6,5,4,3,2,9,8,7,6,5,4,3,2]);
  return dv1 === Number(c[12]) && dv2 === Number(c[13]);
}

export class CadastrosService {
  constructor(
    private readonly pool: Pool,
    /** Vem da licença (limites.empresas). Sem provider, não trava. */
    private readonly limiteEmpresas: () => number | Promise<number> = () => Number.MAX_SAFE_INTEGER,
  ) {}

  /**
   * Só quem administra estrutura cria empresa — mesmo gate de criarSerie.
   *
   * Empresa NÃO trava no limite do plano — série sim é de graça, empresa
   * muda o contrato (decisão do Rafael, 15/09/2026, ver docs/decisoes.md
   * §17). Passar do contratado não impede nada: fica sinalizado
   * (`acimaDoPlano`) para a cobrança proporcional acontecer do lado do
   * Klavo e o cliente ser avisado — nenhuma das duas coisas é feita
   * daqui (reportar uso ao Klavo é fase 7; notificar por e-mail é fase 8,
   * nenhuma das duas está implementada ainda).
   */
  async criarEmpresa(u: UsuarioRef, dados: NovaEmpresa) {
    exigir(u, 'gerir_series');
    if (!documentoValido(dados.cnpjCpf)) {
      throw new DadosInvalidos(`O documento "${dados.cnpjCpf}" não passa na checagem dos dígitos verificadores.`);
    }
    if (!dados.razaoSocial?.trim()) {
      throw new DadosInvalidos('Razão social é obrigatória.');
    }
    const tipoReal = digitos(dados.cnpjCpf).length === 11 ? 'PF' : 'PJ';

    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `INSERT INTO recibos.companies
           (tenant_id, razao_social, nome_fantasia, tipo_pessoa, cnpj_cpf, cidade, uf)
         VALUES (current_setting('app.tenant_id')::uuid, $1,$2,$3,$4,$5,$6)
         RETURNING *`,
        [dados.razaoSocial.trim(), dados.nomeFantasia?.trim() ?? null, dados.tipoPessoa ?? tipoReal,
         dados.cnpjCpf, dados.cidade ?? null, dados.uf ?? null],
      );
      const empresa = rows[0];

      const { rows: contagem } = await x.query(
        `SELECT count(*)::int AS n FROM recibos.companies WHERE ativo`,
      );
      const contratadas = await this.limiteEmpresas();
      const totalAtivas = contagem[0].n;
      const acimaDoPlano = totalAtivas > contratadas;

      return { empresa, acimaDoPlano, contratadas, totalAtivas };
    });
  }

  async criarSerie(u: UsuarioRef, dados: NovaSerie) {
    exigir(u, 'gerir_series');

    const codigo = dados.codigo.trim().toUpperCase();
    if (!/^[A-Z0-9]{1,4}$/.test(codigo)) {
      throw new DadosInvalidos('O código da série usa de 1 a 4 letras ou números, sem espaço.');
    }
    if (dados.direcao !== 'RECEBIMENTO' && dados.direcao !== 'PAGAMENTO') {
      throw new DadosInvalidos('Direção precisa ser RECEBIMENTO ou PAGAMENTO.');
    }

    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `INSERT INTO recibos.series
           (tenant_id, company_id, codigo, descricao, direcao, reservada_para)
         VALUES (current_setting('app.tenant_id')::uuid, $1, $2, $3, $4, $5)
         RETURNING *`,
        [dados.companyId, codigo, dados.descricao ?? null, dados.direcao, dados.reservadaPara ?? null],
      );
      return rows[0];
    });
  }

  /**
   * A direção só pode ser corrigida enquanto a série não emitiu nada.
   * Depois disso o banco recusa (trigger trg_series_protegida); aqui a
   * checagem existe para dar uma mensagem melhor que a do Postgres.
   */
  async corrigirDirecao(u: UsuarioRef, seriesId: string, direcao: Direcao) {
    exigir(u, 'gerir_series');
    return emTransacao(this.pool, async (x) => {
      const { rows: emitidos } = await x.query(
        'SELECT count(*)::int AS n FROM recibos.receipts WHERE series_id = $1', [seriesId],
      );
      if (emitidos[0].n > 0) {
        throw new DadosInvalidos(
          `Esta série já emitiu ${emitidos[0].n} documento(s), então a direção não muda mais. ` +
          'Crie uma série nova com a direção correta e desative esta.',
        );
      }
      const { rows } = await x.query(
        'UPDATE recibos.series SET direcao = $2 WHERE id = $1 RETURNING *', [seriesId, direcao],
      );
      if (!rows[0]) throw new NaoEncontrado('Série');
      return rows[0];
    });
  }

  async criarContraparte(u: UsuarioRef, dados: NovaContraparte) {
    exigir(u, 'gerir_cadastros');

    if (!documentoValido(dados.cpfCnpj)) {
      throw new DadosInvalidos(`O documento "${dados.cpfCnpj}" não passa na checagem dos dígitos verificadores.`);
    }
    const d = digitos(dados.cpfCnpj);
    const tipoReal = d.length === 11 ? 'PF' : 'PJ';
    if (tipoReal !== dados.tipoPessoa) {
      throw new DadosInvalidos(
        `Marcado como ${dados.tipoPessoa}, mas o documento tem ${d.length} dígitos, o que é ${tipoReal}.`,
      );
    }

    try {
      return await emTransacao(this.pool, async (x) => {
        const { rows } = await x.query(
          `INSERT INTO recibos.counterparties
             (tenant_id, tipo_pessoa, nome_razao_social, cpf_cnpj, cidade, uf, email, telefone, pis_nit, papeis)
           VALUES (current_setting('app.tenant_id')::uuid, $1,$2,$3,$4,$5,$6,$7,$8,$9)
           RETURNING *`,
          [dados.tipoPessoa, dados.nomeRazaoSocial.trim(), dados.cpfCnpj,
           dados.cidade ?? null, dados.uf ?? null, dados.email ?? null,
           dados.telefone ?? null, dados.pisNit ?? null, dados.papeis ?? []],
        );
        return rows[0];
      });
    } catch (erro: any) {
      // 23505 = violação do índice único por tenant + dígitos do documento
      if (erro?.code === '23505' && String(erro.constraint).includes('documento_unico')) {
        const jaExiste = await this.porDocumento(dados.cpfCnpj);
        throw new DadosInvalidos(
          `O documento ${dados.cpfCnpj} já está cadastrado` +
          (jaExiste ? ` para ${jaExiste.nome_razao_social}` : '') +
          '. Use a busca para localizar a contraparte em vez de cadastrar de novo.',
        );
      }
      throw erro;
    }
  }

  async buscarContrapartes(termo: string, limite = 20) {
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `SELECT id, tipo_pessoa, nome_razao_social, cpf_cnpj, cidade, uf
           FROM recibos.counterparties
          WHERE ativo
            AND (nome_razao_social ILIKE $1 OR regexp_replace(cpf_cnpj,'\\D','','g') LIKE $2)
          ORDER BY nome_razao_social
          LIMIT $3`,
        [`%${termo}%`, `%${digitos(termo)}%`, limite],
      );
      return rows;
    });
  }

  /** Busca exata pelo documento, ignorando pontuação. */
  async porDocumento(doc: string) {
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `SELECT * FROM recibos.counterparties
          WHERE regexp_replace(cpf_cnpj, '\\D', '', 'g') = $1`,
        [digitos(doc)],
      );
      return rows[0] ?? null;
    });
  }

  /**
   * Cadastro rápido, de dentro do rascunho: o CPF/CNPJ é a chave.
   *
   * Busca antes de inserir. Documento já cadastrado devolve a pessoa em vez
   * de duplicar — é o que faz "localizar pelo CPF" ter sentido. Contraparte
   * inativa é reativada, nunca recriada: os recibos já emitidos apontam para
   * o cadastro original e o histórico precisa continuar ligado.
   */
  async localizarOuCriar(u: UsuarioRef, dados: NovaContraparte):
      Promise<{ contraparte: any; criada: boolean; reativada: boolean }> {
    exigir(u, 'gerir_cadastros');

    if (!documentoValido(dados.cpfCnpj)) {
      throw new DadosInvalidos(`O documento "${dados.cpfCnpj}" não passa na checagem dos dígitos verificadores.`);
    }

    const existente = await this.porDocumento(dados.cpfCnpj);
    if (existente) {
      if (existente.ativo) return { contraparte: existente, criada: false, reativada: false };
      const reativada = await emTransacao(this.pool, async (x) => {
        const { rows } = await x.query(
          `UPDATE recibos.counterparties SET ativo = true, atualizado_em = now()
            WHERE id = $1 RETURNING *`, [existente.id],
        );
        return rows[0];
      });
      return { contraparte: reativada, criada: false, reativada: true };
    }

    return { contraparte: await this.criarContraparte(u, dados), criada: true, reativada: false };
  }

  async empresa(companyId: string) {
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query('SELECT * FROM recibos.companies WHERE id = $1', [companyId]);
      if (!rows[0]) throw new NaoEncontrado('Empresa');
      return rows[0];
    });
  }

  /**
   * MVP: um tenant, uma empresa — a tela de emissão não pede pra escolher.
   * Quando isso deixar de valer (multiempresa por tenant), isto vira uma
   * lista com seleção; até lá, a mais antiga é a única que importa.
   */
  async minhaEmpresa() {
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query('SELECT * FROM recibos.companies ORDER BY criado_em LIMIT 1');
      if (!rows[0]) throw new NaoEncontrado('Empresa');
      return rows[0];
    });
  }

  /**
   * Série pro seletor de emissão. Sem companyId, lista as de todas as
   * empresas do tenant — RLS já cuida do isolamento entre tenants.
   * Emissor só vê as séries liberadas pra ele; os outros papéis veem tudo
   * (é a mesma regra de podeEmitirNaSerie, só que pra decidir o que listar).
   */
  async listarSeries(u: UsuarioRef, companyId?: string) {
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `SELECT * FROM recibos.series
          WHERE ativo AND ($1::uuid IS NULL OR company_id = $1)
          ORDER BY codigo`,
        [companyId ?? null],
      );
      if (u.papel === 'PROPRIETARIO') return rows;
      return rows.filter((s: { id: string }) => u.seriesPermitidas.includes(s.id));
    });
  }
}
