﻿import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CreateClienteDto } from "./dto/create-cliente.dto";
import { SearchClienteDto } from "./dto/search-cliente.dto";
import { UpdateClienteDto } from "./dto/update-cliente.dto";
import { ClienteLocal } from "./entities/cliente-local.entity";
import { Cliente } from "./entities/cliente.entity";

type ClienteView = {
  tarjeta: string;
  nombre: string;
  direccion?: string;
  telefono?: string;
  celular?: string;
  nit?: string;
  email?: string;
  comercial?: string;
  razonsocial?: string;
  origen: "local" | "erp";
};

@Injectable()
export class ClientesService {
  private readonly logger = new Logger(ClientesService.name);

  constructor(
    @InjectRepository(ClienteLocal)
    private localClienteRepository: Repository<ClienteLocal>,
    @InjectRepository(Cliente)
    private erpClienteRepository: Repository<Cliente>,
  ) {}

  async findAll(query: SearchClienteDto) {
    const { search, page = 1, limit = 20 } = query;
    const skip = (page - 1) * limit;

    const takeForSource = Math.max(page * limit, limit);

    const localItems = await this.searchLocal(search, takeForSource);
    const erpItems = await this.searchErp(search, takeForSource);

    const merged = this.mergeUnique(localItems, erpItems);
    const data = merged.slice(skip, skip + limit);

    const localTotal = await this.countLocal(search);
    const erpTotal = await this.countErp(search);

    // The exact total would require a full dedup across all ERP rows.
    const estimatedTotal = Math.max(data.length, localTotal + erpTotal);

    return {
      data,
      total: estimatedTotal,
      page,
      totalPages: Math.max(1, Math.ceil(estimatedTotal / limit)),
    };
  }

  async findByTarjeta(tarjeta: string): Promise<ClienteView> {
    const local = await this.localClienteRepository.findOne({
      where: { tarjeta },
    });
    if (local) {
      return this.toView(local, "local");
    }

    const erp = await this.erpClienteRepository.findOne({ where: { tarjeta } });
    if (erp) {
      return this.toView(erp, "erp");
    }

    throw new NotFoundException(`Cliente con tarjeta ${tarjeta} no encontrado`);
  }

  async search(query: string, limit = 20): Promise<ClienteView[]> {
    if (!query || query.length < 2) {
      return [];
    }

    const take = Number(limit) > 0 ? Number(limit) : 20;

    const localItems = await this.searchLocal(query, take);
    const erpItems = await this.searchErp(query, take);

    return this.mergeUnique(localItems, erpItems).slice(0, take);
  }

  async create(createClienteDto: CreateClienteDto): Promise<ClienteView> {
    let tarjeta = createClienteDto.tarjeta;

    if (!tarjeta) {
      tarjeta = await this.generateTarjeta();
    } else {
      await this.ensureTarjetaAvailable(tarjeta);
    }

    await this.ensureNitAvailable(createClienteDto.nit);

    const saved = await this.localClienteRepository.manager.transaction(
      async (manager) => {
        const localRepo = manager.getRepository(ClienteLocal);
        const erpRepo = manager.getRepository(Cliente);

        const cliente = localRepo.create({
          ...createClienteDto,
          tarjeta,
        });

        const localSaved = await localRepo.save(cliente);
        await this.upsertClienteInErp(erpRepo, localSaved);
        return localSaved;
      },
    );

    return this.toView(saved, "local");
  }

  async update(
    tarjeta: string,
    updateClienteDto: UpdateClienteDto,
  ): Promise<ClienteView> {
    let cliente = await this.localClienteRepository.findOne({
      where: { tarjeta },
    });

    if (!cliente) {
      const existsInErp = await this.erpClienteRepository.findOne({
        where: { tarjeta },
      });
      if (!existsInErp) {
        throw new NotFoundException(
          `Cliente con tarjeta ${tarjeta} no encontrado`,
        );
      }

      cliente = this.localClienteRepository.create({
        tarjeta: existsInErp.tarjeta,
        nombre: this.resolveNombre(existsInErp),
        direccion: existsInErp.direccion || "",
        telefono: existsInErp.telefono,
        celular: existsInErp.celular,
        nit: existsInErp.nit || "CF",
        email: existsInErp.email,
      });
      await this.localClienteRepository.save(cliente);
    }

    if (updateClienteDto.nit && updateClienteDto.nit !== cliente.nit) {
      await this.ensureNitAvailable(updateClienteDto.nit, tarjeta);
    }

    const saved = await this.localClienteRepository.manager.transaction(
      async (manager) => {
        const localRepo = manager.getRepository(ClienteLocal);
        const erpRepo = manager.getRepository(Cliente);

        Object.assign(cliente, updateClienteDto);
        const localSaved = await localRepo.save(cliente);
        await this.upsertClienteInErp(erpRepo, localSaved);
        return localSaved;
      },
    );

    return this.toView(saved, "local");
  }

