import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { AuditService } from '../../audit/audit.service';
import { PrismaService } from '../../prisma/prisma.service';
import { ContabilidadSeedService } from './seed.service';

@Injectable()
export class PlanCuentasService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly seed: ContabilidadSeedService,
    private readonly audit: AuditService,
  ) {}

  async getArbol(empresaId: string, incluirInactivas = false) {
    // Trae plan de la empresa + plan maestro del sistema (empresa_id null)
    const cuentas = await this.prisma.cont_plan_cuentas.findMany({
      where: {
        OR: [{ empresa_id: empresaId }, { empresa_id: null }],
        ...(incluirInactivas ? {} : { active: true }),
      },
      orderBy: { codigo: 'asc' },
    });
    return this.buildTree(cuentas);
  }

  async getLista(empresaId: string, soloHojas = false, incluirInactivas = false) {
    return this.prisma.cont_plan_cuentas.findMany({
      where: {
        OR: [{ empresa_id: empresaId }, { empresa_id: null }],
        ...(incluirInactivas ? {} : { active: true }),
        ...(soloHojas ? { acepta_movimientos: true } : {}),
      },
      orderBy: { codigo: 'asc' },
    });
  }

  async crear(empresaId: string, data: { codigo: string; descripcion: string; tipo: string; naturaleza: string; cuenta_padre_id?: string; acepta_cc?: boolean; moneda_fija?: string }) {
    const existente = await this.prisma.cont_plan_cuentas.findFirst({
      where: { empresa_id: empresaId, codigo: data.codigo },
    });
    if (existente) throw new BadRequestException(`Ya existe una cuenta con código ${data.codigo}`);

    let nivel = 1;
    if (data.cuenta_padre_id) {
      const padre = await this.prisma.cont_plan_cuentas.findUnique({ where: { id: data.cuenta_padre_id } });
      if (!padre) throw new NotFoundException('Cuenta padre no encontrada');
      nivel = padre.nivel + 1;
      // El padre ya no acepta movimientos directos si tiene hijos
      await this.prisma.cont_plan_cuentas.update({ where: { id: data.cuenta_padre_id }, data: { acepta_movimientos: false } });
    }

    const cuenta = await this.prisma.cont_plan_cuentas.create({
      data: {
        empresa_id: empresaId,
        codigo: data.codigo,
        descripcion: data.descripcion,
        tipo: data.tipo as any,
        naturaleza: data.naturaleza as any,
        nivel,
        cuenta_padre_id: data.cuenta_padre_id || null,
        acepta_movimientos: true,
        acepta_cc: data.acepta_cc ?? false,
        moneda_fija: data.moneda_fija ?? null,
      },
    });
    this.audit.log({ empresa_id: empresaId, action: 'CREATE', entity_type: 'cont_plan_cuentas', entity_id: cuenta.id, descripcion: `Cuenta ${cuenta.codigo} - ${cuenta.descripcion} creada` });
    return cuenta;
  }

  async editar(
    id: string,
    empresaId: string,
    data: Partial<{
      codigo: string;
      descripcion: string;
      tipo: string;
      naturaleza: string;
      cuenta_padre_id: string | null;
      acepta_cc: boolean;
      active: boolean;
      moneda_fija: string;
    }>,
  ) {
    const cuenta = await this.prisma.cont_plan_cuentas.findFirst({
      where: { id, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
    });
    if (!cuenta) throw new NotFoundException('Cuenta no encontrada');
    if (cuenta.empresa_id === null || cuenta.is_sistema) {
      throw new BadRequestException('Las cuentas del sistema no pueden editarse');
    }

    const cambiaEstructura =
      (data.codigo !== undefined && data.codigo !== cuenta.codigo) ||
      (data.tipo !== undefined && data.tipo !== cuenta.tipo) ||
      (data.naturaleza !== undefined && data.naturaleza !== cuenta.naturaleza) ||
      (data.cuenta_padre_id !== undefined && data.cuenta_padre_id !== cuenta.cuenta_padre_id);

    if (cambiaEstructura) {
      const tieneMovimientos = await this.prisma.cont_asientos_det.findFirst({ where: { cuenta_id: id } });
      if (tieneMovimientos && (data.tipo !== undefined || data.naturaleza !== undefined)) {
        throw new BadRequestException('No se puede cambiar tipo/naturaleza: la cuenta tiene movimientos registrados');
      }
    }

    // Unicidad del código dentro de la empresa
    if (data.codigo !== undefined && data.codigo !== cuenta.codigo) {
      const dup = await this.prisma.cont_plan_cuentas.findFirst({
        where: { empresa_id: empresaId, codigo: data.codigo, NOT: { id } },
      });
      if (dup) throw new BadRequestException(`Ya existe una cuenta con código ${data.codigo}`);
    }

    // Cambio de padre: validar ciclos + recalcular niveles + acepta_movimientos de padres
    let nuevoNivel: number | undefined;
    const padreAnteriorId = cuenta.cuenta_padre_id;
    if (data.cuenta_padre_id !== undefined && data.cuenta_padre_id !== cuenta.cuenta_padre_id) {
      if (data.cuenta_padre_id === id) {
        throw new BadRequestException('Una cuenta no puede ser su propio padre');
      }
      if (data.cuenta_padre_id) {
        const nuevoPadre = await this.prisma.cont_plan_cuentas.findUnique({
          where: { id: data.cuenta_padre_id },
        });
        if (!nuevoPadre) throw new NotFoundException('Cuenta padre no encontrada');

        // Verificar que el nuevo padre no sea descendiente de esta cuenta (evitar ciclo)
        const descendientes = await this.getDescendientesIds(id);
        if (descendientes.has(data.cuenta_padre_id)) {
          throw new BadRequestException('El nuevo padre no puede ser una subcuenta de la cuenta actual');
        }
        nuevoNivel = nuevoPadre.nivel + 1;
      } else {
        nuevoNivel = 1;
      }
    }

    const result = await this.prisma.$transaction(async (tx) => {
      const updateData: any = { ...data, updated_at: new Date() };
      if (nuevoNivel !== undefined) updateData.nivel = nuevoNivel;

      const upd = await tx.cont_plan_cuentas.update({ where: { id }, data: updateData });

      // Recalcular niveles de descendientes si cambió el nivel
      if (nuevoNivel !== undefined) {
        await this.recalcularNivelDescendientes(tx, id, nuevoNivel);
      }

      // Marcar el nuevo padre como no acepta_movimientos
      if (data.cuenta_padre_id) {
        await tx.cont_plan_cuentas.update({
          where: { id: data.cuenta_padre_id },
          data: { acepta_movimientos: false },
        });
      }

      // Si el padre anterior se quedó sin hijos, vuelve a aceptar movimientos
      if (padreAnteriorId && padreAnteriorId !== data.cuenta_padre_id) {
        const quedan = await tx.cont_plan_cuentas.findFirst({
          where: { cuenta_padre_id: padreAnteriorId },
          select: { id: true },
        });
        if (!quedan) {
          await tx.cont_plan_cuentas.update({
            where: { id: padreAnteriorId },
            data: { acepta_movimientos: true },
          });
        }
      }

      return upd;
    });

    this.audit.log({
      empresa_id: empresaId,
      action: 'UPDATE',
      entity_type: 'cont_plan_cuentas',
      entity_id: id,
      old_value: {
        codigo: cuenta.codigo,
        descripcion: cuenta.descripcion,
        tipo: cuenta.tipo,
        naturaleza: cuenta.naturaleza,
        cuenta_padre_id: cuenta.cuenta_padre_id,
        acepta_cc: cuenta.acepta_cc,
        active: cuenta.active,
      },
      new_value: data as any,
      descripcion: `Cuenta ${cuenta.codigo} editada`,
    });
    return result;
  }

  private async getDescendientesIds(cuentaId: string): Promise<Set<string>> {
    const acc = new Set<string>();
    const stack = [cuentaId];
    while (stack.length) {
      const actual = stack.pop() as string;
      const hijos = await this.prisma.cont_plan_cuentas.findMany({
        where: { cuenta_padre_id: actual },
        select: { id: true },
      });
      for (const h of hijos) {
        if (!acc.has(h.id)) {
          acc.add(h.id);
          stack.push(h.id);
        }
      }
    }
    return acc;
  }

  private async recalcularNivelDescendientes(tx: any, padreId: string, padreNivel: number) {
    const hijos = await tx.cont_plan_cuentas.findMany({
      where: { cuenta_padre_id: padreId },
      select: { id: true },
    });
    for (const h of hijos) {
      const nuevoNivel = padreNivel + 1;
      await tx.cont_plan_cuentas.update({ where: { id: h.id }, data: { nivel: nuevoNivel } });
      await this.recalcularNivelDescendientes(tx, h.id, nuevoNivel);
    }
  }

  async eliminar(id: string, empresaId: string) {
    const cuenta = await this.prisma.cont_plan_cuentas.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!cuenta) throw new NotFoundException('Cuenta no encontrada o no pertenece a la empresa');
    if (cuenta.is_sistema) throw new BadRequestException('No se pueden eliminar cuentas del sistema');

    const tieneMovimientos = await this.prisma.cont_asientos_det.findFirst({ where: { cuenta_id: id } });
    if (tieneMovimientos) throw new BadRequestException('La cuenta tiene movimientos registrados y no puede eliminarse');

    const tieneHijos = await this.prisma.cont_plan_cuentas.findFirst({ where: { cuenta_padre_id: id } });
    if (tieneHijos) throw new BadRequestException('La cuenta tiene subcuentas y no puede eliminarse');

    const result = await this.prisma.cont_plan_cuentas.delete({ where: { id } });
    this.audit.log({ empresa_id: empresaId, action: 'DELETE', entity_type: 'cont_plan_cuentas', entity_id: id, descripcion: `Cuenta ${cuenta.codigo} - ${cuenta.descripcion} eliminada` });
    return result;
  }

  async cargarSeed(empresaId: string) {
    const plan = await this.seed.seedPlanCuentas(empresaId);
    const mapeo = await this.seed.seedMapeoDefault(empresaId);
    return { plan, mapeo };
  }

  private buildTree(cuentas: any[]) {
    const mapa = new Map(cuentas.map((c) => [c.id, { ...c, hijos: [] }]));
    const raices: any[] = [];

    for (const cuenta of mapa.values()) {
      if (cuenta.cuenta_padre_id && mapa.has(cuenta.cuenta_padre_id)) {
        mapa.get(cuenta.cuenta_padre_id).hijos.push(cuenta);
      } else {
        raices.push(cuenta);
      }
    }

    return raices;
  }
}
