import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CreateProductoDto } from "./dto/create-producto.dto";
import { UpdateProductoDto } from "./dto/update-producto.dto";
import { ProductoLocal } from "./entities/producto-local.entity";
import { Producto } from "./entities/producto.entity";

export type ProductoView = {
  plu: string;
  desclarga: string;
  desccorta?: string;
  alterno?: string;
  precio: number;
  pagaiva: boolean;
  iva: number;
  es_servicio: boolean;
  usainventario: boolean;
  estaller: boolean;
  origen: "local" | "erp";
};

@Injectable()
export class ProductosService {
  private readonly logger = new Logger(ProductosService.name);
  private erpHasEstallerColumnPromise: Promise<boolean> | null = null;

  constructor(
    @InjectRepository(ProductoLocal)
    private localProductoRepository: Repository<ProductoLocal>,
    @InjectRepository(Producto)
    private erpProductoRepository: Repository<Producto>,
  ) {}

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

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

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

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

  async findByPlu(plu: string): Promise<ProductoView> {
    const local = await this.localProductoRepository.findOne({
      where: { plu },
    });
    if (local) {
      return this.toView(local, "local");
    }

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

    throw new NotFoundException(`Producto con PLU ${plu} no encontrado`);
  }

  async findByPlus(plus: string[]): Promise<ProductoView[]> {
    if (!plus || plus.length === 0) {
      return [];
    }

    const localRows = await this.localProductoRepository
      .createQueryBuilder("producto")
      .where("producto.plu IN (:...plus)", { plus })
      .getMany();

    const erpRows = await this.erpProductoRepository
      .createQueryBuilder("producto")
      .where("producto.plu IN (:...plus)", { plus })
      .getMany();

    const merged = this.mergeUnique(
      localRows.map((row) => this.toView(row, "local")),
      erpRows.map((row) => this.toView(row, "erp")),
    );

    const byPlu = new Map(merged.map((p) => [p.plu, p]));
    return plus
      .map((plu) => byPlu.get(plu))
      .filter((p): p is ProductoView => !!p);
  }

  async findTopProductos(
    limit = 50,
    esServicio?: boolean,
    soloTaller?: boolean,
  ): Promise<ProductoView[]> {
    const take = Number(limit) > 0 ? Number(limit) : 50;

    const localQuery = this.localProductoRepository
      .createQueryBuilder("producto")
      .orderBy("producto.desclarga", "ASC");

    if (esServicio !== undefined) {
      localQuery.andWhere("producto.es_servicio = :esServicio", {
        esServicio,
      });
    }

    if (soloTaller) {
      localQuery.andWhere("producto.estaller = :estaller", { estaller: true });
    }

    const erpQuery = this.erpProductoRepository
      .createQueryBuilder("producto")
      .orderBy("producto.desclarga", "ASC");

    if (esServicio !== undefined) {
      erpQuery.andWhere("producto.es_servicio = :esServicio", {
        esServicio,
      });
    }

    if (soloTaller && (await this.hasErpEstallerColumn())) {
      erpQuery.andWhere("COALESCE(producto.estaller, 0) = :estaller", {
        estaller: 1,
      });
    }

    const [localRows, erpRows] = await Promise.all([
      localQuery.take(take).getMany(),
      erpQuery.take(take).getMany(),
    ]);

    return this.mergeUnique(
      localRows.map((row) => this.toView(row, "local")),
      erpRows.map((row) => this.toView(row, "erp")),
    ).slice(0, take);
  }

