import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';

export type FlujoMedioPago = 'COBRO' | 'PAGO';

export const FLUJOS_MEDIO_PAGO: FlujoMedioPago[] = ['COBRO', 'PAGO'];

/**
 * Códigos SIFEN de `medio_pago` que usan los documentos que guardan el medio
 * como texto en vez de FK (los recibos MULTI: `recibo_cobro_medios_pago.medio`).
 * Es un puente entre dos formas de guardar el mismo dato, no un criterio
 * contable: la cuenta la sigue eligiendo el contador en el mapeo.
 */
export const CODIGO_MEDIO_POR_NOMBRE: Record<string, number> = {
  EFECTIVO: 1,
  CHEQUE: 2,
  TRANSFERENCIA: 5,
  GIRO: 6,
  TARJETA: 3,
};

/**
 * Medios que NO se configuran acá porque ya tienen su propio lugar en el mapeo.
 *
 * El cheque se imputa por los conceptos CHEQUES_EN_CARTERA y CHEQUES_EN_TRANSITO
 * (grupo "Caja y Bancos"), que además distinguen el cheque al día del diferido —
 * algo que un medio de pago no puede representar. Tenerlo en los dos lugares
 * dejaba dos configuraciones para una misma decisión, sin que se viera cuál
 * mandaba.
 */
export const CODIGOS_MEDIO_CON_CONCEPTO_PROPIO = [CODIGO_MEDIO_POR_NOMBRE.CHEQUE];

/**
 * Qué cuenta contable usa cada medio de pago, por empresa.
 *
 * El sistema no supone nada: si la empresa no configuró el medio, se cae al
 * concepto del mapeo que se venía usando (CAJA_GENERAL y compañía), así que una
 * empresa que no toca esta pantalla sigue generando los mismos asientos que antes.
 */
@Injectable()
export class MapeoMediosPagoService {
  private readonly logger = new Logger(MapeoMediosPagoService.name);

  constructor(private readonly prisma: PrismaService) {}

  /** Medios que la empresa usa (o todos, si no restringió ninguno). */
  private async mediosDeLaEmpresa(empresaId: string) {
    const habilitados = await this.prisma.empresa_medio_pago.findMany({
      where: { empresa_id: empresaId, activo: true },
      select: { medio_pago_id: true, orden: true },
    });

    const where = habilitados.length
      ? { id: { in: habilitados.map((h) => h.medio_pago_id) } }
      : {};
    const medios = await this.prisma.medio_pago.findMany({
      where: { ...where, codigo: { notIn: CODIGOS_MEDIO_CON_CONCEPTO_PROPIO } },
      select: { id: true, codigo: true, descripcion: true },
      orderBy: { codigo: 'asc' },
    });

    const orden = new Map(habilitados.map((h) => [h.medio_pago_id, h.orden ?? 999]));
    return medios.sort(
      (a, b) => (orden.get(a.id) ?? 999) - (orden.get(b.id) ?? 999) || (a.codigo ?? 0) - (b.codigo ?? 0),
    );
  }

  /**
   * Lo que dibuja la pantalla: un renglón por medio, con la cuenta elegida para
   * cobros y para pagos. `null` significa "sin configurar", no un error.
   */
  async listar(empresaId: string) {
    const [medios, mapeos] = await Promise.all([
      this.mediosDeLaEmpresa(empresaId),
      this.prisma.cont_mapeo_medio_pago.findMany({
        where: { empresa_id: empresaId },
        select: {
          medio_pago_id: true,
          flujo: true,
          cuenta_contable_id: true,
          cuenta: { select: { codigo: true, descripcion: true, active: true, acepta_movimientos: true } },
        },
      }),
    ]);

    const porClave = new Map(mapeos.map((m) => [`${m.medio_pago_id}:${m.flujo}`, m]));
    const cuentaDe = (medioId: string, flujo: FlujoMedioPago) => {
      const m = porClave.get(`${medioId}:${flujo}`);
      if (!m) return null;
      return {
        cuenta_id: m.cuenta_contable_id,
        codigo: m.cuenta.codigo,
        descripcion: m.cuenta.descripcion,
        // El contador tiene que ver por qué un mapeo no se está aplicando.
        usable: !!m.cuenta.active && !!m.cuenta.acepta_movimientos,
      };
    };

    return {
      medios: medios.map((m) => ({
        medio_pago_id: m.id,
        codigo: m.codigo,
        descripcion: m.descripcion,
        cobro: cuentaDe(m.id, 'COBRO'),
        pago: cuentaDe(m.id, 'PAGO'),
      })),
      total_configurados: mapeos.length,
    };
  }

