import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { VehiculosService } from "../vehiculos/vehiculos.service";
import { CreateDanoDto } from "./dto/create-dano.dto";
import { CreateFirmaDto } from "./dto/create-firma.dto";
import { CreateFotoDto } from "./dto/create-foto.dto";
import { CreateOtDto } from "./dto/create-ot.dto";
import { SearchOtDto } from "./dto/search-ot.dto";
import { UpdateOtDto } from "./dto/update-ot.dto";
import { EstadoOT, OrdenTrabajo } from "./entities/orden-trabajo.entity";
import { OtCosaTrae } from "./entities/ot-cosa-trae.entity";
import { OtDano } from "./entities/ot-dano.entity";
import { OtFirma } from "./entities/ot-firma.entity";
import { OtFoto } from "./entities/ot-foto.entity";

const ALLOWED_ESTADO_TRANSITIONS: Record<EstadoOT, EstadoOT[]> = {
  [EstadoOT.BORRADOR]: [EstadoOT.EN_PROCESO],
  [EstadoOT.EN_PROCESO]: [EstadoOT.COTIZADA],
  [EstadoOT.COTIZADA]: [EstadoOT.APROBADA],
  [EstadoOT.APROBADA]: [EstadoOT.TERMINADA],
  [EstadoOT.TERMINADA]: [EstadoOT.ENTREGADA],
  [EstadoOT.ENTREGADA]: [],
};

@Injectable()
export class OtsService {
  constructor(
    @InjectRepository(OrdenTrabajo)
    private otRepository: Repository<OrdenTrabajo>,
    @InjectRepository(OtCosaTrae)
    private cosaTraeRepository: Repository<OtCosaTrae>,
    @InjectRepository(OtDano)
    private danoRepository: Repository<OtDano>,
    @InjectRepository(OtFoto)
    private fotoRepository: Repository<OtFoto>,
    @InjectRepository(OtFirma)
    private firmaRepository: Repository<OtFirma>,
    private vehiculosService: VehiculosService,
  ) {}

  async findAll(query: SearchOtDto) {
    const {
      estado,
      clienteTarjeta,
      placa,
      fechaDesde,
      fechaHasta,
      page = 1,
      limit = 20,
    } = query;
    const skip = (page - 1) * limit;

    const queryBuilder = this.otRepository
      .createQueryBuilder("ot")
      .leftJoinAndSelect("ot.vehiculo", "vehiculo")
      .leftJoinAndSelect("vehiculo.tipoVehiculo", "tipoVehiculo");

    if (estado) {
      queryBuilder.andWhere("ot.estado = :estado", { estado });
    }

    if (clienteTarjeta) {
      queryBuilder.andWhere("ot.clienteTarjeta = :clienteTarjeta", {
        clienteTarjeta,
      });
    }

    if (placa) {
      queryBuilder.andWhere("vehiculo.placa LIKE :placa", {
        placa: `%${placa}%`,
      });
    }

    if (fechaDesde) {
      queryBuilder.andWhere("ot.fechaEntrada >= :fechaDesde", { fechaDesde });
    }

    if (fechaHasta) {
      queryBuilder.andWhere("ot.fechaEntrada <= :fechaHasta", { fechaHasta });
    }

    queryBuilder.orderBy("ot.fechaEntrada", "DESC");
    queryBuilder.skip(skip).take(limit);

    const [data, total] = await queryBuilder.getManyAndCount();

    const byEstado = await this.otRepository
      .createQueryBuilder("ot")
      .select("ot.estado", "estado")
      .addSelect("COUNT(*)", "count")
      .groupBy("ot.estado")
      .getRawMany();

    const estadoCounts = {
      borrador: 0,
      en_proceso: 0,
      cotizada: 0,
      aprobada: 0,
      terminada: 0,
      entregada: 0,
    };

    byEstado.forEach((item) => {
      estadoCounts[item.estado as keyof typeof estadoCounts] = parseInt(
        item.count,
        10,
      );
    });

    return {
      data,
      total,
      page,
      totalPages: Math.ceil(total / limit),
      byEstado: estadoCounts,
    };
  }

