import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { DeliveryRouteStatus, DeliveryStatus, OrderStatus, Prisma, UserRole } from '@prisma/client';
import { PrismaService } from '../database/prisma.service';
import { MapsService } from '../maps/maps.service';
import { OrdersGateway } from '../orders/orders.gateway';
import { CalculateRouteDto, UpdateDriverLocationDto } from './dto/tracking.dto';

interface RequestUser {
  sub: string;
  role: UserRole;
}

@Injectable()
export class TrackingService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly ordersGateway: OrdersGateway,
    private readonly mapsService: MapsService,
  ) {}

  async getOrderTracking(orderId: string, user: RequestUser) {
    const order = await this.loadOrder(orderId);
    this.assertOrderAccess(order, user);
    if (!this.isPickupPhase(order.delivery?.status)) {
      await this.ensureCustomerCoordinates(order);
    }
    return this.buildTrackingPayload(order);
  }

  async getPublicOrderTracking(orderId: string, phone: string) {
    const order = await this.loadOrder(orderId);
    const normalizedPhone = (phone ?? '').trim();
    if (!normalizedPhone || order.customer.user.phone !== normalizedPhone) {
      throw new ForbiddenException('Telefone divergente para este rastreio.');
    }
    if (!this.isPickupPhase(order.delivery?.status)) {
      await this.ensureCustomerCoordinates(order);
    }
    return this.buildTrackingPayload(order);
  }

  async getRouteByOrder(orderId: string, user: RequestUser) {
    const order = await this.loadOrder(orderId);
    this.assertOrderAccess(order, user);
    return order.deliveryRoute;
  }

  async calculateRoute(body: CalculateRouteDto) {
    const route = await this.mapsService.getRoute(
      { latitude: body.originLatitude, longitude: body.originLongitude },
      { latitude: body.destinationLatitude, longitude: body.destinationLongitude },
    );

    return {
      distanceKm: route.distanceKm,
      etaMinutes: route.durationMinutes,
      encodedPolyline: route.encodedPolyline,
      provider: route.provider,
      legs: route.legs ?? [],
    };
  }

  async recalculateEta(orderId: string, user?: RequestUser) {
    const order = await this.loadOrder(orderId);
    if (user) {
      this.assertOrderAccess(order, user);
    }

    const latestLocation = order.driverLocations[0];
    if (!latestLocation) {
      return this.buildTrackingPayload(order);
    }

    const destination = await this.resolveDestination(order);
    const routeResult = await this.mapsService.getRoute(
      { latitude: Number(latestLocation.latitude), longitude: Number(latestLocation.longitude) },
      destination,
    );

    const route = await this.prisma.deliveryRoute.upsert({
      where: { orderId },
      update: {
        driverId: order.delivery?.driverId ?? undefined,
        pickupDistanceKm: this.isPickupPhase(order.delivery?.status) ? new Prisma.Decimal(routeResult.distanceKm) : order.deliveryRoute?.pickupDistanceKm,
        deliveryDistanceKm: !this.isPickupPhase(order.delivery?.status) ? new Prisma.Decimal(routeResult.distanceKm) : order.deliveryRoute?.deliveryDistanceKm,
        estimatedPickupTime: this.isPickupPhase(order.delivery?.status) ? routeResult.durationMinutes : order.deliveryRoute?.estimatedPickupTime,
        estimatedDeliveryTime: !this.isPickupPhase(order.delivery?.status) ? routeResult.durationMinutes : order.deliveryRoute?.estimatedDeliveryTime,
        status: this.resolveRouteStatus(order.delivery?.status),
        encodedPolylineToRestaurant: this.isPickupPhase(order.delivery?.status) ? routeResult.encodedPolyline : order.deliveryRoute?.encodedPolylineToRestaurant,
        encodedPolylineToCustomer: !this.isPickupPhase(order.delivery?.status) ? routeResult.encodedPolyline : order.deliveryRoute?.encodedPolylineToCustomer,
      },
      create: {
        orderId,
        driverId: order.delivery?.driverId,
        restaurantLatitude: order.restaurant.latitude,
        restaurantLongitude: order.restaurant.longitude,
        customerLatitude: order.address?.latitude,
        customerLongitude: order.address?.longitude,
        pickupDistanceKm: this.isPickupPhase(order.delivery?.status) ? new Prisma.Decimal(routeResult.distanceKm) : null,
        deliveryDistanceKm: !this.isPickupPhase(order.delivery?.status) ? new Prisma.Decimal(routeResult.distanceKm) : null,
        estimatedPickupTime: this.isPickupPhase(order.delivery?.status) ? routeResult.durationMinutes : null,
        estimatedDeliveryTime: !this.isPickupPhase(order.delivery?.status) ? routeResult.durationMinutes : null,
        encodedPolylineToRestaurant: this.isPickupPhase(order.delivery?.status) ? routeResult.encodedPolyline : null,
        encodedPolylineToCustomer: !this.isPickupPhase(order.delivery?.status) ? routeResult.encodedPolyline : null,
        status: this.resolveRouteStatus(order.delivery?.status),
      },
    });

    const payload = {
      orderId,
      etaMinutes: routeResult.durationMinutes,
      provider: routeResult.provider,
      route,
      timestamp: new Date().toISOString(),
    };
    this.ordersGateway.emitToOrder(orderId, 'eta.updated', payload);
    this.ordersGateway.emitToOrder(orderId, 'route.updated', payload);
    return payload;
  }

  async updateDriverLocation(user: RequestUser, deliveryId: string, body: UpdateDriverLocationDto) {
    const delivery = await this.prisma.delivery.findUnique({
      where: { id: deliveryId },
      include: { driver: true },
    });

    if (!delivery) {
      throw new NotFoundException('Entrega nao encontrada.');
    }
    if (!delivery.driverId) {
      throw new ForbiddenException('Entrega ainda sem entregador.');
    }

    const driver = await this.prisma.driver.findUnique({ where: { id: delivery.driverId } });
    if (!driver || driver.userId !== user.sub) {
      throw new ForbiddenException('Esta entrega nao pertence ao entregador autenticado.');
    }

    const location = await this.prisma.driverLocation.create({
      data: {
        driverId: delivery.driverId,
        orderId: delivery.orderId,
        latitude: body.latitude,
        longitude: body.longitude,
        heading: body.heading,
        speed: body.speed,
        accuracy: body.accuracy,
      },
    });

    await this.prisma.driver.update({
      where: { id: delivery.driverId },
      data: { isOnline: true },
    });

    const routeUpdate = await this.recalculateEta(delivery.orderId);
    const payload = {
      orderId: delivery.orderId,
      deliveryId: delivery.id,
      driverId: delivery.driverId,
      location,
      route: 'route' in routeUpdate ? routeUpdate.route : null,
    };

    this.ordersGateway.emitToOrder(delivery.orderId, 'driver.location.updated', payload);
    if (payload.route && (payload.route.estimatedPickupTime ?? 999) <= 2) {
      this.ordersGateway.emitToOrder(delivery.orderId, 'driver.arriving_to_restaurant', payload);
    }
    if (payload.route && (payload.route.estimatedDeliveryTime ?? 999) <= 2 && !this.isPickupPhase(delivery.status)) {
      this.ordersGateway.emitToOrder(delivery.orderId, 'driver.arriving_to_customer', payload);
    }

    return payload;
  }

  private async loadOrder(orderId: string) {
    const order = await this.prisma.order.findUnique({
      where: { id: orderId },
      include: {
        address: true,
        customer: { include: { user: true } },
        restaurant: { include: { user: true } },
        delivery: { include: { driver: true } },
        deliveryRoute: true,
        driverLocations: { orderBy: { createdAt: 'desc' }, take: 1 },
      },
    });

    if (!order) {
      throw new NotFoundException('Pedido nao encontrado.');
    }

    return order;
  }

  private buildTrackingPayload(order: Awaited<ReturnType<TrackingService['loadOrder']>>) {
    const latestLocation = order.driverLocations[0] ?? null;
    const restaurantAddress = [
      [order.restaurant.street, order.restaurant.number].filter(Boolean).join(', '),
      order.restaurant.complement,
      order.restaurant.neighborhood,
      [order.restaurant.city, order.restaurant.state].filter(Boolean).join(' - '),
      order.restaurant.zipCode,
    ]
      .filter(Boolean)
      .join(' | ');
    const customerAddress = order.address
      ? [
          order.address.street,
          order.address.number,
          order.address.complement,
          order.address.neighborhood,
          order.address.city,
          order.address.state,
        ]
          .filter(Boolean)
          .join(', ')
      : null;
    return {
      orderId: order.id,
      status: order.status,
      trackingStatusLabel: this.mapOrderStatus(order.status),
      deliveryConfirmationCode:
        order.delivery?.status === DeliveryStatus.PICKED_UP || order.delivery?.status === DeliveryStatus.ON_THE_WAY
          ? order.id.slice(-6).toUpperCase()
          : null,
      restaurant: {
        id: order.restaurant.id,
        name: order.restaurant.name,
        latitude: order.deliveryRoute?.restaurantLatitude ?? order.restaurant.latitude,
        longitude: order.deliveryRoute?.restaurantLongitude ?? order.restaurant.longitude,
        addressLabel: restaurantAddress || null,
      },
      customer: {
        id: order.customer.id,
        name: order.customer.fullName,
        latitude: order.address?.latitude ?? order.deliveryRoute?.customerLatitude ?? null,
        longitude: order.address?.longitude ?? order.deliveryRoute?.customerLongitude ?? null,
        addressLabel: customerAddress,
      },
      driver: order.delivery?.driver
        ? {
            id: order.delivery.driver.id,
            name: order.delivery.driver.fullName,
            photo: order.delivery.driver.avatarUrl,
            rating: Number(order.delivery.driver.rating),
            vehiclePlate: order.delivery.driver.vehiclePlate,
            vehicleModel: order.delivery.driver.vehicleModel,
          }
        : null,
      delivery: order.delivery,
      route: order.deliveryRoute,
      latestLocation,
    };
  }

  private assertOrderAccess(order: Awaited<ReturnType<TrackingService['loadOrder']>>, user: RequestUser) {
    if (user.role === UserRole.ADMIN || user.role === UserRole.SUPER_ADMIN) {
      return;
    }
    if (user.role === UserRole.CUSTOMER && order.customer.userId === user.sub) {
      return;
    }
    if (user.role === UserRole.RESTAURANT && order.restaurant.userId === user.sub) {
      return;
    }
    if (user.role === UserRole.DRIVER && order.delivery?.driver && order.delivery.driver.userId === user.sub) {
      return;
    }

    throw new ForbiddenException('Voce nao pode acessar este rastreamento.');
  }

  private async resolveDestination(order: Awaited<ReturnType<TrackingService['loadOrder']>>) {
    if (this.isPickupPhase(order.delivery?.status)) {
      const restaurantCoords = await this.ensureRestaurantCoordinates(order.restaurant);
      return restaurantCoords;
    }

    const customerCoords = await this.ensureCustomerCoordinates(order);
    if (customerCoords) {
      return customerCoords;
    }

    return this.ensureRestaurantCoordinates(order.restaurant);
  }

  private async ensureRestaurantCoordinates(restaurant: Awaited<ReturnType<TrackingService['loadOrder']>>['restaurant']) {
    const geocoded = await this.mapsService.geocodeAddressStrict({
      street: restaurant.street,
      number: restaurant.number,
      complement: restaurant.complement,
      neighborhood: restaurant.neighborhood,
      city: restaurant.city,
      state: restaurant.state,
      zipCode: restaurant.zipCode,
      label: restaurant.name,
    });
    if (geocoded) {
      await this.prisma.restaurant.update({
        where: { id: restaurant.id },
        data: { latitude: geocoded.latitude, longitude: geocoded.longitude },
      });

      return { latitude: geocoded.latitude, longitude: geocoded.longitude };
    }

    const lat = restaurant.latitude ? Number(restaurant.latitude) : null;
    const lng = restaurant.longitude ? Number(restaurant.longitude) : null;
    if (lat !== null && lng !== null) {
      return { latitude: lat, longitude: lng };
    }

    return { latitude: 0, longitude: 0 };
  }

  private async ensureCustomerCoordinates(order: Awaited<ReturnType<TrackingService['loadOrder']>>) {
    const lat = order.address?.latitude ? Number(order.address.latitude) : null;
    const lng = order.address?.longitude ? Number(order.address.longitude) : null;
    if (lat !== null && lng !== null) {
      return { latitude: lat, longitude: lng };
    }
    if (!order.address) {
      return null;
    }

    const geocoded = await this.mapsService.geocodeAddressStrict({
      street: order.address.street,
      number: order.address.number,
      complement: order.address.complement,
      neighborhood: order.address.neighborhood,
      city: order.address.city,
      state: order.address.state,
      zipCode: order.address.zipCode,
    });
    if (!geocoded) {
      return null;
    }

    await this.prisma.address.update({
      where: { id: order.address.id },
      data: { latitude: geocoded.latitude, longitude: geocoded.longitude },
    });

    return { latitude: geocoded.latitude, longitude: geocoded.longitude };
  }

  private resolveRouteStatus(status?: DeliveryStatus | null): DeliveryRouteStatus {
    if (!status || status === DeliveryStatus.PENDING) {
      return DeliveryRouteStatus.SEARCHING_DRIVER;
    }
    if (status === DeliveryStatus.ASSIGNED || status === DeliveryStatus.ACCEPTED) {
      return DeliveryRouteStatus.TO_RESTAURANT;
    }
    if (status === DeliveryStatus.PICKED_UP || status === DeliveryStatus.ON_THE_WAY) {
      return DeliveryRouteStatus.TO_CUSTOMER;
    }
    if (status === DeliveryStatus.DELIVERED) {
      return DeliveryRouteStatus.DELIVERED;
    }
    return DeliveryRouteStatus.CANCELLED;
  }

  private isPickupPhase(status?: DeliveryStatus | null) {
    return !status || status === DeliveryStatus.PENDING || status === DeliveryStatus.ASSIGNED || status === DeliveryStatus.ACCEPTED;
  }

  private mapOrderStatus(status: OrderStatus) {
    const labels: Record<OrderStatus, string> = {
      CREATED: 'Pedido confirmado',
      WAITING_PAYMENT: 'Aguardando pagamento',
      PAYMENT_CONFIRMED: 'Pagamento aprovado',
      SENT_TO_RESTAURANT: 'Enviado ao restaurante',
      RESTAURANT_ACCEPTED: 'Restaurante preparando',
      PREPARING: 'Restaurante preparando',
      READY_FOR_PICKUP: 'Buscando entregador',
      SEARCHING_DRIVER: 'Buscando entregador',
      DRIVER_ACCEPTED: 'Entregador indo ao restaurante',
      DRIVER_ARRIVED: 'Entregador chegou ao restaurante',
      PICKED_UP: 'Pedido retirado',
      ON_THE_WAY: 'Entregador a caminho',
      DELIVERED: 'Pedido entregue',
      CANCELLED: 'Pedido cancelado',
      REFUNDED: 'Pedido reembolsado',
    };

    return labels[status] ?? status;
  }
}
