import { ForbiddenException, Injectable } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import type {
  EmpresaInfo,
  UpdateLogoBody,
  FormaPagamento,
  Municipio,
  Pauta,
  ProdutoDetail,
  ProdutoListQuery,
  ProdutoListResponse,
  ProdutoSummary,
} from '@sar/api-interface';
import type { WorkspaceClsStore } from '../workspace/workspace.types';

// ADR 0006 revogado: produtos lidos diretamente de vw_produtos (ERP) + vw_estoque.
// Sem sync — dados sempre frescos da view do ERP.

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

// Produtos, pautas e estoque são por-empresa e vivem na MATRIZ. O ERP usa códigos
// de empresa > 9000 para origem de pedido (ex.: 9001), mas o cadastro fica na
// matriz correspondente (9001 → 1), espelhando o CASE de vw_pedidos_erp.
// Pedidos continuam usando idEmpresa cru; só o catálogo normaliza.
function matrizEmpresa(idEmpresa: number): number {
  return idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa;
}

interface ProdutoRow {
  id_erp: number;
  codigo: string;
  descricao: string;
  unidade: string | null;
  vl_preco1: string;
  cod_grupo: number | null;
  grupo: string | null;
  cod_subgrupo: number | null;
  subgrupo: string | null;
  marca: string | null;
  ativo: number;
  qtd_estoque: string | null;
  lista_pauta: number | null;
  referencia: string | null;
  descr_det: string | null;
  vl_preco2: string | null;
  vl_preco3: string | null;
  aliq_ipi: string | null;
  peso_liquido: string | null;
  qtd_volume: string | null;
  lote_mul_venda: number | null;
  preco_promocional: string | null;
}

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

  // Dados legais da empresa matriz que fatura o pedido (cabeçalho do PDF).
  async company(): Promise<EmpresaInfo> {
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));

    interface Row {
      id_empresa: number;
      razao_social: string | null;
      nome_fantasia: string | null;
      cnpj: string | null;
      inscr_estadual: string | null;
      endereco: string | null;
      numero: string | null;
      complemento: string | null;
      bairro: string | null;
      cidade: string | null;
      uf: string | null;
      cep: string | null;
      telefone: string | null;
      email: string | null;
    }
    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT e.id_empresa,
             TRIM(e.razao_social) AS razao_social,
             TRIM(e.nome)         AS nome_fantasia,
             TRIM(e.cnpj)         AS cnpj,
             TRIM(e.inscr_estadual) AS inscr_estadual,
             TRIM(e.endereco)       AS endereco,
             e.num_endereco::text   AS numero,
             e.complemento,
             TRIM(e.bairro)         AS bairro,
             TRIM(m.nome)           AS cidade,
             TRIM(e.uf)             AS uf,
             e.cep,
             e.telefone,
             TRIM(e.email)          AS email
      FROM sar.vw_empresas e
      LEFT JOIN sar.vw_municipios m ON m.id_municipio = e.id_municipio
      WHERE e.id_empresa = ${idEmpresa}
      LIMIT 1
    `);
    const r = rows[0];
    if (!r) throw new Error(`Empresa matriz ${idEmpresa} não encontrada`);

    const config = await prisma.configEmpresa.findUnique({ where: { idEmpresa } });

    const clean = (v: string | null) => {
      const t = (v ?? '').trim();
      return t === '' ? null : t;
    };
    return {
      idEmpresa: Number(r.id_empresa),
      razaoSocial: clean(r.razao_social) ?? clean(r.nome_fantasia) ?? `Empresa ${r.id_empresa}`,
      nomeFantasia: clean(r.nome_fantasia),
      cnpj: clean(r.cnpj),
      inscricaoEstadual: clean(r.inscr_estadual),
      endereco: clean(r.endereco),
      numero: clean(r.numero),
      complemento: clean(r.complemento),
      bairro: clean(r.bairro),
      cidade: clean(r.cidade),
      uf: clean(r.uf),
      cep: clean(r.cep),
      telefone: clean(r.telefone),
      email: clean(r.email),
      logoBase64: config?.logoBase64 ?? null,
    };
  }

  // Logo do cabeçalho da impressão — só gerente/admin altera; vale para todos
  // os representantes da empresa (gravado na matriz, mesma fonte do company()).
  async updateLogo(body: UpdateLogoBody): Promise<void> {
    const role = this.cls.get('role') ?? 'rep';
    if (role !== 'manager' && role !== 'admin') {
      throw new ForbiddenException('Apenas gerentes podem alterar o logo da empresa');
    }
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));

    await prisma.configEmpresa.upsert({
      where: { idEmpresa },
      create: { idEmpresa, logoBase64: body.logoBase64 },
      update: { logoBase64: body.logoBase64 },
    });
  }

  async municipios(q?: string): Promise<Municipio[]> {
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');

    interface Row {
      id_municipio: number;
      nome: string;
      uf: string;
      codigo_ibge: number | null;
    }

    const searchFilter = q ? `WHERE UPPER(nome) LIKE UPPER('%${escSql(q)}%')` : '';

    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT id_municipio, TRIM(nome) AS nome, uf::text AS uf, codigo_ibge
      FROM sar.vw_municipios
      ${searchFilter}
      ORDER BY nome
      LIMIT 100
    `);

    return rows.map((r) => ({
      idMunicipio: Number(r.id_municipio),
      nome: r.nome,
      uf: r.uf,
      codigoIbge: r.codigo_ibge !== null ? Number(r.codigo_ibge) : null,
    }));
  }

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

    interface PautaRow {
      id_pauta: number;
      codigo: number;
      descricao: string;
    }
    const rows = await prisma.$queryRawUnsafe<PautaRow[]>(`
      SELECT DISTINCT pa.id_pauta, pa.codigo, TRIM(pa.descricao) AS descricao
      FROM vw_pautas pa
      JOIN vw_representantes r ON pa.codigo IN (
          r.cod_pauta1, r.cod_pauta2, r.cod_pauta3,
          r.cod_pauta4, r.cod_pauta5, r.cod_pauta6
        )
      WHERE pa.id_empresa = ${idEmpresa}
        AND pa.ativo = 1
        AND r.codigo = ${codVendedor}
      ORDER BY pa.codigo
    `);

    return rows.map((r) => ({
      idPauta: Number(r.id_pauta),
      codigo: Number(r.codigo),
      descricao: r.descricao,
    }));
  }

  async list(query: ProdutoListQuery): Promise<ProdutoListResponse> {
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));

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

    const grupoFilter = codGrupo != null ? `AND p.cod_grupo = ${codGrupo}` : '';
    const searchFilter = q
      ? `AND (p.descricao ILIKE '%${escSql(q)}%' OR p.codigo ILIKE '%${escSql(q)}%')`
      : '';

    // Com pauta: usa preço específico da pauta. Sem pauta: filtra vl_preco1 > 0.
    if (idPauta != null) {
      interface PautaItemRow extends ProdutoRow {
        preco_pauta: string;
      }
      const [rows, countRows] = await Promise.all([
        prisma.$queryRawUnsafe<PautaItemRow[]>(`
          SELECT
            p.id_erp, p.codigo, p.descricao, p.unidade,
            pp.preco1::text AS preco_pauta,
            p.cod_grupo, p.grupo, p.cod_subgrupo, p.subgrupo, p.marca,
            p.ativo, e.qtd_estoque::text, p.lista_pauta,
            p.referencia, p.descr_det, p.vl_preco2::text, p.vl_preco3::text,
            p.aliq_ipi::text, p.peso_liquido::text, p.qtd_volume::text,
            p.lote_mul_venda, p.preco_promocional::text
          FROM vw_pauta_produtos pp
          JOIN vw_produtos p ON p.id_erp = pp.id_produto AND p.id_empresa = ${idEmpresa}
          LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
          WHERE pp.id_pauta = ${idPauta}
            AND p.ativo = 1
            AND pp.preco1 > 0
            ${grupoFilter}
            ${searchFilter}
          ORDER BY p.descricao
          LIMIT ${limit} OFFSET ${offset}
        `),
        prisma.$queryRawUnsafe<[{ count: string }]>(`
          SELECT COUNT(*)::text AS count
          FROM vw_pauta_produtos pp
          JOIN vw_produtos p ON p.id_erp = pp.id_produto AND p.id_empresa = ${idEmpresa}
          WHERE pp.id_pauta = ${idPauta} AND p.ativo = 1 AND pp.preco1 > 0
            ${grupoFilter} ${searchFilter}
        `),
      ]);
      const total = parseInt(countRows[0]?.count ?? '0', 10);
      return {
        data: rows.map((p) => this.mapRow(p, p.preco_pauta)),
        total,
        page,
        limit,
      };
    }

    // Sem pauta: produtos com preço base preenchido
    const [rows, countRows] = await Promise.all([
      prisma.$queryRawUnsafe<ProdutoRow[]>(`
        SELECT
          p.id_erp, p.codigo, p.descricao, p.unidade,
          p.vl_preco1::text,
          p.cod_grupo, p.grupo, p.cod_subgrupo, p.subgrupo, p.marca,
          p.ativo, e.qtd_estoque::text, p.lista_pauta,
          p.referencia, p.descr_det, p.vl_preco2::text, p.vl_preco3::text,
          p.aliq_ipi::text, p.peso_liquido::text, p.qtd_volume::text,
          p.lote_mul_venda, p.preco_promocional::text
        FROM vw_produtos p
        LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
        WHERE p.id_empresa = ${idEmpresa} AND p.ativo = 1 AND p.vl_preco1 > 0
          ${grupoFilter} ${searchFilter}
        ORDER BY p.descricao
        LIMIT ${limit} OFFSET ${offset}
      `),
      prisma.$queryRawUnsafe<[{ count: string }]>(`
        SELECT COUNT(*)::text AS count FROM vw_produtos p
        WHERE p.id_empresa = ${idEmpresa} AND p.ativo = 1 AND p.vl_preco1 > 0
          ${grupoFilter} ${searchFilter}
      `),
    ]);
    const total = parseInt(countRows[0]?.count ?? '0', 10);
    return { data: rows.map((p) => this.mapRow(p, p.vl_preco1)), total, page, limit };
  }

  private mapRow(p: ProdutoRow, preco: string): ProdutoSummary {
    return {
      idErp: Number(p.id_erp),
      codigo: (p.codigo ?? '').trim(),
      descricao: (p.descricao ?? '').trim(),
      unidade: p.unidade,
      vlPreco1: preco ?? '0',
      codGrupo: p.cod_grupo !== null ? Number(p.cod_grupo) : null,
      grupo: p.grupo ? p.grupo.trim() : null,
      codSubgrupo: p.cod_subgrupo !== null ? Number(p.cod_subgrupo) : null,
      subgrupo: p.subgrupo ? p.subgrupo.trim() : null,
      marca: p.marca ? p.marca.trim() : null,
      ativo: Number(p.ativo),
      qtdEstoque: p.qtd_estoque,
      listaParauta: p.lista_pauta !== null ? Number(p.lista_pauta) : null,
      loteMulVenda: p.lote_mul_venda !== null ? Number(p.lote_mul_venda) : null,
    };
  }

  async findOne(idErp: number): Promise<ProdutoDetail | null> {
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
    const userId = this.cls.get('userId');
    const codVendedor = userId ? parseInt(userId, 10) : 0;

    interface PautaPrecoRow {
      id_pauta: number;
      codigo: number;
      descricao: string;
      preco: string;
    }

    const [rows, pautaRows] = await Promise.all([
      prisma.$queryRawUnsafe<ProdutoRow[]>(`
        SELECT
          p.id_erp,
          p.codigo,
          p.descricao,
          p.unidade,
          p.vl_preco1::text,
          p.cod_grupo,
          p.grupo,
          p.cod_subgrupo,
          p.subgrupo,
          p.marca,
          p.ativo,
          e.qtd_estoque::text,
          p.lista_pauta,
          p.referencia,
          p.descr_det,
          p.vl_preco2::text,
          p.vl_preco3::text,
          p.aliq_ipi::text,
          p.peso_liquido::text,
          p.qtd_volume::text,
          p.lote_mul_venda,
          p.preco_promocional::text
        FROM vw_produtos p
        LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
        WHERE p.id_erp = ${idErp} AND p.ativo = 1
        LIMIT 1
      `),
      prisma.$queryRawUnsafe<PautaPrecoRow[]>(`
        SELECT DISTINCT pa.id_pauta, pa.codigo, TRIM(pa.descricao) AS descricao, pp.preco1::text AS preco
        FROM vw_pauta_produtos pp
        JOIN vw_pautas pa ON pa.id_pauta = pp.id_pauta
          AND pa.id_empresa = ${idEmpresa}
          AND pa.ativo = 1
        JOIN vw_representantes r ON pa.codigo IN (
          r.cod_pauta1, r.cod_pauta2, r.cod_pauta3,
          r.cod_pauta4, r.cod_pauta5, r.cod_pauta6
        )
        WHERE pp.id_produto = ${idErp}
          AND r.codigo = ${codVendedor}
          AND pp.preco1 > 0
        ORDER BY pa.codigo
      `),
    ]);

    const p = rows[0];
    if (!p) return null;

    return {
      ...this.mapRow(p, p.vl_preco1),
      referencia: p.referencia,
      descricaoDetalhada: p.descr_det,
      vlPreco2: p.vl_preco2,
      vlPreco3: p.vl_preco3,
      aliqIpi: p.aliq_ipi,
      pesoLiquido: p.peso_liquido,
      qtdVolume: p.qtd_volume,
      loteMulVenda: p.lote_mul_venda !== null ? Number(p.lote_mul_venda) : null,
      precoComIpi: null,
      precoPromocional: p.preco_promocional,
      pautaPrecos: pautaRows.map((r) => ({
        idPauta: Number(r.id_pauta),
        codigo: Number(r.codigo),
        descricao: r.descricao,
        preco: r.preco,
      })),
    };
  }

  async formasPagamento(): Promise<FormaPagamento[]> {
    const prisma = this.cls.get('prisma');
    if (!prisma) throw new Error('prisma não disponível no CLS');
    const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));

    interface Row {
      codigo: number;
      descricao: string;
      num_parcelas: number | null;
      tx_acrescimo: string;
    }

    const rows = await prisma.$queryRawUnsafe<Row[]>(`
      SELECT codigo, TRIM(descricao) AS descricao, num_parcelas, tx_acrescimo::text
      FROM sar.vw_formas_pagamento
      WHERE id_empresa = ${idEmpresa}
        AND ativa = 1
        AND integrar_sar = 1
      ORDER BY codigo
    `);

    return rows.map((r) => ({
      codigo: Number(r.codigo),
      descricao: r.descricao,
      numParcelas: r.num_parcelas !== null ? Number(r.num_parcelas) : null,
      txAcrescimo: r.tx_acrescimo ?? '0',
    }));
  }
}