  async findById(id: number): Promise<OrdenTrabajo> {
    const ot = await this.otRepository.findOne({
      where: { id },
      relations: [
        "vehiculo",
        "vehiculo.tipoVehiculo",
        "cosasTrae",
        "cosasTrae.cosaTrae",
        "danos",
        "danos.tipoDano",
        "fotos",
        "firma",
        "cotizaciones",
        "cotizaciones.lineas",
      ],
    });

    if (!ot) {
      throw new NotFoundException(`OT ${id} no encontrada`);
    }

    return ot;
  }

  async findByNumero(numeroOt: string): Promise<OrdenTrabajo> {
    const ot = await this.otRepository.findOne({
      where: { numeroOt },
      relations: ["vehiculo", "vehiculo.tipoVehiculo"],
    });

    if (!ot) {
      throw new NotFoundException(`OT ${numeroOt} no encontrada`);
    }

    return ot;
  }

  async create(dto: CreateOtDto, userId?: number): Promise<OrdenTrabajo> {
    let vehiculoId = dto.vehiculoId;

    if (!vehiculoId && dto.vehiculo) {
      const vehiculo = await this.vehiculosService.findOrCreate(
        dto.vehiculo,
        userId,
      );
      vehiculoId = vehiculo.id;
    }

    if (!vehiculoId) {
      throw new BadRequestException("Se requiere un vehiculo");
    }

    const numeroOt = await this.generateNumeroOt();

    const ot = this.otRepository.create({
      numeroOt,
      clienteTarjeta: dto.clienteTarjeta,
      vehiculoId,
      fechaEntrada: new Date(dto.fechaEntrada),
      km: dto.km,
      nivelGasolina: dto.nivelGasolina || 0,
      requerimientoCliente: dto.requerimientoCliente,
      observaciones: dto.observaciones,
      localId: dto.localId,
      createdBy: userId,
      updatedBy: userId,
    });

    const savedOt = await this.otRepository.save(ot);

    if (dto.cosasTrae && dto.cosasTrae.length > 0) {
      const cosasTrae = dto.cosasTrae.map((cosaId) =>
        this.cosaTraeRepository.create({
          otId: savedOt.id,
          cosaTraeId: cosaId,
        }),
      );
      await this.cosaTraeRepository.save(cosasTrae);
    }

    return this.findById(savedOt.id);
  }

  async update(
    id: number,
    dto: UpdateOtDto,
    userId?: number,
  ): Promise<OrdenTrabajo> {
    const ot = await this.findById(id);
    this.ensureEditable(ot);

    Object.assign(ot, {
      ...dto,
      fechaEntrada: dto.fechaEntrada
        ? new Date(dto.fechaEntrada)
        : ot.fechaEntrada,
      fechaSalida: dto.fechaSalida ? new Date(dto.fechaSalida) : ot.fechaSalida,
      version: ot.version + 1,
      updatedBy: userId,
    });

    await this.otRepository.save(ot);

    if (dto.cosasTrae !== undefined) {
      await this.cosaTraeRepository.delete({ otId: id });

      if (dto.cosasTrae.length > 0) {
        const cosasTrae = dto.cosasTrae.map((cosaId) =>
          this.cosaTraeRepository.create({
            otId: id,
            cosaTraeId: cosaId,
          }),
        );
        await this.cosaTraeRepository.save(cosasTrae);
      }
    }

    return this.findById(id);
  }

  async changeEstado(
    id: number,
    estado: EstadoOT,
    fechaSalida?: string,
    userId?: number,
  ): Promise<OrdenTrabajo> {
    const ot = await this.findById(id);
    const nextStates = ALLOWED_ESTADO_TRANSITIONS[ot.estado] || [];

    if (estado !== ot.estado && !nextStates.includes(estado)) {
      throw new BadRequestException(
        `Transicion invalida: ${ot.estado} -> ${estado}`,
      );
    }

    if (estado === EstadoOT.ENTREGADA && !fechaSalida && !ot.fechaSalida) {
      throw new BadRequestException(
        "Se requiere la fecha de salida para marcar como entregada",
      );
    }

    ot.estado = estado;
    if (fechaSalida) {
      ot.fechaSalida = new Date(fechaSalida);
    }
    ot.version = ot.version + 1;
    ot.updatedBy = userId ?? null;

    await this.otRepository.save(ot);
    return this.findById(id);
  }

