import { Injectable, NotFoundException } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import type {
  ActivityStatus,
  ClientDetail,
  ClientListQuery,
  ClientListResponse,
  ClientNovoResult,
  ClientSummary,
  Contato,
  ContatoResult,
  CtrSummary,
  CtrTitulo,
  CreateClientNovo,
  CreateContato,
  NotaFiscal,
  PedidoSummary,
  TopProduto,
} from '@sar/api-interface';
import type { WorkspaceClsStore } from '../workspace/workspace.types';
import { getTeamCodes } from '../workspace/team-scope.util';

// Thresholds de atividade (FR-2.3). Configuráveis por empresa futuramente.
const ALERT_DAYS = 30;
const INACTIVE_DAYS = 60;

// Usado apenas por findOne (já tem dt_ultima_compra calculado pelo SQL)
function activityStatus(dtUltimaCompra: Date | null): ActivityStatus {
  if (!dtUltimaCompra) return 'inactive';
  const days = Math.floor((Date.now() - dtUltimaCompra.getTime()) / 86_400_000);
  if (days >= INACTIVE_DAYS) return 'inactive';
  if (days >= ALERT_DAYS) return 'alert';
  return 'active';
}

function escSql(s: string): string {
  return s.replace(/'/g, "''");
}

// Financeiro (CTR) e NF vivem na empresa MATRIZ. O ERP usa códigos > 9000 para
// origem de pedido (ex.: 9001 → matriz 1), espelhando catalog/dashboard/equipe.
function matrizEmpresa(idEmpresa: number): number {
  return idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa;
}

// Row bruta do $queryRawUnsafe
interface ClientRow {
  id_cliente: number;
  id_empresa: number;
  nome: string;
  razao: string | null;
  cgcpf: string | null;
  email: string | null;
  telefone: string | null;
  cod_vendedor: number;
  nome_vendedor: string | null;
  limite_credito: string | null;
  dt_ultima_compra: Date | null;
  ativo: number;
  pessoa: number | null;
  inscricao_estadual: string | null;
  endereco: string | null;
  num_endereco: string | null;
  bairro: string | null;
  cep: string | null;
  ddd: string | null;
  obs: string | null;
  cod_pauta: number | null;
  dt_cadastro: string | null;
  dt_atual: string | null;
}

// SQL compartilhado: dois subqueries que calculam a data do último pedido
// considerando TANTO pedidos ERP (vw_pedidos_erp) QUANTO pedidos SAR (tabela pedidos).
// vw_pedidos_erp: situa SIG 5=Cancelado (excluir); pedidos SAR: situa 3=Cancelado (excluir).
// Clientes são cadastro GLOBAL (sem vínculo de id_empresa). A "última compra",
// porém, é escopada à empresa atual: filtramos os pedidos por idEmpresa e juntamos
// apenas por id_cliente.
function pedidosJoins(idEmpresa: number): string {
  return `
  LEFT JOIN (
    SELECT id_cliente, MAX(dt_pedido) AS dt_max
    FROM vw_pedidos_erp
    WHERE situa NOT IN (5) AND id_empresa = ${idEmpresa}
    GROUP BY id_cliente
  ) erp_ped ON erp_ped.id_cliente = c.id_cliente
  LEFT JOIN (
    SELECT id_cliente, MAX(dt_pedido) AS dt_max
    FROM pedidos
    WHERE situa != 3 AND id_empresa = ${idEmpresa}
    GROUP BY id_cliente
  ) sar_ped ON sar_ped.id_cliente = c.id_cliente
  `;
}

// Subquery escalar para o nome do representante (cadastro global, sem id_empresa).
// NÃO usar JOIN: vw_representantes tem códigos duplicados, o que multiplicaria as
// linhas de cliente e quebraria contagem/paginação. LIMIT 1 garante 1 nome.
const NOME_VENDEDOR_SUBQ = `
  (SELECT r.nome FROM vw_representantes r
    WHERE r.codigo = c.cod_vendedor
    LIMIT 1) AS nome_vendedor`;

// Expressão SQL que calcula o activity_status a partir das datas dos dois joins.
const ACTIVITY_CASE = (alias_erp = 'erp_ped', alias_sar = 'sar_ped') => `
  CASE
    WHEN GREATEST(${alias_erp}.dt_max, ${alias_sar}.dt_max) IS NULL THEN 'inactive'
    WHEN (CURRENT_DATE - GREATEST(${alias_erp}.dt_max, ${alias_sar}.dt_max)::date) >= ${INACTIVE_DAYS} THEN 'inactive'
    WHEN (CURRENT_DATE - GREATEST(${alias_erp}.dt_max, ${alias_sar}.dt_max)::date) >= ${ALERT_DAYS} THEN 'alert'
    ELSE 'active'
  END
`;

@Injectable()
export class ClientsService {
  constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}

  // PGD-AUTHZ: rep só acessa clientes da própria carteira; supervisor acessa as
  // carteiras da sua equipe (cod_supervisor); gerente/admin passam direto.
  // NotFound (não Forbidden) para não revelar a existência de clientes de terceiros.
  private async assertClienteDaCarteira(idCliente: number): Promise<void> {
    const role = this.cls.get('role') ?? 'rep';
    if (role !== 'rep' && role !== 'supervisor') return;
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const userId = this.cls.get('userId');
    const codVendedor = userId ? parseInt(userId, 10) : 0;

    const rows = await prisma.$queryRawUnsafe<{ cod_vendedor: number }[]>(
      `SELECT cod_vendedor FROM vw_clientes WHERE id_cliente = $1 LIMIT 1`,
      idCliente,
    );
    const dono = rows[0] ? Number(rows[0].cod_vendedor) : null;
    const permitidos =
      role === 'supervisor' ? await getTeamCodes(prisma, codVendedor) : [codVendedor];
    if (dono == null || !permitidos.includes(dono)) {
      throw new NotFoundException(`Cliente ${idCliente} não encontrado`);
    }
  }

  async list(query: ClientListQuery): Promise<ClientListResponse> {
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = this.cls.get('idEmpresa');
    const role = this.cls.get('role');
    const userId = this.cls.get('userId');
    const codVendedor = userId ? parseInt(userId, 10) : 0;

    const { q, status, page, limit } = query;
    const offset = (page - 1) * limit;

    // Rep vê apenas sua carteira; supervisor vê as carteiras da equipe
    const vendedorFilter =
      role === 'rep'
        ? `AND c.cod_vendedor = ${codVendedor}`
        : role === 'supervisor'
          ? `AND c.cod_vendedor IN (${(await getTeamCodes(prisma, codVendedor)).join(',')})`
          : '';
    const searchFilter = q
      ? `AND (c.nome ILIKE '%${escSql(q)}%' OR c.cgcpf LIKE '%${escSql(q)}%')`
      : '';

    // Filtro de status calculado em SQL — evita paginação quebrada do filtro pós-SQL
    const statusFilter = status ? `AND ${ACTIVITY_CASE()} = '${status}'` : '';

    // Clientes globais: sem filtro de id_empresa. Rep continua escopado por cod_vendedor.
    const baseWhere = `
      WHERE c.ativo = 1
        ${vendedorFilter}
        ${searchFilter}
        ${statusFilter}
    `;
    const joins = pedidosJoins(idEmpresa);

    const [rows, totalRows] = await Promise.all([
      prisma.$queryRawUnsafe<ClientRow[]>(`
        SELECT
          c.id_cliente,
          c.id_empresa,
          c.nome,
          c.razao,
          c.cgcpf,
          c.email,
          c.telefone,
          c.cod_vendedor,
          ${NOME_VENDEDOR_SUBQ},
          c.limite_credito::text,
          c.ativo,
          c.pessoa,
          c.inscricao_estadual,
          c.endereco,
          c.num_endereco,
          c.bairro,
          c.cep,
          c.ddd,
          c.obs,
          c.cod_pauta,
          c.dt_cadastro::text,
          c.dt_atual::text,
          GREATEST(erp_ped.dt_max, sar_ped.dt_max) AS dt_ultima_compra
        FROM vw_clientes c
        ${joins}
        ${baseWhere}
        ORDER BY c.nome
        LIMIT ${limit} OFFSET ${offset}
      `),
      prisma.$queryRawUnsafe<[{ count: string }]>(`
        SELECT COUNT(*)::text AS count
        FROM vw_clientes c
        ${joins}
        ${baseWhere}
      `),
    ]);

    const total = parseInt(totalRows[0]?.count ?? '0', 10);

    const mapped: ClientSummary[] = rows.map((r) => ({
      idCliente: Number(r.id_cliente),
      idEmpresa: Number(r.id_empresa),
      nome: r.nome,
      razao: r.razao,
      cgcpf: r.cgcpf,
      email: r.email,
      telefone: r.telefone,
      codVendedor: Number(r.cod_vendedor),
      nomeVendedor: r.nome_vendedor ?? null,
      limiteCreditoStr: r.limite_credito,
      activityStatus: activityStatus(r.dt_ultima_compra),
      dtUltimaCompra: r.dt_ultima_compra?.toISOString() ?? null,
    }));

    return { data: mapped, total, page, limit };
  }

  async createNovo(dto: CreateClientNovo): Promise<ClientNovoResult> {
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = this.cls.get('idEmpresa');
    const userId = this.cls.get('userId');
    const codVendedor = userId ? parseInt(userId, 10) : 0;

    interface Row {
      id: string;
      id_corrent_erp: number | null;
      sincronizado: boolean;
      erro_sync: string | null;
    }

    const esc = (s: string) => s.replace(/'/g, "''");
    const opt = (v: string | undefined) => (v != null ? `'${esc(v)}'` : 'NULL');
    const optN = (v: number | undefined) => (v != null ? String(v) : 'NULL');

    // INSERT retorna id — RETURNING lê valores pré-trigger, então
    // fazemos SELECT separado para capturar o que o trigger gravou
    const insertRows = await prisma.$queryRawUnsafe<[{ id: string }]>(`
      INSERT INTO sar.clientes_novos (
        id_empresa, cod_vendedor, pesso, consfinal,
        nome, razao, cgcpf, suf_cgcpf, inscr, indicador_ie,
        endereco, num_endereco, bairro, id_municipio, cep,
        ddd, telefone, email, limite_credito, cod_formapag, cod_pauta
      ) VALUES (
        ${idEmpresa}, ${codVendedor}, ${dto.pesso}, ${dto.consfinal},
        '${esc(dto.nome)}', '${esc(dto.razao)}',
        ${opt(dto.cgcpf)}, '${esc(dto.sufCgcpf ?? '')}',
        '${esc(dto.inscr ?? '')}', ${optN(dto.indicadorIe)},
        '${esc(dto.endereco ?? '')}', '${esc(dto.numEndereco ?? '')}',
        '${esc(dto.bairro ?? '')}', ${dto.idMunicipio}, '${esc(dto.cep ?? '')}',
        '${esc(dto.ddd ?? '')}', '${esc(dto.telefone ?? '')}',
        '${esc(dto.email ?? '')}', ${dto.limiteCredito ?? 0},
        ${optN(dto.codFormapag)}, ${optN(dto.codPauta)}
      )
      RETURNING id::text
    `);

    const id = insertRows[0]?.id;
    if (!id) throw new Error('Falha ao inserir cliente');

    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT id::text, id_corrent_erp, sincronizado, erro_sync
      FROM sar.clientes_novos WHERE id = '${id}'
    `);

    const r = rows[0];
    if (!r) throw new Error('Falha ao recuperar cliente inserido');

    return {
      id: r.id,
      idCorrentErp: r.id_corrent_erp !== null ? Number(r.id_corrent_erp) : null,
      sincronizado: r.sincronizado,
      erroSync: r.erro_sync,
    };
  }

  async listContacts(idCorrent: number): Promise<Contato[]> {
    await this.assertClienteDaCarteira(idCorrent);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');

    interface Row {
      id_contato: number;
      id_empresa: number;
      id_corrent: number;
      id_chatwoot: number | null;
      nome: string;
      empresa: string | null;
      cargo: string | null;
      departamento: string | null;
      dt_aniversario: string | null;
      telefone: string | null;
      ramal: string | null;
      celular: string | null;
      whatsapp: string | null;
      email: string | null;
      ativo: number;
      anotacoes: string | null;
      nome_cliente: string | null;
      razao_cliente: string | null;
      cod_vendedor: number | null;
    }

    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT id_contato, id_empresa, id_corrent, id_chatwoot,
             nome, empresa, cargo, departamento,
             dt_aniversario::text, telefone, ramal, celular, whatsapp, email,
             ativo, anotacoes, nome_cliente, razao_cliente, cod_vendedor
      FROM sar.vw_contatos
      WHERE id_corrent = ${idCorrent} AND ativo = 1
      ORDER BY nome
    `);

    return rows.map((r) => ({
      idContato: Number(r.id_contato),
      idEmpresa: Number(r.id_empresa),
      idCorrent: Number(r.id_corrent),
      idChatwoot: r.id_chatwoot !== null ? Number(r.id_chatwoot) : null,
      nome: r.nome,
      empresa: r.empresa,
      cargo: r.cargo,
      departamento: r.departamento,
      dtAniversario: r.dt_aniversario,
      telefone: r.telefone,
      ramal: r.ramal,
      celular: r.celular,
      whatsapp: r.whatsapp,
      email: r.email,
      ativo: Number(r.ativo),
      anotacoes: r.anotacoes,
      nomeCliente: r.nome_cliente,
      razaoCliente: r.razao_cliente,
      codVendedor: r.cod_vendedor !== null ? Number(r.cod_vendedor) : null,
    }));
  }

  async createContato(idCorrent: number, dto: CreateContato): Promise<ContatoResult> {
    await this.assertClienteDaCarteira(idCorrent);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = this.cls.get('idEmpresa');
    const userId = this.cls.get('userId');
    const codVendedor = userId ? parseInt(userId, 10) : 0;

    interface Row {
      id: string;
    }
    const esc = (s: string) => s.replace(/'/g, "''");
    const opt = (v: string | undefined) => (v ? `'${esc(v)}'` : 'NULL');

    const insertRows = await prisma.$queryRawUnsafe<Row[]>(`
      INSERT INTO sar.contatos_novos (
        id_empresa, cod_vendedor, id_corrent,
        nome, empresa, cargo, departamento,
        dt_aniversario, telefone, ramal, celular, whatsapp, email, anotacoes
      ) VALUES (
        ${idEmpresa}, ${codVendedor}, ${idCorrent},
        '${esc(dto.nome)}',
        ${opt(dto.empresa)}, ${opt(dto.cargo)}, ${opt(dto.departamento)},
        ${dto.dtAniversario ? `'${esc(dto.dtAniversario)}'` : 'NULL'},
        ${opt(dto.telefone)}, ${opt(dto.ramal)}, ${opt(dto.celular)},
        ${opt(dto.whatsapp)}, ${opt(dto.email)}, ${opt(dto.anotacoes)}
      )
      RETURNING id::text
    `);

    const id = insertRows[0]?.id;
    if (!id) throw new Error('Falha ao inserir contato');

    interface ResultRow {
      id: string;
      id_contato_erp: number | null;
      sincronizado: boolean;
      erro_sync: string | null;
    }
    const rows = await prisma.$queryRawUnsafe<ResultRow[]>(`
      SELECT id::text, id_contato_erp, sincronizado, erro_sync
      FROM sar.contatos_novos WHERE id = '${id}'
    `);

    const r = rows[0];
    if (!r) throw new Error('Falha ao recuperar contato inserido');

    return {
      id: r.id,
      idContatoErp: r.id_contato_erp !== null ? Number(r.id_contato_erp) : null,
      sincronizado: r.sincronizado,
      erroSync: r.erro_sync,
    };
  }

  async listOrdersHistory(idCliente: number, limit: number): Promise<PedidoSummary[]> {
    await this.assertClienteDaCarteira(idCliente);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = this.cls.get('idEmpresa');

    interface Row {
      id: string;
      num_ped: string;
      situa: number;
      dt_pedido: Date;
      total: string;
      fonte: string;
    }

    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT id::text        AS id,
             num_ped_sar     AS num_ped,
             situa,
             dt_pedido,
             total::text,
             'sar'           AS fonte
      FROM sar.pedidos
      WHERE id_cliente = ${idCliente}
        AND id_empresa = ${idEmpresa}

      UNION ALL

      SELECT ('erp-' || id_pedido::text) AS id,
             COALESCE(NULLIF(TRIM(num_ped_sar::text),''), numero::text) AS num_ped,
             CASE WHEN situa = 5 THEN 3 ELSE situa END AS situa,
             dt_pedido,
             total::text,
             'erp'           AS fonte
      FROM sar.vw_pedidos_erp
      WHERE id_cliente = ${idCliente}
        AND id_empresa = ${idEmpresa}
        AND situa != 5

      ORDER BY dt_pedido DESC
      LIMIT ${limit}
    `);

    return rows.map((r) => ({
      id: r.id,
      numPedSar: r.num_ped ?? r.id,
      idCliente,
      nomeCliente: null,
      razaoCliente: null,
      codVendedor: 0,
      nomeVendedor: null,
      situa: Number(r.situa),
      dtPedido: r.dt_pedido.toISOString(),
      total: r.total ?? '0',
      descontoPerc: '0',
      obs: null,
      createdAt: r.dt_pedido.toISOString(),
      fonte: r.fonte as 'sar' | 'erp',
    }));
  }

  async listTopProdutos(idCliente: number, limit: number): Promise<TopProduto[]> {
    await this.assertClienteDaCarteira(idCliente);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = this.cls.get('idEmpresa');

    // Normaliza empresa fiscal (9001) → gerencial (1) onde ficam os produtos
    const idEmpresaMatriz = matrizEmpresa(idEmpresa);

    interface Row {
      id_produto: number;
      codigo: string;
      descricao: string;
      unidade: string | null;
      qtd_total: string;
      num_pedidos: string;
      ultima_compra: Date;
      ultimo_preco: string;
    }

    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT
        i.id_produto,
        TRIM(p.codigo)                                 AS codigo,
        TRIM(p.descricao)                              AS descricao,
        TRIM(p.unidade)                                AS unidade,
        SUM(i.qtd)::numeric(15,3)::text               AS qtd_total,
        COUNT(DISTINCT ped.id_pedido)::text            AS num_pedidos,
        MAX(ped.dt_pedido)                             AS ultima_compra,
        (
          SELECT i2.preco_unitario::numeric(15,2)::text
          FROM sar.vw_peditens_erp i2
          JOIN sar.vw_pedidos_erp  p2 ON p2.id_pedido = i2.id_pedido
          WHERE i2.id_produto  = i.id_produto
            AND p2.id_cliente  = ped.id_cliente
            AND p2.id_empresa  = ${idEmpresa}
            AND p2.situa      NOT IN (5)
          ORDER BY p2.dt_pedido DESC, p2.id_pedido DESC
          LIMIT 1
        )                                              AS ultimo_preco
      FROM sar.vw_pedidos_erp  ped
      JOIN sar.vw_peditens_erp i   ON i.id_pedido  = ped.id_pedido
      JOIN sar.vw_produtos     p   ON p.id_erp      = i.id_produto
                                  AND p.id_empresa  = ${idEmpresaMatriz}
      WHERE ped.id_cliente   = ${idCliente}
        AND ped.id_empresa   = ${idEmpresa}
        AND ped.situa       NOT IN (5)
      GROUP BY i.id_produto, p.codigo, p.descricao, p.unidade, ped.id_cliente
      ORDER BY SUM(i.qtd) DESC
      LIMIT ${limit}
    `);

    return rows.map((r) => ({
      idProduto: Number(r.id_produto),
      codProduto: r.codigo ?? '',
      descricao: r.descricao,
      unidade: r.unidade,
      qtdTotal: parseFloat(r.qtd_total ?? '0'),
      numPedidos: parseInt(r.num_pedidos ?? '0', 10),
      ultimaCompra: (r.ultima_compra as Date).toISOString().slice(0, 10),
      ultimoPreco: parseFloat(r.ultimo_preco ?? '0'),
    }));
  }

  async getCtrSummary(idCliente: number): Promise<CtrSummary> {
    await this.assertClienteDaCarteira(idCliente);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresaMatriz = matrizEmpresa(this.cls.get('idEmpresa'));

    interface Row {
      qtd_aberto: string;
      total_aberto: string;
      qtd_vencido: string;
      total_vencido: string;
      maior_atraso_dias: string;
    }

    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT
        COUNT(*)::text                                                                          AS qtd_aberto,
        COALESCE(SUM(saldo), 0)::numeric(15,2)::text                                          AS total_aberto,
        (COUNT(*) FILTER (WHERE dt_vencimento < CURRENT_DATE))::text                          AS qtd_vencido,
        COALESCE(SUM(saldo) FILTER (WHERE dt_vencimento < CURRENT_DATE), 0)::numeric(15,2)::text AS total_vencido,
        COALESCE(MAX(CURRENT_DATE - dt_vencimento) FILTER (WHERE dt_vencimento < CURRENT_DATE), 0)::text AS maior_atraso_dias
      FROM sar.vw_ctr
      WHERE id_cliente = ${idCliente}
        AND id_empresa = ${idEmpresaMatriz}
        AND situacao = 'A'
        AND saldo > 0
    `);

    const r = rows[0];
    return {
      qtdAberto: parseInt(r?.qtd_aberto ?? '0', 10),
      totalAberto: parseFloat(r?.total_aberto ?? '0'),
      qtdVencido: parseInt(r?.qtd_vencido ?? '0', 10),
      totalVencido: parseFloat(r?.total_vencido ?? '0'),
      maiorAtrasoDias: parseInt(r?.maior_atraso_dias ?? '0', 10),
    };
  }

  async findOne(idCliente: number): Promise<ClientDetail> {
    await this.assertClienteDaCarteira(idCliente);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = this.cls.get('idEmpresa');

    const rows = await prisma.$queryRawUnsafe<ClientRow[]>(`
      SELECT
        c.id_cliente, c.id_empresa, c.nome, c.razao, c.cgcpf, c.email,
        c.telefone, c.cod_vendedor, ${NOME_VENDEDOR_SUBQ}, c.limite_credito::text,
        c.ativo, c.pessoa, c.inscricao_estadual, c.endereco, c.num_endereco,
        c.bairro, c.cep, c.ddd, c.obs, c.cod_pauta,
        c.dt_cadastro::text, c.dt_atual::text,
        GREATEST(erp_ped.dt_max, sar_ped.dt_max) AS dt_ultima_compra
      FROM vw_clientes c
      ${pedidosJoins(idEmpresa)}
      WHERE c.id_cliente = ${idCliente}
      LIMIT 1
    `);

    const r = rows[0];
    if (!r) throw new NotFoundException(`Cliente ${idCliente} não encontrado`);

    return {
      idCliente: Number(r.id_cliente),

      idEmpresa: Number(r.id_empresa),
      nome: r.nome,
      razao: r.razao,
      cgcpf: r.cgcpf,
      email: r.email,
      telefone: r.telefone,
      codVendedor: Number(r.cod_vendedor),
      nomeVendedor: r.nome_vendedor ?? null,
      limiteCreditoStr: r.limite_credito,
      activityStatus: activityStatus(r.dt_ultima_compra),
      dtUltimaCompra: r.dt_ultima_compra?.toISOString() ?? null,
      ativo: Number(r.ativo),
      pessoa: r.pessoa !== null ? Number(r.pessoa) : null,
      inscricaoEstadual: r.inscricao_estadual,
      endereco: r.endereco,
      numEndereco: r.num_endereco,
      bairro: r.bairro,
      cep: r.cep,
      ddd: r.ddd,
      obs: r.obs,
      codPauta: r.cod_pauta !== null ? Number(r.cod_pauta) : null,
      dtCadastro: r.dt_cadastro,
      dtAtual: r.dt_atual,
    };
  }

  async listCtrTitulos(idCliente: number, limit: number): Promise<CtrTitulo[]> {
    await this.assertClienteDaCarteira(idCliente);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresaMatriz = matrizEmpresa(this.cls.get('idEmpresa'));

    type CtrRow = {
      id_ctr: number;
      numero: string;
      prefixo: string;
      dt_emissao: Date;
      dt_vencimento: Date;
      saldo: string;
    };

    const rows = await prisma.$queryRawUnsafe<CtrRow[]>(
      `
      SELECT
        id_ctr,
        TRIM(numero)     AS numero,
        TRIM(prefixo)    AS prefixo,
        dt_emissao,
        dt_vencimento,
        saldo::text      AS saldo
      FROM sar.vw_ctr
      WHERE id_cliente = $1
        AND id_empresa  = $3
        AND situacao    = 'A'
        AND saldo       > 0
      ORDER BY dt_vencimento ASC
      LIMIT $2
    `,
      idCliente,
      limit,
      idEmpresaMatriz,
    );

    const toDate = (d: Date | string): string =>
      d instanceof Date ? d.toISOString().slice(0, 10) : String(d).slice(0, 10);

    return rows.map((r: CtrRow) => ({
      idCtr: Number(r.id_ctr),
      numero: r.numero,
      prefixo: r.prefixo,
      dtEmissao: toDate(r.dt_emissao),
      dtVencimento: toDate(r.dt_vencimento),
      saldo: Number(r.saldo),
    }));
  }

  async listNotasFiscais(idCliente: number, limit: number): Promise<NotaFiscal[]> {
    await this.assertClienteDaCarteira(idCliente);
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    // NF fica na matriz; pedidos podem ter origem na matriz ou na empresa fiscal (matriz+9000)
    const idEmpresaMatriz = matrizEmpresa(this.cls.get('idEmpresa'));
    const idEmpresaFiscal = idEmpresaMatriz + 9000;

    type NfRow = {
      id_nf: number;
      numero: number;
      serie: string;
      dt_emissao: Date;
      vl_nf: string;
      chave_acesso_nfe: string | null;
    };

    const rows = await prisma.$queryRawUnsafe<NfRow[]>(
      `
      SELECT id_nf, numero, serie, dt_emissao, vl_nf, chave_acesso_nfe
      FROM (
        SELECT DISTINCT ON (nf.id_nf)
          nf.id_nf,
          nf.numero,
          TRIM(nf.serie)            AS serie,
          nf.dt_emissao,
          nf.vl_nf::text            AS vl_nf,
          TRIM(nf.chave_acesso_nfe) AS chave_acesso_nfe
        FROM sar.vw_nf nf
        JOIN sar.vw_pedidos_erp p
          ON p.numero = nf.num_entrega
         AND p.tipo = 'E'
         AND p.id_empresa IN ($3, $4)
        WHERE p.id_cliente = $1
          AND nf.id_empresa = $3
          AND nf.status = 'E'
          AND TRIM(COALESCE(nf.chave_acesso_nfe, '')) != ''
        ORDER BY nf.id_nf
      ) sub
      ORDER BY dt_emissao DESC
      LIMIT $2
    `,
      idCliente,
      limit,
      idEmpresaMatriz,
      idEmpresaFiscal,
    );

    return rows.map((r: NfRow) => ({
      idNf: Number(r.id_nf),
      numero: Number(r.numero),
      serie: r.serie?.trim() ?? '',
      dtEmissao:
        (r.dt_emissao instanceof Date
          ? r.dt_emissao.toISOString().split('T')[0]
          : String(r.dt_emissao)) ?? '',
      vlNf: Number(r.vl_nf),
      chaveAcessoNfe: r.chave_acesso_nfe?.trim() || null,
    }));
  }
}
