import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { alcanceSucursalUsuario } from 'src/utils/alcance-sucursal';
import { CreateViajeDto } from './dto/create-viaje.dto';
import { UpdateViajeDto } from './dto/update-viaje.dto';
import { ListViajesDto } from './dto/list-viajes.dto';
import { CerrarViajeDto } from './dto/cerrar-viaje.dto';
import { ESTADOS_EDITABLES, ESTADO_VIAJE } from './viajes.constants';

@Injectable()
export class ViajesService {
  constructor(private readonly prisma: PrismaService) {}

  /** Correlativo por empresa: VJ-000001, VJ-000002, … */
  async siguienteNumero(empresaId: string): Promise<string> {
    const ultimo = await this.prisma.viajes.findFirst({
      where: { empresa_id: empresaId },
      orderBy: { numero: 'desc' },
      select: { numero: true },
    });
    const n = ultimo?.numero ? Number(String(ultimo.numero).replace(/\D/g, '')) : 0;
    return `VJ-${String(n + 1).padStart(6, '0')}`;
  }

  async create(dto: CreateViajeDto, empresaId: string, userId: string) {
    const numero = await this.siguienteNumero(empresaId);
    return this.prisma.viajes.create({
      data: {
        empresa_id: empresaId,
        numero,
        descripcion: dto.descripcion ?? null,
        zona: dto.zona ?? null,
        fecha_salida: new Date(dto.fecha_salida),
        fecha_retorno: dto.fecha_retorno ? new Date(dto.fecha_retorno) : null,
        sucursal_id: dto.sucursal_id ?? null,
        vendedor_id: dto.vendedor_id ?? null,
        chofer_id: dto.chofer_id ?? null,
        vehiculo_id: dto.vehiculo_id ?? null,
        km_salida: dto.km_salida ?? null,
        km_retorno: dto.km_retorno ?? null,
        observaciones: dto.observaciones ?? null,
        estado: ESTADO_VIAJE.PLANIFICADO,
        creado_por: userId,
      },
    });
  }

  async findAll(empresaId: string, filtros: ListViajesDto, usuarioId?: string) {
    const page = Math.max(1, Number(filtros.page) || 1);
    const limit = Math.min(100, Math.max(1, Number(filtros.limit) || 20));
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);

    const where: any = { empresa_id: empresaId };
    // Búsqueda server-side: antes el frontend filtraba sólo la página cargada,
    // así que un VJ-000030 era invisible mientras no se paginara hasta su página.
    if (filtros.search?.trim()) {
      const q = filtros.search.trim();
      where.OR = [
        { numero: { contains: q, mode: 'insensitive' } },
        { descripcion: { contains: q, mode: 'insensitive' } },
      ];
    }
    if (filtros.estado) where.estado = filtros.estado;
    if (filtros.vendedor_id) where.vendedor_id = filtros.vendedor_id;
    if (filtros.vehiculo_id) where.vehiculo_id = filtros.vehiculo_id;
    if (filtros.zona) where.zona = { contains: filtros.zona, mode: 'insensitive' };
    if (filtros.fecha_desde || filtros.fecha_hasta) {
      where.fecha_salida = {};
      if (filtros.fecha_desde) where.fecha_salida.gte = new Date(filtros.fecha_desde);
      if (filtros.fecha_hasta) where.fecha_salida.lte = new Date(filtros.fecha_hasta);
    }
    // Un viaje sin sucursal lo ve cualquiera con permiso; con sucursal, sólo quien la alcanza.
    if (alcance) where.OR = [{ sucursal_id: null }, { sucursal_id: { in: alcance.sucursalIds } }];