  /**
   * Alta/baja en lote. `cuenta_id: null` borra el mapeo y devuelve ese medio al
   * comportamiento por concepto.
   */
  async actualizarBulk(
    empresaId: string,
    items: Array<{ medio_pago_id: string; flujo: FlujoMedioPago; cuenta_id: string | null }>,
  ) {
    const resultados: Array<{ medio_pago_id: string; flujo: FlujoMedioPago; cuenta_id: string | null }> = [];

    for (const item of items) {
      if (!FLUJOS_MEDIO_PAGO.includes(item.flujo)) {
        throw new NotFoundException(`Flujo "${item.flujo}" inválido (COBRO o PAGO)`);
      }
      const clave = {
        empresa_id_medio_pago_id_flujo: {
          empresa_id: empresaId,
          medio_pago_id: item.medio_pago_id,
          flujo: item.flujo,
        },
      };

      if (!item.cuenta_id) {
        await this.prisma.cont_mapeo_medio_pago.deleteMany({
          where: { empresa_id: empresaId, medio_pago_id: item.medio_pago_id, flujo: item.flujo },
        });
        resultados.push({ ...item, cuenta_id: null });
        continue;
      }

      const cuenta = await this.prisma.cont_plan_cuentas.findFirst({
        where: { id: item.cuenta_id, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
        select: { id: true },
      });
      if (!cuenta) throw new NotFoundException(`Cuenta ${item.cuenta_id} no encontrada`);

      const medio = await this.prisma.medio_pago.findUnique({
        where: { id: item.medio_pago_id },
        select: { id: true, codigo: true, descripcion: true },
      });
      if (!medio) throw new NotFoundException(`Medio de pago ${item.medio_pago_id} no encontrado`);
      if (medio.codigo != null && CODIGOS_MEDIO_CON_CONCEPTO_PROPIO.includes(medio.codigo)) {
        throw new BadRequestException(
          `"${medio.descripcion}" no se configura por medio de pago: se imputa con los conceptos ` +
            '"Cheques en cartera" y "Cheques diferidos / en tránsito" del grupo Caja y Bancos.',
        );
      }

      await this.prisma.cont_mapeo_medio_pago.upsert({
        where: clave,
        update: { cuenta_contable_id: item.cuenta_id, updated_at: new Date() },
        create: {
          empresa_id: empresaId,
          medio_pago_id: item.medio_pago_id,
          flujo: item.flujo,
          cuenta_contable_id: item.cuenta_id,
        },
      });
      resultados.push(item);
    }

    return resultados;
  }

  /**
   * `medio_pago_id` → `cuenta_id`, solo con los mapeos que se pueden imputar.
   *
   * Un mapeo apuntando a una cuenta desactivada o de agrupación NO frena la
   * facturación: se avisa y ese medio cae al concepto de siempre. Bloquear acá
   * dejaría a la empresa sin poder facturar por un error de configuración
   * contable.
   */
  async resolverPorMedio(empresaId: string, flujo: FlujoMedioPago): Promise<Map<string, string>> {
    const mapeos = await this.prisma.cont_mapeo_medio_pago.findMany({
      // Se ignoran los medios que tienen concepto propio aunque haya quedado una
      // fila vieja en la base: el cheque lo resuelven sus conceptos, siempre.
      where: {
        empresa_id: empresaId,
        flujo,
        medio_pago: { codigo: { notIn: CODIGOS_MEDIO_CON_CONCEPTO_PROPIO } },
      },
      select: {
        medio_pago_id: true,
        cuenta_contable_id: true,
        cuenta: { select: { codigo: true, active: true, acepta_movimientos: true } },
      },
    });

    const mapa = new Map<string, string>();
    for (const m of mapeos) {
      if (!m.cuenta.active || !m.cuenta.acepta_movimientos) {
        this.logger.warn(
          `Mapeo de medio de pago ignorado: la cuenta ${m.cuenta.codigo} está desactivada o no acepta ` +
            `movimientos (empresa ${empresaId}, flujo ${flujo}). Se usa el concepto por defecto.`,
        );
        continue;
      }
      mapa.set(m.medio_pago_id, m.cuenta_contable_id);
    }
    return mapa;
  }

  /** Id del medio de pago con ese código SIFEN (1 efectivo, 2 cheque, 5 transferencia…). */
  async idMedioPorCodigo(codigo: number): Promise<string | null> {
    const medio = await this.prisma.medio_pago.findFirst({ where: { codigo }, select: { id: true } });
    return medio?.id ?? null;
  }

  /** Igual que `resolverPorMedio`, pero indexado por el código SIFEN del medio. */
  async resolverPorCodigoMedio(empresaId: string, flujo: FlujoMedioPago): Promise<Map<number, string>> {
    const porMedio = await this.resolverPorMedio(empresaId, flujo);
    if (porMedio.size === 0) return new Map();

    const medios = await this.prisma.medio_pago.findMany({
      where: { id: { in: [...porMedio.keys()] } },
      select: { id: true, codigo: true },
    });

    const mapa = new Map<number, string>();
    for (const m of medios) {
      const cuenta = porMedio.get(m.id);
      if (m.codigo != null && cuenta) mapa.set(m.codigo, cuenta);
    }
    return mapa;
  }
}