  async create(dto: CreateProductoDto): Promise<ProductoView> {
    const resolvedPlu = dto.plu?.trim() || (await this.generatePlu());
    await this.ensurePluAvailable(resolvedPlu);

    if (dto.alterno) {
      await this.ensureAlternoAvailable(dto.alterno);
    }

    const desclarga = dto.desclarga?.trim();
    if (!desclarga) {
      throw new BadRequestException("La descripcion larga es requerida");
    }

    const saved = await this.localProductoRepository.manager.transaction(
      async (manager) => {
        const localRepo = manager.getRepository(ProductoLocal);
        const erpRepo = manager.getRepository(Producto);

        const producto = localRepo.create({
          plu: resolvedPlu,
          desclarga,
          desccorta: dto.desccorta?.trim() || undefined,
          alterno: dto.alterno?.trim() || undefined,
          precio: dto.precio ?? 0,
          pagaiva: dto.pagaiva ?? false,
          iva: dto.iva ?? 0,
          es_servicio: dto.esServicio ?? false,
          usainventario: dto.usainventario ?? true,
          estaller: dto.estaller ?? true,
        });

        const localSaved = await localRepo.save(producto);
        await this.upsertProductoInErp(erpRepo, localSaved);
        return localSaved;
      },
    );

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

  async update(plu: string, dto: UpdateProductoDto): Promise<ProductoView> {
    const local = await this.localProductoRepository.findOne({
      where: { plu },
    });

    if (!local) {
      const existsInErp = await this.erpProductoRepository.findOne({
        where: { plu },
      });
      if (existsInErp) {
        throw new BadRequestException(
          "No se puede actualizar un producto del ERP (solo lectura)",
        );
      }

      throw new NotFoundException(`Producto con PLU ${plu} no encontrado`);
    }

    if (dto.alterno && dto.alterno !== local.alterno) {
      await this.ensureAlternoAvailable(dto.alterno, plu);
    }

    if (dto.desclarga !== undefined) {
      const desclarga = dto.desclarga.trim();
      if (!desclarga) {
        throw new BadRequestException(
          "La descripcion larga no puede estar vacia",
        );
      }
      local.desclarga = desclarga;
    }

    if (dto.desccorta !== undefined) {
      local.desccorta = dto.desccorta?.trim() || undefined;
    }

    if (dto.alterno !== undefined) {
      local.alterno = dto.alterno?.trim() || undefined;
    }

    if (dto.precio !== undefined) {
      local.precio = dto.precio;
    }

    if (dto.pagaiva !== undefined) {
      local.pagaiva = dto.pagaiva;
    }

    if (dto.iva !== undefined) {
      local.iva = dto.iva;
    }

    if (dto.esServicio !== undefined) {
      local.es_servicio = dto.esServicio;
    }

    if (dto.usainventario !== undefined) {
      local.usainventario = dto.usainventario;
    }

    if (dto.estaller !== undefined) {
      local.estaller = dto.estaller;
    }

    const saved = await this.localProductoRepository.manager.transaction(
      async (manager) => {
        const localRepo = manager.getRepository(ProductoLocal);
        const erpRepo = manager.getRepository(Producto);

        const localSaved = await localRepo.save(local);
        await this.upsertProductoInErp(erpRepo, localSaved);
        return localSaved;
      },
    );

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

  async delete(plu: string): Promise<{ message: string }> {
    const local = await this.localProductoRepository.findOne({
      where: { plu },
    });

    if (!local) {
      const existsInErp = await this.erpProductoRepository.findOne({
        where: { plu },
      });
      if (existsInErp) {
        throw new BadRequestException(
          "No se puede eliminar un producto del ERP (solo lectura)",
        );
      }

      throw new NotFoundException(`Producto con PLU ${plu} no encontrado`);
    }

    await this.localProductoRepository.manager.transaction(async (manager) => {
      const localRepo = manager.getRepository(ProductoLocal);
      const erpRepo = manager.getRepository(Producto);

      await localRepo.remove(local);

      if (plu.startsWith("TLLP-")) {
        await erpRepo.delete({ plu });
      }
    });

    return { message: `Producto ${plu} eliminado correctamente` };
  }

  private async searchLocal(
    query: string,
    take: number,
    esServicio?: boolean,
    soloTaller?: boolean,
  ): Promise<ProductoView[]> {
    const queryBuilder = this.localProductoRepository
      .createQueryBuilder("producto")
      .where(
        "(producto.plu LIKE :q OR producto.desclarga LIKE :q OR producto.desccorta LIKE :q OR producto.alterno LIKE :q)",
        { q: `%${query}%` },
      );

    if (esServicio !== undefined) {
      queryBuilder.andWhere("producto.es_servicio = :esServicio", {
        esServicio,
      });
    }

    if (soloTaller) {
      queryBuilder.andWhere("producto.estaller = :estaller", {
        estaller: true,
      });
    }

    const rows = await queryBuilder
      .orderBy("producto.desclarga", "ASC")
      .take(take)
      .getMany();

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

  private async searchErp(
    query: string,
    take: number,
    esServicio?: boolean,
    soloTaller?: boolean,
  ): Promise<ProductoView[]> {
    const queryBuilder = this.erpProductoRepository
      .createQueryBuilder("producto")
      .where(
        "(producto.plu LIKE :q OR producto.desclarga LIKE :q OR producto.desccorta LIKE :q OR producto.alterno LIKE :q)",
        { q: `%${query}%` },
      );

    if (esServicio !== undefined) {
      queryBuilder.andWhere("producto.es_servicio = :esServicio", {
        esServicio,
      });
    }

    if (soloTaller && (await this.hasErpEstallerColumn())) {
      queryBuilder.andWhere("COALESCE(producto.estaller, 0) = :estaller", {
        estaller: 1,
      });
    }

    const rows = await queryBuilder
      .orderBy("producto.desclarga", "ASC")
      .take(take)
      .getMany();

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

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

    for (const p of erp) {
      map.set(p.plu, p);
    }

    for (const p of local) {
      map.set(p.plu, p);
    }

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

  private toView(
    producto: Partial<Producto & ProductoLocal>,
    origen: "local" | "erp",
  ): ProductoView {
    return {
      plu: producto.plu || "",
      desclarga: (
        producto.desclarga ||
        producto.desccorta ||
        producto.plu ||
        ""
      ).trim(),
      desccorta: producto.desccorta,
      alterno: producto.alterno,
      precio: Number(producto.precio ?? 0),
      pagaiva: Boolean(producto.pagaiva),
      iva: Number(producto.iva ?? 0),
      es_servicio: Boolean(producto.es_servicio),
      usainventario:
        producto.usainventario === undefined
          ? true
          : Boolean(producto.usainventario),
      estaller:
        producto.estaller === undefined ? true : Boolean(producto.estaller),
      origen,
    };
  }

  private async hasErpEstallerColumn(): Promise<boolean> {
    if (!this.erpHasEstallerColumnPromise) {
      this.erpHasEstallerColumnPromise = this.erpProductoRepository
        .query(
          `
            SELECT COUNT(*) AS total
            FROM information_schema.COLUMNS
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = ?
              AND COLUMN_NAME = ?
          `,
          ["maeplu", "estaller"],
        )
        .then((rows: Array<{ total: number | string }>) => Number(rows?.[0]?.total || 0) > 0)
        .catch((error: any) => {
          this.logger.warn(
            `No se pudo verificar columna maeplu.estaller: ${error?.message ?? error}`,
          );
          return false;
        });
    }

    return this.erpHasEstallerColumnPromise;
  }

  private async ensurePluAvailable(plu: string): Promise<void> {
    const existsLocal = await this.localProductoRepository.findOne({
      where: { plu },
    });
    if (existsLocal) {
      throw new ConflictException(`Ya existe un producto local con PLU ${plu}`);
    }

    const existsErp = await this.erpProductoRepository.findOne({
      where: { plu },
    });
    if (existsErp) {
      throw new ConflictException(`El PLU ${plu} ya existe en ERP`);
    }
  }

  private async ensureAlternoAvailable(
    alterno: string,
    currentPlu?: string,
  ): Promise<void> {
    const normalizedAlterno = alterno.trim();
    if (!normalizedAlterno) {
      return;
    }

    const qb = this.localProductoRepository
      .createQueryBuilder("producto")
      .where("producto.alterno = :alterno", { alterno: normalizedAlterno });

    if (currentPlu) {
      qb.andWhere("producto.plu != :plu", { plu: currentPlu });
    }

    const existing = await qb.getOne();
    if (existing) {
      throw new ConflictException(
        `Ya existe un producto local con codigo alterno ${normalizedAlterno}`,
      );
    }
  }

  private async generatePlu(): Promise<string> {
    const last = await this.localProductoRepository
      .createQueryBuilder("producto")
      .where("producto.plu LIKE :prefix", { prefix: "TLLP-%" })
      .orderBy("producto.plu", "DESC")
      .getOne();

    let nextNumber = 1;

    if (last) {
      const match = last.plu.match(/TLLP-(\d+)/);
      if (match) {
        nextNumber = parseInt(match[1], 10) + 1;
      }
    }

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

  private async upsertProductoInErp(
    erpRepo: Repository<Producto>,
    source: Partial<ProductoLocal>,
  ): Promise<void> {
    const plu = source.plu?.trim();
    if (!plu) {
      return;
    }

    const payload: Partial<Producto> & { estaller?: boolean } = {
      plu,
      desclarga: (source.desclarga || "").trim() || plu,
      desccorta: source.desccorta || undefined,
      alterno: source.alterno || undefined,
      precio: Number(source.precio ?? 0),
      pagaiva: Boolean(source.pagaiva),
      iva: Number(source.iva ?? 0),
      es_servicio: Boolean(source.es_servicio),
      usainventario:
        source.usainventario === undefined
          ? true
          : Boolean(source.usainventario),
      estaller: source.estaller === undefined ? true : Boolean(source.estaller),
    };

    try {
      const existing = await erpRepo.findOne({ where: { plu } });
      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 producto ${plu} to ERP: ${error?.message ?? error}`,
      );
      throw new BadRequestException(
        "No se pudo sincronizar el producto con la tabla ERP (maeplu)",
      );
    }
  }
}