  async delete(tarjeta: string): Promise<{ message: string }> {
    const cliente = await this.localClienteRepository.findOne({
      where: { tarjeta },
    });

    if (!cliente) {
      const existsInErp = await this.erpClienteRepository.findOne({
        where: { tarjeta },
      });
      if (existsInErp) {
        throw new BadRequestException(
          "No se puede eliminar un cliente del ERP (solo lectura)",
        );
      }
      throw new NotFoundException(
        `Cliente con tarjeta ${tarjeta} no encontrado`,
      );
    }

    await this.localClienteRepository.manager.transaction(async (manager) => {
      const localRepo = manager.getRepository(ClienteLocal);
      const erpRepo = manager.getRepository(Cliente);

      await localRepo.remove(cliente);

      // Safety guard: only remove ERP entries generated by this app.
      if (tarjeta.startsWith("TLL-")) {
        await erpRepo.delete({ tarjeta });
      }
    });

    return {
      message: `Cliente con tarjeta ${tarjeta} eliminado correctamente`,
    };
  }

  private async searchLocal(
    search: string | undefined,
    take: number,
  ): Promise<ClienteView[]> {
    const qb = this.localClienteRepository.createQueryBuilder("cliente");

    if (search) {
      qb.where(
        "(cliente.nombre LIKE :search OR cliente.nit LIKE :search OR cliente.telefono LIKE :search OR cliente.celular LIKE :search OR cliente.tarjeta LIKE :search)",
        { search: `%${search}%` },
      );
    }

    const rows = await qb.orderBy("cliente.nombre", "ASC").take(take).getMany();

    return rows.map((row) => this.toView(row, "local"));
  }

  private async searchErp(
    search: string | undefined,
    take: number,
  ): Promise<ClienteView[]> {
    const qb = this.erpClienteRepository.createQueryBuilder("cliente");

    if (search) {
      qb.where(
        "(cliente.nombre LIKE :search OR cliente.nit LIKE :search OR cliente.telefono LIKE :search OR cliente.celular LIKE :search OR cliente.tarjeta LIKE :search OR cliente.comercial LIKE :search OR cliente.razonsocial LIKE :search)",
        { search: `%${search}%` },
      );
    }

    const rows = await qb.orderBy("cliente.nombre", "ASC").take(take).getMany();

    return rows.map((row) => this.toView(row, "erp"));
  }

  private async countLocal(search?: string): Promise<number> {
    const qb = this.localClienteRepository.createQueryBuilder("cliente");

    if (search) {
      qb.where(
        "(cliente.nombre LIKE :search OR cliente.nit LIKE :search OR cliente.telefono LIKE :search OR cliente.celular LIKE :search OR cliente.tarjeta LIKE :search)",
        { search: `%${search}%` },
      );
    }

    return qb.getCount();
  }

  private async countErp(search?: string): Promise<number> {
    const qb = this.erpClienteRepository.createQueryBuilder("cliente");

    if (search) {
      qb.where(
        "(cliente.nombre LIKE :search OR cliente.nit LIKE :search OR cliente.telefono LIKE :search OR cliente.celular LIKE :search OR cliente.tarjeta LIKE :search OR cliente.comercial LIKE :search OR cliente.razonsocial LIKE :search)",
        { search: `%${search}%` },
      );
    }

    return qb.getCount();
  }

