import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { DataSource } from 'typeorm';

import { NotificationAudience, NotificationType } from '@edr/types';

import { FilesService } from '../files/files.service';
import { LastMileService } from '../last-mile/last-mile.service';
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
import { NotificationsService } from '../notifications/notifications.service';
import { sendCompanyChannels } from '../notifications/notify-company.util';
import { CreateInspectionReportDto } from './dto/create-inspection-report.dto';
import { UpdateInspectionReportDto } from './dto/update-inspection-report.dto';
import { WarehouseInspectionReport } from './entities/warehouse-inspection-report.entity';
import { WarehouseInventory } from './entities/warehouse-inventory.entity';
import { WarehouseInspectionRepository } from './warehouse-inspection.repository';

const INSPECTION_RESOURCE = 'warehouse-inspection-report';

@Injectable()
export class WarehouseInspectionService {
  private readonly logger = new Logger(WarehouseInspectionService.name);

  constructor(
    private readonly dataSource: DataSource,
    private readonly inspectionRepository: WarehouseInspectionRepository,
    private readonly filesService: FilesService,
    private readonly lastMileService: LastMileService,
    private readonly inbox: NotificationInboxService,
    private readonly notifications: NotificationsService,
  ) {}

  /** Create or update the inspection report for an inventory item and sync its inspectionStatus. */
  async create(inventoryId: string, dto: CreateInspectionReportDto): Promise<WarehouseInspectionReport> {
    const inventoryRepo = this.dataSource.getRepository(WarehouseInventory);
    const inventory = await inventoryRepo.findOne({ where: { id: inventoryId } });
    if (!inventory) {
      throw new NotFoundException(`Inventory item ${inventoryId} not found`);
    }

    const expected = dto.expectedWeight ?? null;
    const actual = dto.actualWeight ?? null;
    const weightLoss = expected !== null && actual !== null ? Math.max(0, expected - actual) : null;
    const inspectedAt = new Date();

    const payload = {
      inventoryId,
      bookingId: inventory.bookingId ?? null,
      reportType: dto.reportType,
      inspectionStatus: dto.inspectionStatus,
      hasDamage: dto.hasDamage ?? false,
      damageDescription: dto.damageDescription ?? null,
      hasWeightLoss: dto.hasWeightLoss ?? false,
      expectedWeight: expected,
      actualWeight: actual,
      weightLoss,
      weightLossUnit: weightLoss !== null ? 't' : null,
      hasMissingItems: dto.hasMissingItems ?? false,
      missingItemsDescription: dto.missingItemsDescription ?? null,
      remarks: dto.remarks ?? null,
      inspectedById: dto.inspectedById ?? null,
      inspectedAt,
    };

    const [existingReport] = await this.inspectionRepository.findAll({
      where: { inventoryId },
      order: { createdAt: 'DESC' },
      take: 1,
    });

    let report: WarehouseInspectionReport;
    if (existingReport) {
      await this.inspectionRepository.update(existingReport.id, payload);
      report = await this.findById(existingReport.id);
    } else {
      report = await this.inspectionRepository.create(payload);
    }

    // Mirror the latest outcome onto the inventory item so loading rules can read it.
    await inventoryRepo.update(inventoryId, {
      inspectionStatus: dto.inspectionStatus,
      inspectedAt,
    });

    if (dto.inspectionStatus === 'PASSED') {
      await this.markImportPickupReadyAndAcceptLastMile(inventoryId);
    }

    return report;
  }

  private async markImportPickupReadyAndAcceptLastMile(inventoryId: string): Promise<void> {
    const [row] = await this.dataSource.query(
      `SELECT inv.booking_id AS "bookingId",
              b.reference AS "bookingReference",
              b.company_id AS "companyId",
              b.trade_direction AS "tradeDirection",
              b.last_mile_delivery_address AS "lastMileDeliveryAddress",
              b.customer_truck_assigned_at AS "customerTruckAssignedAt",
              COALESCE(st.includes_last_mile, false) AS "serviceIncludesLastMile"
         FROM freight.warehouse_inventory inv
         LEFT JOIN freight.bookings b ON b.id = inv.booking_id
         LEFT JOIN freight.service_types st ON st.id = b.service_type_id
        WHERE inv.id = $1 AND inv.deleted_at IS NULL
        LIMIT 1`,
      [inventoryId],
    );
    if ((row?.tradeDirection ?? '').toUpperCase() !== 'IMPORT') return;

    await this.dataSource.getRepository(WarehouseInventory).update(inventoryId, {
      status: 'READY_FOR_PICKUP',
      readyForPickupAt: new Date(),
    });

    const hasLastMile =
      Boolean(row.lastMileDeliveryAddress?.trim?.()) || Boolean(row.serviceIncludesLastMile);

    if (row.bookingReference && hasLastMile) {
      await this.lastMileService.acceptBooking(row.bookingReference);
    } else if (!hasLastMile && !row.customerTruckAssignedAt) {
      // Self-haul import: goods are pickup-ready but no collection truck is
      // assigned yet — nudge the customer to assign one from the portal.
      void this.notifyTruckAssignmentNeeded(row);
    }
  }

