﻿import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from "@nestjs/common";
import { Repository } from "typeorm";
import { TenantRepositoryAccessor } from "../tenancy/tenant-repository.accessor";
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(private readonly tenantAccessor: TenantRepositoryAccessor) {}

  private get localClienteRepository(): Repository<ClienteLocal> {
    return this.tenantAccessor.repositoryFor(ClienteLocal);
  }

  private get erpClienteRepository(): Repository<Cliente> {
    return this.tenantAccessor.repositoryFor(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> {
    const payload = this.normalizeClientePayload(createClienteDto);
    let tarjeta = createClienteDto.tarjeta;

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

    await this.ensureNitAvailable(payload.nit || "CF");

    const saved = await this.tenantAccessor.runInTransaction(async (repos) => {
      const localRepo = repos.get(ClienteLocal);
      const erpRepo = repos.get(Cliente);

      const cliente = localRepo.create({
        ...payload,
        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(this.normalizeNit(updateClienteDto.nit), tarjeta);
    }

    const saved = await this.tenantAccessor.runInTransaction(async (repos) => {
      const localRepo = repos.get(ClienteLocal);
      const erpRepo = repos.get(Cliente);

      Object.assign(cliente, this.normalizeClientePayload(updateClienteDto, cliente));
      const localSaved = await localRepo.save(cliente);

      // Guard critico: si `cliente` fue clonado desde un cliente real del
      // ERP (arriba, lineas 144-153), su tarjeta NO empieza con "TLL-" y
      // NUNCA se propaga a maecli — evita sobreescribir uno de los 3.257
      // clientes reales con datos derivados de la copia local. Mismo guard
      // que ya existia en delete() (linea ~201); aca faltaba.
      if (tarjeta.startsWith("TLL-")) {
        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.tenantAccessor.runInTransaction(async (repos) => {
      const localRepo = repos.get(ClienteLocal);
      const erpRepo = repos.get(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) {
      const normalizedSearch = `%${search.toLowerCase()}%`;
      qb.where(
        "(LOWER(cliente.nombre) LIKE :search OR LOWER(cliente.nit) LIKE :search OR LOWER(cliente.telefono) LIKE :search OR LOWER(cliente.celular) LIKE :search OR LOWER(cliente.tarjeta) LIKE :search)",
        { search: normalizedSearch },
      );
    }

    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) {
      const normalizedSearch = `%${search.toLowerCase()}%`;
      qb.where(
        "(LOWER(cliente.nombre) LIKE :search OR LOWER(cliente.nit) LIKE :search OR LOWER(cliente.telefono) LIKE :search OR LOWER(cliente.celular) LIKE :search OR LOWER(cliente.tarjeta) LIKE :search OR LOWER(cliente.comercial) LIKE :search OR LOWER(cliente.razonsocial) LIKE :search)",
        { search: normalizedSearch },
      );
    }

    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) {
      const normalizedSearch = `%${search.toLowerCase()}%`;
      qb.where(
        "(LOWER(cliente.nombre) LIKE :search OR LOWER(cliente.nit) LIKE :search OR LOWER(cliente.telefono) LIKE :search OR LOWER(cliente.celular) LIKE :search OR LOWER(cliente.tarjeta) LIKE :search)",
        { search: normalizedSearch },
      );
    }

    return qb.getCount();
  }

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

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

    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 normalizedNit = this.normalizeNit(nit);
    if (normalizedNit.toUpperCase() === "CF") {
      return;
    }

    const qb = this.localClienteRepository
      .createQueryBuilder("cliente")
      .where("cliente.nit = :nit", { nit: normalizedNit });

    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 ${normalizedNit}`,
      );
    }
  }

  private normalizeNit(value?: string | null): string {
    return (value || "").trim() || "CF";
  }

  private normalizeClientePayload(
    dto: Partial<CreateClienteDto & UpdateClienteDto>,
    fallback?: Partial<ClienteLocal>,
  ): Partial<ClienteLocal> {
    const payload: Partial<ClienteLocal> = {
      ...dto,
    };

    if (dto.nombre !== undefined) {
      payload.nombre = dto.nombre.trim();
    }

    if (dto.direccion !== undefined) {
      payload.direccion = dto.direccion.trim();
    } else if (fallback?.direccion !== undefined) {
      payload.direccion = fallback.direccion;
    }

    if (dto.nit !== undefined || !fallback?.nit) {
      payload.nit = this.normalizeNit(dto.nit);
    }

    if (dto.telefono !== undefined) {
      payload.telefono = dto.telefono?.trim() || undefined;
    }

    if (dto.celular !== undefined) {
      payload.celular = dto.celular?.trim() || undefined;
    }

    if (dto.email !== undefined) {
      payload.email = dto.email?.trim() || undefined;
    }

    return payload;
  }

  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) {
        // update() parcial, no findOne+Object.assign+save de la entidad
        // completa — evita reescribir columnas no tocadas (limite, credito,
        // descuento, etc.) con lo que se leyo en un instante anterior si
        // otro sistema las cambio entretanto.
        const { tarjeta: _tarjeta, ...fields } = payload;
        await erpRepo.update({ tarjeta }, fields);
        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)",
      );
    }
  }
}