  private mergeUnique(local: ClienteView[], erp: ClienteView[]): ClienteView[] {
    const map = new Map<string, ClienteView>();

    for (const c of erp) {
      map.set(c.tarjeta, c);
    }

    for (const c of local) {
      // Local has priority if same tarjeta exists in both sources.
      map.set(c.tarjeta, c);
    }

    return Array.from(map.values()).sort((a, b) =>
      a.nombre.localeCompare(b.nombre),
    );
  }

  private toView(
    cliente: Partial<Cliente & ClienteLocal>,
    origen: "local" | "erp",
  ): ClienteView {
    return {
      tarjeta: cliente.tarjeta || "",
      nombre: this.resolveNombre(cliente),
      direccion: cliente.direccion,
      telefono: cliente.telefono,
      celular: cliente.celular,
      nit: cliente.nit,
      email: cliente.email,
      comercial: cliente.comercial,
      razonsocial: cliente.razonsocial,
      origen,
    };
  }

  private resolveNombre(cliente: Partial<Cliente & ClienteLocal>): string {
    const nombre = (cliente.nombre || "").trim();
    if (nombre) {
      return nombre;
    }

    const comercial = (cliente.comercial || "").trim();
    if (comercial) {
      return comercial;
    }

    const razon = (cliente.razonsocial || "").trim();
    if (razon) {
      return razon;
    }

    return (cliente.tarjeta || "").trim();
  }

  private async ensureTarjetaAvailable(tarjeta: string): Promise<void> {
    const existingLocal = await this.localClienteRepository.findOne({
      where: { tarjeta },
    });
    if (existingLocal) {
      throw new ConflictException(
        `Ya existe un cliente con tarjeta ${tarjeta}`,
      );
    }

    const existingErp = await this.erpClienteRepository.findOne({
      where: { tarjeta },
    });
    if (existingErp) {
      throw new ConflictException(`La tarjeta ${tarjeta} ya existe en ERP`);
    }
  }

  private async ensureNitAvailable(
    nit: string,
    currentTarjeta?: string,
  ): Promise<void> {
    const qb = this.localClienteRepository
      .createQueryBuilder("cliente")
      .where("cliente.nit = :nit", { nit });

    if (currentTarjeta) {
      qb.andWhere("cliente.tarjeta != :tarjeta", { tarjeta: currentTarjeta });
    }

    const existingLocal = await qb.getOne();
    if (existingLocal) {
      throw new ConflictException(`Ya existe un cliente local con NIT ${nit}`);
    }
  }

  private async generateTarjeta(): Promise<string> {
    const lastCliente = await this.localClienteRepository
      .createQueryBuilder("cliente")
      .where("cliente.tarjeta LIKE :prefix", { prefix: "TLL-%" })
      .orderBy("cliente.tarjeta", "DESC")
      .getOne();

    let nextNumber = 1;

    if (lastCliente) {
      const match = lastCliente.tarjeta.match(/TLL-(\d+)/);
      if (match) {
        nextNumber = parseInt(match[1], 10) + 1;
      }
    }

    return `TLL-${nextNumber.toString().padStart(8, "0")}`;
  }

  private async upsertClienteInErp(
    erpRepo: Repository<Cliente>,
    source: Partial<ClienteLocal>,
  ): Promise<void> {
    const tarjeta = source.tarjeta?.trim();
    if (!tarjeta) {
      return;
    }

    const payload: Partial<Cliente> = {
      tarjeta,
      nombre: (source.nombre || "").trim() || tarjeta,
      direccion: source.direccion || "",
      telefono: source.telefono || undefined,
      celular: source.celular || undefined,
      nit: source.nit || "CF",
      email: source.email || undefined,
      comercial: (source.nombre || "").trim() || undefined,
      razonsocial: (source.nombre || "").trim() || undefined,
    };

    try {
      const existing = await erpRepo.findOne({ where: { tarjeta } });
      if (existing) {
        Object.assign(existing, payload);
        await erpRepo.save(existing);
        return;
      }

      const created = erpRepo.create(payload);
      await erpRepo.save(created);
    } catch (error: any) {
      this.logger.error(
        `Error syncing cliente ${tarjeta} to ERP: ${error?.message ?? error}`,
      );
      throw new BadRequestException(
        "No se pudo sincronizar el cliente con la tabla ERP (maecli)",
      );
    }
  }
}