  /** Portal nudge: import goods are ready for pickup but no customer truck is assigned. */
  private async notifyTruckAssignmentNeeded(row: {
    bookingId?: string | null;
    bookingReference?: string | null;
    companyId?: string | null;
  }): Promise<void> {
    if (!row.companyId || !row.bookingId) return;
    const body = `Booking ${row.bookingReference ?? row.bookingId} has passed inspection and is ready for pickup. Please assign your collection truck(s) from the portal to proceed.`;
    try {
      await this.inbox.notify({
        recipients: { companyId: row.companyId },
        audience: NotificationAudience.PORTAL,
        type: NotificationType.BOOKING_STATUS,
        title: 'Assign a truck for pickup',
        body,
        link: `/bookings/${row.bookingId}`,
        data: { bookingId: row.bookingId, action: 'ASSIGN_TRUCK' },
      });
      await sendCompanyChannels(this.dataSource, this.notifications, row.companyId, body);
    } catch (err) {
      this.logger.warn(
        `Truck-assignment notify failed for ${row.bookingId}: ${(err as Error).message}`,
      );
    }
  }

  async findByInventory(inventoryId: string): Promise<WarehouseInspectionReport[]> {
    return this.inspectionRepository.findAll({
      where: { inventoryId },
      order: { createdAt: 'DESC' },
    });
  }

  async findById(id: string): Promise<WarehouseInspectionReport> {
    const report = await this.inspectionRepository.findById(id);
    if (!report) {
      throw new NotFoundException(`Inspection report ${id} not found`);
    }
    return report;
  }

  async update(id: string, dto: UpdateInspectionReportDto): Promise<WarehouseInspectionReport> {
    const report = await this.findById(id);

    const expected = dto.expectedWeight ?? report.expectedWeight ?? null;
    const actual = dto.actualWeight ?? report.actualWeight ?? null;
    const weightLoss = expected !== null && actual !== null ? Math.max(0, expected - actual) : report.weightLoss ?? null;

    await this.inspectionRepository.update(id, {
      ...(dto.reportType ? { reportType: dto.reportType } : {}),
      ...(dto.inspectionStatus ? { inspectionStatus: dto.inspectionStatus } : {}),
      ...(dto.hasDamage !== undefined ? { hasDamage: dto.hasDamage } : {}),
      ...(dto.damageDescription !== undefined ? { damageDescription: dto.damageDescription } : {}),
      ...(dto.hasWeightLoss !== undefined ? { hasWeightLoss: dto.hasWeightLoss } : {}),
      expectedWeight: expected,
      actualWeight: actual,
      weightLoss,
      ...(dto.hasMissingItems !== undefined ? { hasMissingItems: dto.hasMissingItems } : {}),
      ...(dto.missingItemsDescription !== undefined ? { missingItemsDescription: dto.missingItemsDescription } : {}),
      ...(dto.remarks !== undefined ? { remarks: dto.remarks } : {}),
    });

    if (dto.inspectionStatus) {
      await this.dataSource
        .getRepository(WarehouseInventory)
        .update(report.inventoryId, { inspectionStatus: dto.inspectionStatus });
      if (dto.inspectionStatus === 'PASSED') {
        await this.markImportPickupReadyAndAcceptLastMile(report.inventoryId);
      }
    }

    return this.findById(id);
  }

  /** Attach uploaded images/documents to a report, reusing the shared Files (MinIO) module. */
  async addAttachments(reportId: string, files: Express.Multer.File[]) {
    await this.findById(reportId);
    if (!files?.length) return [];
    return this.filesService.uploadMany(reportId, INSPECTION_RESOURCE, files);
  }

  listAttachments(reportId: string) {
    return this.filesService.findByResource(reportId, INSPECTION_RESOURCE);
  }
}