    const [data, total] = await this.prisma.$transaction([
      this.prisma.viajes.findMany({
        where,
        orderBy: [{ fecha_salida: 'desc' }, { numero: 'desc' }],
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.viajes.count({ where }),
    ]);

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

  async findOne(id: string, empresaId: string) {
    const viaje = await this.prisma.viajes.findFirst({
      where: { id, empresa_id: empresaId },
      include: { documentos: { orderBy: { created_at: 'asc' } } },
    });
    if (!viaje) throw new NotFoundException('Viaje no encontrado');
    return viaje;
  }

  /** Lanza si el viaje no admite cambios; devuelve el viaje si sí. */
  async exigirEditable(id: string, empresaId: string) {
    const viaje = await this.findOne(id, empresaId);
    if (!ESTADOS_EDITABLES.includes(viaje.estado)) {
      throw new BadRequestException(
        viaje.estado === ESTADO_VIAJE.CERRADO
          ? 'El viaje está cerrado: reabrilo para modificarlo'
          : 'El viaje está anulado',
      );
    }
    return viaje;
  }

  async update(id: string, dto: UpdateViajeDto, empresaId: string) {
    await this.exigirEditable(id, empresaId);
    return this.prisma.viajes.update({
      where: { id },
      data: {
        ...(dto.descripcion !== undefined && { descripcion: dto.descripcion }),
        ...(dto.zona !== undefined && { zona: dto.zona }),
        ...(dto.fecha_salida && { fecha_salida: new Date(dto.fecha_salida) }),
        ...(dto.fecha_retorno !== undefined && {
          fecha_retorno: dto.fecha_retorno ? new Date(dto.fecha_retorno) : null,
        }),
        ...(dto.sucursal_id !== undefined && { sucursal_id: dto.sucursal_id }),
        ...(dto.vendedor_id !== undefined && { vendedor_id: dto.vendedor_id }),
        ...(dto.chofer_id !== undefined && { chofer_id: dto.chofer_id }),
        ...(dto.vehiculo_id !== undefined && { vehiculo_id: dto.vehiculo_id }),
        ...(dto.km_salida !== undefined && { km_salida: dto.km_salida }),
        ...(dto.km_retorno !== undefined && { km_retorno: dto.km_retorno }),
        ...(dto.observaciones !== undefined && { observaciones: dto.observaciones }),
        updated_at: new Date(),
      },
    });
  }

  async cerrar(id: string, dto: CerrarViajeDto, empresaId: string, userId: string) {
    const viaje = await this.exigirEditable(id, empresaId);
    const kmRetorno = dto.km_retorno ?? viaje.km_retorno;
    const fechaRetorno = dto.fecha_retorno ? new Date(dto.fecha_retorno) : viaje.fecha_retorno;
    if (kmRetorno == null) throw new BadRequestException('Falta el km de retorno para cerrar el viaje');
    if (!fechaRetorno) throw new BadRequestException('Falta la fecha de retorno para cerrar el viaje');
    if (viaje.km_salida != null && kmRetorno < viaje.km_salida) {
      throw new BadRequestException('El km de retorno no puede ser menor al de salida');
    }
    return this.prisma.viajes.update({
      where: { id },
      data: {
        estado: ESTADO_VIAJE.CERRADO,
        km_retorno: kmRetorno,
        fecha_retorno: fechaRetorno,
        cerrado_por: userId,
        fecha_cierre: new Date(),
        updated_at: new Date(),
      },
    });
  }

  /**
   * PLANIFICADO → EN_RUTA. La spec define los dos estados pero no había forma de
   * pasar del primero al segundo: EN_RUTA sólo se alcanzaba cerrando y reabriendo.
   */
  async marcarEnRuta(id: string, empresaId: string, _userId?: string) {
    const viaje = await this.findOne(id, empresaId);
    if (viaje.estado !== ESTADO_VIAJE.PLANIFICADO) {
      throw new BadRequestException('Sólo un viaje PLANIFICADO puede marcarse en ruta');
    }
    return this.prisma.viajes.update({
      where: { id },
      // No hay columna para 'quién la marcó en ruta': el dato queda en la auditoría.
      data: { estado: ESTADO_VIAJE.EN_RUTA, updated_at: new Date() },
    });
  }

  async reabrir(id: string, empresaId: string, userId: string) {
    const viaje = await this.findOne(id, empresaId);
    if (viaje.estado !== ESTADO_VIAJE.CERRADO) {
      throw new BadRequestException('Sólo se puede reabrir un viaje cerrado');
    }
    return this.prisma.viajes.update({
      where: { id },
      data: {
        estado: ESTADO_VIAJE.EN_RUTA,
        reabierto_por: userId,
        fecha_reapertura: new Date(),
        updated_at: new Date(),
      },
    });
  }

  /**
   * Anular NO borra `viaje_documentos`: la unique (tipo, documento_id) sigue
   * ocupada y el detalle los sigue mostrando. Para que un documento pueda ir a
   * otro viaje hay que desengancharlo a mano (DELETE de la Tarea 5).
   */
  /**
   * Anula y libera: en la misma transacción marca el viaje, guarda `registro` (lo que
   * tenía) y suelta sus documentos y rendiciones para que puedan ir a otro viaje.
   * El registro lo arma ViajeDocumentosService.anularViaje.
   */
  async anular(id: string, motivo: string, empresaId: string, userId: string, registro?: Record<string, unknown>) {
    const viaje = await this.findOne(id, empresaId);
    if (viaje.estado === ESTADO_VIAJE.ANULADO) throw new BadRequestException('El viaje ya está anulado');
    const [actualizado] = await this.prisma.$transaction([
      this.prisma.viajes.update({
        where: { id },
        data: {
          estado: ESTADO_VIAJE.ANULADO,
          motivo_anulacion: motivo,
          anulado_por: userId,
          fecha_anulacion: new Date(),
          ...(registro && { documentos_al_anular: registro as any }),
          updated_at: new Date(),
        },
      }),
      this.prisma.viaje_documentos.deleteMany({ where: { viaje_id: id } }),
      this.prisma.viaje_rendiciones.deleteMany({ where: { viaje_id: id } }),
    ]);
    return actualizado;
  }
}
