import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { AccountStatus, DeliveryRouteStatus, DeliveryStatus, NotificationChannel, OrderStatus, UserRole, WalletType } from '@prisma/client';
import bcrypt from 'bcrypt';
import { PrismaService } from '../database/prisma.service';
import { FinanceService } from '../finance/finance.service';
import { OrdersGateway } from '../orders/orders.gateway';
import { TrackingService } from '../tracking/tracking.service';
import { UpdateDriverProfileDto } from './dto/update-driver-profile.dto';
import { UpsertDriverDto } from './dto/upsert-driver.dto';

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

const driverPresenceWindowMinutes = 10;

@Injectable()
export class DriversService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly trackingService: TrackingService,
    private readonly financeService: FinanceService,
    private readonly ordersGateway: OrdersGateway,
  ) {}

  index() {
    return this.prisma.driver.findMany({
      orderBy: { createdAt: 'desc' },
      include: { user: true, deliveries: true, wallet: true, _count: { select: { deliveries: true } } },
    });
  }

  async create(data: UpsertDriverDto) {
    const passwordHash = await bcrypt.hash(data.password, 12);

    return this.prisma.$transaction(async (tx) => {
      const user = await tx.user.create({
        data: {
          email: data.email.trim().toLowerCase(),
          phone: data.phone.trim(),
          passwordHash,
          role: UserRole.DRIVER,
          status: data.status ?? AccountStatus.ACTIVE,
          emailVerifiedAt: new Date(),
          phoneVerifiedAt: new Date(),
        },
      });

      const driver = await tx.driver.create({
        data: {
          userId: user.id,
          fullName: data.fullName,
          documentNumber: data.documentNumber,
          vehiclePlate: data.vehiclePlate,
          vehicleModel: data.vehicleModel,
          pixKey: data.pixKey,
          status: data.status ?? AccountStatus.ACTIVE,
        },
        include: { user: true, deliveries: true, wallet: true, _count: { select: { deliveries: true } } },
      });

      await tx.wallet.create({
        data: { userId: user.id, driverId: driver.id, type: WalletType.DRIVER, balance: 0 },
      });

      return driver;
    });
  }

  async updateAdmin(id: string, data: Partial<UpsertDriverDto>) {
    const current = await this.prisma.driver.findUnique({ where: { id } });
    if (!current) throw new NotFoundException('Entregador nao encontrado.');

    const userUpdate: Record<string, unknown> = {};
    if (data.email) userUpdate.email = data.email.trim().toLowerCase();
    if (data.phone) userUpdate.phone = data.phone.trim();
    if (data.password) userUpdate.passwordHash = await bcrypt.hash(data.password, 12);
    if (data.status) userUpdate.status = data.status;

    return this.prisma.$transaction(async (tx) => {
      if (Object.keys(userUpdate).length) {
        await tx.user.update({ where: { id: current.userId }, data: userUpdate });
      }

      return tx.driver.update({
        where: { id },
        data: {
          fullName: data.fullName,
          documentNumber: data.documentNumber,
          vehiclePlate: data.vehiclePlate,
          vehicleModel: data.vehicleModel,
          pixKey: data.pixKey,
          status: data.status,
        },
        include: { user: true, deliveries: true, wallet: true, _count: { select: { deliveries: true } } },
      });
    });
  }

  async archive(id: string) {
    const current = await this.prisma.driver.findUnique({ where: { id } });
    if (!current) throw new NotFoundException('Entregador nao encontrado.');

    return this.prisma.$transaction(async (tx) => {
      await tx.user.update({ where: { id: current.userId }, data: { status: AccountStatus.SUSPENDED } });
      return tx.driver.update({ where: { id }, data: { status: AccountStatus.SUSPENDED, isOnline: false } });
    });
  }

  async getDeliveryRoute(user: RequestUser, deliveryId: string) {
    const delivery = await this.findOwnedDelivery(user, deliveryId);
    return this.trackingService.getRouteByOrder(delivery.orderId, user);
  }

  async getActiveDeliveries(user: RequestUser) {
    const driver = await this.findDriverByUser(user);
    return this.prisma.delivery.findMany({
      where: {
        driverId: driver.id,
        status: {
          in: [DeliveryStatus.ACCEPTED, DeliveryStatus.PICKED_UP, DeliveryStatus.ON_THE_WAY],
        },
      },
      include: {
        order: {
          include: {
            restaurant: true,
            customer: true,
            address: true,
          },
        },
      },
      orderBy: { updatedAt: 'desc' },
    });
  }

  async getDeliveryOffers(user: RequestUser) {
    const driver = await this.findDriverByUser(user);
    if (!this.hasFreshPresence(driver.updatedAt) || !driver.isOnline || driver.status !== AccountStatus.ACTIVE) {
      return [];
    }

    return this.prisma.delivery.findMany({
      where: {
        driverId: driver.id,
        status: DeliveryStatus.ASSIGNED,
      },
      include: {
        order: {
          include: {
            restaurant: true,
            customer: true,
            address: true,
          },
        },
      },
      orderBy: { createdAt: 'asc' },
    });
  }

  async getProfile(user: RequestUser) {
    const driver = await this.findDriverByUser(user);
    return this.prisma.driver.findUniqueOrThrow({
      where: { id: driver.id },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            phone: true,
            status: true,
          },
        },
        wallet: true,
      },
    });
  }

  async updateOwnProfile(user: RequestUser, data: UpdateDriverProfileDto) {
    const driver = await this.findDriverByUser(user);

    return this.prisma.$transaction(async (tx) => {
      if (data.phone?.trim()) {
        await tx.user.update({
          where: { id: driver.userId },
          data: {
            phone: data.phone.trim(),
          },
        });
      }

      return tx.driver.update({
        where: { id: driver.id },
        data: {
          fullName: data.fullName?.trim(),
          vehiclePlate: data.vehiclePlate?.trim(),
          vehicleModel: data.vehicleModel?.trim(),
          pixKey: data.pixKey?.trim(),
        },
        include: {
          user: {
            select: {
              id: true,
              email: true,
              phone: true,
              status: true,
            },
          },
          wallet: true,
        },
      });
    });
  }

  async updateOwnStatus(user: RequestUser, isOnline: boolean) {
    const driver = await this.findDriverByUser(user);
    return this.prisma.driver.update({
      where: { id: driver.id },
      data: { isOnline },
    });
  }

  async acceptDelivery(user: RequestUser, deliveryId: string) {
    const driver = await this.findDriverByUser(user);
    if (!driver.isOnline || !this.hasFreshPresence(driver.updatedAt) || driver.status !== AccountStatus.ACTIVE) {
      throw new ForbiddenException('Somente motoboys ativos, online e com presença recente podem aceitar corridas.');
    }
    const delivery = await this.findOwnedDelivery(user, deliveryId);
    if (delivery.status !== DeliveryStatus.ASSIGNED) {
      throw new ForbiddenException('A corrida nao esta disponivel para aceite.');
    }

    const updated = await this.prisma.$transaction(async (tx) => {
      const acceptedDelivery = await tx.delivery.update({
        where: { id: deliveryId },
        data: {
          status: DeliveryStatus.ACCEPTED,
        },
        include: {
          order: {
            include: {
              restaurant: true,
              customer: true,
              address: true,
            },
          },
        },
      });

      await tx.order.update({
        where: { id: delivery.orderId },
        data: { status: OrderStatus.DRIVER_ACCEPTED },
      });

      await tx.deliveryRoute.upsert({
        where: { orderId: delivery.orderId },
        update: {
          driverId: driver.id,
          status: DeliveryRouteStatus.TO_RESTAURANT,
        },
        create: {
          orderId: delivery.orderId,
          driverId: driver.id,
          status: DeliveryRouteStatus.TO_RESTAURANT,
        },
      });

      return acceptedDelivery;
    });

    const payload = {
      orderId: delivery.orderId,
      deliveryId,
      driverId: driver.id,
      driverName: driver.fullName,
      etaMinutes: updated.etaMinutes,
      status: DeliveryStatus.ACCEPTED,
    };
    this.ordersGateway.emitToOrder(delivery.orderId, 'driver.assigned', payload);
    this.ordersGateway.emitToUser(driver.userId, 'driver.delivery.accepted', payload);
    return payload;
  }

  async rejectDelivery(user: RequestUser, deliveryId: string) {
    const driver = await this.findDriverByUser(user);
    const delivery = await this.findOwnedDelivery(user, deliveryId);
    if (delivery.status !== DeliveryStatus.ASSIGNED) {
      throw new ForbiddenException('A corrida nao esta disponivel para recusa.');
    }

    const nextDriver = await this.prisma.driver.findFirst({
      where: {
        id: { not: driver.id },
        isOnline: true,
        status: AccountStatus.ACTIVE,
        updatedAt: { gte: this.presenceThreshold() },
        user: { status: AccountStatus.ACTIVE },
      },
      orderBy: { updatedAt: 'asc' },
      include: {
        locations: { orderBy: { createdAt: 'desc' }, take: 1 },
      },
    });

    const reassigned = await this.prisma.$transaction(async (tx) => {
      if (!nextDriver) {
        await tx.delivery.update({
          where: { id: deliveryId },
          data: {
            driverId: null,
            status: DeliveryStatus.PENDING,
          },
        });

        await tx.deliveryRoute.upsert({
          where: { orderId: delivery.orderId },
          update: {
            driverId: null,
            status: DeliveryRouteStatus.SEARCHING_DRIVER,
          },
          create: {
            orderId: delivery.orderId,
            status: DeliveryRouteStatus.SEARCHING_DRIVER,
          },
        });

        await tx.order.update({
          where: { id: delivery.orderId },
          data: { status: OrderStatus.SEARCHING_DRIVER },
        });

        return null;
      }

      await tx.delivery.update({
        where: { id: deliveryId },
        data: {
          driverId: nextDriver.id,
          status: DeliveryStatus.ASSIGNED,
        },
      });

      await tx.deliveryRoute.upsert({
        where: { orderId: delivery.orderId },
        update: {
          driverId: nextDriver.id,
          status: DeliveryRouteStatus.SEARCHING_DRIVER,
        },
        create: {
          orderId: delivery.orderId,
          driverId: nextDriver.id,
          status: DeliveryRouteStatus.SEARCHING_DRIVER,
        },
      });

      await tx.order.update({
        where: { id: delivery.orderId },
        data: { status: OrderStatus.SEARCHING_DRIVER },
      });

      const latest = nextDriver.locations[0];
      if (latest) {
        await tx.driverLocation.create({
          data: {
            driverId: nextDriver.id,
            orderId: delivery.orderId,
            latitude: latest.latitude,
            longitude: latest.longitude,
            heading: latest.heading,
            speed: latest.speed,
            accuracy: latest.accuracy,
          },
        });
      }

      return nextDriver;
    });

    const basePayload = {
      orderId: delivery.orderId,
      deliveryId,
      rejectedByDriverId: driver.id,
      status: OrderStatus.SEARCHING_DRIVER,
    };
    this.ordersGateway.emitToOrder(delivery.orderId, 'driver.offer.rejected', basePayload);

    if (reassigned) {
      const offerPayload = {
        ...basePayload,
        driverId: reassigned.id,
        driverName: reassigned.fullName,
      };
      this.ordersGateway.emitToUser(reassigned.userId, 'driver.offer.created', offerPayload);
      this.ordersGateway.emitToOrder(delivery.orderId, 'driver.found', offerPayload);
      return { ...offerPayload, reassigned: true };
    }

    return { ...basePayload, reassigned: false };
  }

  updateLocation(user: RequestUser, deliveryId: string, body: { latitude: number; longitude: number; heading?: number; speed?: number; accuracy?: number }) {
    return this.trackingService.updateDriverLocation(user, deliveryId, body);
  }

  async confirmPickup(user: RequestUser, deliveryId: string, confirmationCode?: string) {
    const delivery = await this.findOwnedDelivery(user, deliveryId);
    const order = await this.prisma.order.findUnique({
      where: { id: delivery.orderId },
      include: {
        customer: {
          include: {
            user: true,
          },
        },
        restaurant: true,
      },
    });
    if (!order) {
      throw new NotFoundException('Pedido nao encontrado para iniciar a entrega.');
    }

    const deliveryCode = (confirmationCode ?? delivery.orderId.slice(-6)).toUpperCase();

    const updated = await this.prisma.delivery.update({
      where: { id: deliveryId },
      data: {
        status: DeliveryStatus.PICKED_UP,
        pickedUpAt: new Date(),
        order: { update: { status: OrderStatus.PICKED_UP } },
      },
    });

    await this.prisma.deliveryRoute.updateMany({
      where: { orderId: delivery.orderId },
      data: { status: DeliveryRouteStatus.TO_CUSTOMER },
    });

    await this.prisma.notification.create({
      data: {
        userId: order.customer.userId,
        title: 'Pedido saiu para entrega',
        body: `Seu pedido saiu para entrega. Informe o codigo ${deliveryCode} ao motoboy na chegada.`,
        channel: NotificationChannel.IN_APP,
        payload: {
          orderId: order.id,
          restaurantName: order.restaurant.name,
          confirmationCode: deliveryCode,
          status: OrderStatus.PICKED_UP,
        },
      },
    });

    const payload = { orderId: delivery.orderId, deliveryId, confirmationCode: deliveryCode, status: updated.status };
    this.ordersGateway.emitToOrder(delivery.orderId, 'order.picked_up', payload);
    this.ordersGateway.emitToUser(order.customer.userId, 'notification.created', {
      title: 'Pedido saiu para entrega',
      body: `Seu pedido saiu para entrega. Informe o codigo ${deliveryCode} ao motoboy na chegada.`,
      channel: NotificationChannel.IN_APP,
      userId: order.customer.userId,
      payload: {
        orderId: order.id,
        confirmationCode: deliveryCode,
      },
    });
    return payload;
  }

  async confirmDelivered(user: RequestUser, deliveryId: string, body: { confirmationCode?: string; proofImageUrl?: string }) {
    const delivery = await this.findOwnedDelivery(user, deliveryId);

    const updated = await this.prisma.delivery.update({
      where: { id: deliveryId },
      data: {
        status: DeliveryStatus.DELIVERED,
        deliveredAt: new Date(),
        proofImageUrl: body.proofImageUrl,
        order: { update: { status: OrderStatus.DELIVERED } },
      },
    });

    await this.prisma.deliveryRoute.updateMany({
      where: { orderId: delivery.orderId },
      data: { status: DeliveryRouteStatus.DELIVERED },
    });

    const settlement = await this.financeService.recordOrderSettlement(delivery.orderId);
    const payload = {
      orderId: delivery.orderId,
      deliveryId,
      confirmationCode: body.confirmationCode ?? null,
      proofImageUrl: body.proofImageUrl ?? null,
      status: updated.status,
      settlement,
    };
    this.ordersGateway.emitToOrder(delivery.orderId, 'order.delivered', payload);
    return payload;
  }

  private async findOwnedDelivery(user: RequestUser, deliveryId: string) {
    const delivery = await this.prisma.delivery.findUnique({ where: { id: deliveryId } });
    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('Entrega nao pertence ao entregador autenticado.');
    }

    return delivery;
  }

  private async findDriverByUser(user: RequestUser) {
    const driver = await this.prisma.driver.findFirst({ where: { userId: user.sub } });
    if (!driver) {
      throw new NotFoundException('Entregador autenticado nao encontrado.');
    }
    return driver;
  }

  private presenceThreshold() {
    return new Date(Date.now() - driverPresenceWindowMinutes * 60 * 1000);
  }

  private hasFreshPresence(updatedAt: Date) {
    return updatedAt.getTime() >= this.presenceThreshold().getTime();
  }
}
