import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from "@nestjs/common";
import { Repository } from "typeorm";
import { ReadonlyRepository } from "../common/readonly-repository";
import { TenantRepositoryAccessor } from "../tenancy/tenant-repository.accessor";
import { TallerParametrosService } from "../taller-parametros/taller-parametros.service";
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;
  costo: number;
  pagaiva: boolean;
  iva: number;
  es_servicio: boolean;
  usainventario: boolean;
  estaller: boolean;
  es_producto?: boolean;
  origen: "local" | "erp";
};

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

  constructor(
    private readonly tenantAccessor: TenantRepositoryAccessor,
    private readonly tallerParametros: TallerParametrosService,
  ) {}

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

  private get erpProductoRepository(): ReadonlyRepository<Producto> {
    return new ReadonlyRepository(this.tenantAccessor.repositoryFor(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 normalizedPlu = String(plu || "").trim();
    const local = await this.localProductoRepository.findOne({
      where: { plu: normalizedPlu },
    });
    if (local) {
      return this.toView(local, "local");
    }

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

    throw new NotFoundException(
      `Producto con PLU ${normalizedPlu} 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
      .createSelectQueryBuilder("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
      .createSelectQueryBuilder("producto")
      .orderBy("producto.desclarga", "ASC");

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

    if (soloTaller) {
      erpQuery.andWhere("producto.es_taller = :esTaller", { esTaller: "S" });
    }

    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 permiteCrear = await this.tallerParametros.getBooleanConFallback(
      "productos.permite_crear",
    );
    if (!permiteCrear) {
      throw new ForbiddenException(
        "Este tenant no permite crear/editar productos propios -- activá 'Permitir crear productos desde el taller' en Configuración → Productos.",
      );
    }

    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 descripción larga es requerida");
    }

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

    const saved = await this.localProductoRepository.save(producto);

    // Un producto creado en el taller vive solo en taller_productos: el ERP no
    // se toca. Al facturar, lo creado en el taller se cobra bajo los PLU
    // genericos de la tienda (ver consolidarLineasParaPos); lo que se venda
    // mas de una vez debe crearse en el ERP.
    return this.toView(saved, "local");
  }

  async update(plu: string, dto: UpdateProductoDto): Promise<ProductoView> {
    const permiteCrear = await this.tallerParametros.getBooleanConFallback(
      "productos.permite_crear",
    );
    if (!permiteCrear) {
      throw new ForbiddenException(
        "Este tenant no permite crear/editar productos propios -- activá 'Permitir crear productos desde el taller' en Configuración → Productos.",
      );
    }

    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 descripción larga no puede estar vacía",
        );
      }
      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.costo !== undefined) {
      local.costo = dto.costo;
    }

    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.save(local);

    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`);
    }

    // maeplu es solo lectura — nunca se propaga la eliminacion al ERP, ni
    // siquiera para filas TLLP- creadas por el taller.
    await this.localProductoRepository.remove(local);

    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
      .createSelectQueryBuilder("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.es_taller = :esTaller", {
        esTaller: "S",
      });
    }

    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 {
    // "estaller" (local, taller_productos) and "es_taller" (ERP, maeplu) are
    // distinct columns on distinct tables — pick the one that matches origen.
    const estaller =
      origen === "local"
        ? producto.estaller === undefined
          ? true
          : Boolean(producto.estaller)
        : producto.es_taller === undefined
          ? true
          : Boolean(producto.es_taller);

    return {
      plu: producto.plu || "",
      desclarga: (
        producto.desclarga ||
        producto.desccorta ||
        producto.plu ||
        ""
      ).trim(),
      desccorta: producto.desccorta,
      alterno: producto.alterno,
      precio: Number(producto.precio ?? 0),
      costo: Number(producto.costo ?? 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,
      es_producto:
        origen === "erp" && producto.es_producto !== undefined
          ? Boolean(producto.es_producto)
          : undefined,
      origen,
    };
  }

  async getCostoByPlu(plu: string): Promise<number> {
    const normalizedPlu = String(plu || "").trim();
    const local = await this.localProductoRepository.findOne({
      where: { plu: normalizedPlu },
    });
    if (local) {
      return Number(local.costo || 0);
    }

    return this.getErpCosto(normalizedPlu);
  }

  private async getErpCosto(plu: string): Promise<number> {
    const column = await this.getErpCostColumn();
    if (!column) {
      return 0;
    }

    try {
      const rows = await this.erpProductoRepository.query<
        Array<{ costo: number | string }>
      >(`SELECT \`${column}\` AS costo FROM maeplu WHERE plu = ? LIMIT 1`, [
        plu,
      ]);
      const costo = Number(rows?.[0]?.costo || 0);
      return Number.isFinite(costo) && costo > 0 ? costo : 0;
    } catch (error: any) {
      this.logger.warn(
        `No se pudo leer costo de maeplu.${column}: ${error?.message ?? error}`,
      );
      return 0;
    }
  }

  private async getErpCostColumn(): Promise<string | null> {
    if (!this.erpCostColumnPromise) {
      const candidates = [
        "costo",
        "costoult",
        "costo_ult",
        "costo_ultimo",
        "costoprom",
        "costo_prom",
        "costo_promedio",
        "ultcosto",
        "precio_costo",
      ];

      this.erpCostColumnPromise = this.erpProductoRepository
        .query<Array<{ columnName: string }>>(
          `
            SELECT COLUMN_NAME AS columnName
            FROM information_schema.COLUMNS
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = ?
              AND COLUMN_NAME IN (${candidates.map(() => "?").join(",")})
          `,
          ["maeplu", ...candidates],
        )
        .then((rows: Array<{ columnName: string }>) => {
          const available = new Set(
            rows.map((row) => String(row.columnName).toLowerCase()),
          );
          return (
            candidates.find((candidate) => available.has(candidate)) || null
          );
        })
        .catch((error: any) => {
          this.logger.warn(
            `No se pudo verificar columna de costo en maeplu: ${error?.message ?? error}`,
          );
          return null;
        });
    }

    return this.erpCostColumnPromise;
  }

  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")}`;
  }
}