  async addDano(otId: number, dto: CreateDanoDto): Promise<OtDano> {
    const ot = await this.findById(otId);
    this.ensureEditable(ot);

    const dano = this.danoRepository.create({
      otId,
      zona: dto.zona,
      tipoDanoId: dto.tipoDanoId,
      descripcion: dto.descripcion,
      posicionX: dto.posicionX,
      posicionY: dto.posicionY,
      fotoUrl: dto.fotoUrl,
    });

    return this.danoRepository.save(dano);
  }

  async removeDano(otId: number, danoId: number): Promise<void> {
    const ot = await this.findById(otId);
    this.ensureEditable(ot);

    const result = await this.danoRepository.delete({ id: danoId, otId });
    if (result.affected === 0) {
      throw new NotFoundException(`Dano ${danoId} no encontrado en OT ${otId}`);
    }
  }

  async addFoto(otId: number, dto: CreateFotoDto): Promise<OtFoto> {
    const ot = await this.findById(otId);
    this.ensureEditable(ot);

    const foto = this.fotoRepository.create({
      otId,
      tipo: dto.tipo,
      url: dto.url,
      descripcion: dto.descripcion,
    });

    return this.fotoRepository.save(foto);
  }

  async removeFoto(otId: number, fotoId: number): Promise<void> {
    const ot = await this.findById(otId);
    this.ensureEditable(ot);

    const result = await this.fotoRepository.delete({ id: fotoId, otId });
    if (result.affected === 0) {
      throw new NotFoundException(`Foto ${fotoId} no encontrada en OT ${otId}`);
    }
  }

  async saveFirmas(otId: number, dto: CreateFirmaDto): Promise<OtFirma> {
    const ot = await this.findById(otId);
    this.ensureEditable(ot);

    let firma = await this.firmaRepository.findOne({ where: { otId } });

    if (!firma) {
      firma = this.firmaRepository.create({ otId });
    }

    if (dto.firmaCliente) {
      firma.firmaCliente = dto.firmaCliente;
      firma.fechaFirmaCliente = new Date();
      firma.nombreFirmaCliente = dto.nombreFirmaCliente ?? null;
    }

    if (dto.firmaRecepcion) {
      firma.firmaRecepcion = dto.firmaRecepcion;
      firma.fechaFirmaRecepcion = new Date();
      firma.nombreFirmaRecepcion = dto.nombreFirmaRecepcion ?? null;
    }

    return this.firmaRepository.save(firma);
  }

  async savePdfEscaner(
    otId: number,
    pdfUrl: string,
    userId?: number,
  ): Promise<OrdenTrabajo> {
    const ot = await this.findById(otId);
    this.ensureEditable(ot);
    ot.pdfEscaner = pdfUrl;
    ot.updatedBy = userId ?? null;
    return this.otRepository.save(ot);
  }

  private async generateNumeroOt(): Promise<string> {
    const today = new Date();
    const datePrefix = today.toISOString().slice(0, 10).replace(/-/g, "");

    const lastOt = await this.otRepository
      .createQueryBuilder("ot")
      .where("ot.numeroOt LIKE :prefix", { prefix: `OT-${datePrefix}-%` })
      .orderBy("ot.numeroOt", "DESC")
      .getOne();

    let sequence = 1;

    if (lastOt) {
      const match = lastOt.numeroOt.match(/OT-\d{8}-(\d+)/);
      if (match) {
        sequence = parseInt(match[1], 10) + 1;
      }
    }

    return `OT-${datePrefix}-${sequence.toString().padStart(4, "0")}`;
  }

  private ensureEditable(ot: OrdenTrabajo): void {
    if (ot.estado === EstadoOT.ENTREGADA) {
      throw new BadRequestException("No se puede modificar una OT entregada");
    }
  }
}



