import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Cron, CronExpression } from '@nestjs/schedule';
import { NotificationAudience, NotificationType } from '@edr/types';
import { DataSource, Repository } from 'typeorm';
import { MaintenanceRepository } from './maintenance.repository';
import { MaintenanceIntervalRepository } from './maintenance-interval.repository';
import { MaintenanceSchedule, MaintenanceStatus, MaintenanceType } from './entities/maintenance-schedule.entity';
import { MaintenanceCost } from './entities/maintenance-cost.entity';
import { Vehicle, VehicleAvailability, VehicleStatus } from '../vehicles/entities/vehicle.entity';
import {
  CreateMaintenanceScheduleDto,
  CreateMaintenanceCostDto,
  UpdateMaintenanceScheduleDto,
  UpsertMaintenanceIntervalDto,
} from './dto/create-maintenance.dto';
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';

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

  constructor(
    private readonly maintenanceRepository: MaintenanceRepository,
    private readonly intervalRepository: MaintenanceIntervalRepository,
    @InjectRepository(MaintenanceSchedule)
    private readonly scheduleRepository: Repository<MaintenanceSchedule>,
    @InjectRepository(MaintenanceCost)
    private readonly costRepository: Repository<MaintenanceCost>,
    // Vehicle isn't registered in this module's TypeOrmModule.forFeature, so we
    // reach it through the global DataSource rather than @InjectRepository.
    private readonly dataSource: DataSource,
    private readonly inbox: NotificationInboxService,
  ) {}

  /** Fleet-wide next-due board — see MaintenanceRepository.getDueBoard. */
  async getDueBoard() {
    return this.maintenanceRepository.getDueBoard();
  }

  /**
   * Daily check: a vehicle's driven km (latest fuel-up odometer reading, since
   * that's the only place mileage is actually recorded) or its due date has
   * reached a SCHEDULED item's threshold → alert backoffice once.
   */
  @Cron(CronExpression.EVERY_DAY_AT_7AM, { name: 'maintenance-due-alert' })
  async sendDueAlerts(): Promise<void> {
    try {
      const due = await this.maintenanceRepository.getUnnotifiedDue();
      for (const item of due) {
        const reason =
          item.nextDueKm != null && (item.currentKm ?? 0) >= item.nextDueKm
            ? `driven ${item.currentKm} km (due at ${item.nextDueKm} km)`
            : `due ${new Date(item.nextDueDate as Date).toLocaleDateString()}`;
        await this.inbox.notify({
          recipients: { allBackoffice: true },
          audience: NotificationAudience.BACKOFFICE,
          type: NotificationType.GENERIC,
          title: `Maintenance due — ${item.plateNumber}`,
          body: `${item.plateNumber} (${item.maintenanceType}) is due for maintenance — ${reason}. ${item.description}`,
          link: `/dashboard/maintenance?vehicleId=${item.vehicleId}`,
          data: { vehicleId: item.vehicleId, scheduleId: item.id, action: 'MAINTENANCE_DUE' },
        });
        await this.scheduleRepository.update(item.id, { dueNotifiedAt: new Date() });
      }
    } catch (err) {
      this.logger.error(`sendDueAlerts failed: ${(err as Error).message}`, (err as Error).stack);
    }
  }

  /**
   * Reflect a maintenance schedule's lifecycle on the target vehicle. A vehicle
   * under maintenance is taken out of service (MAINTENANCE + BUSY); once the
   * maintenance completes or is cancelled it returns to service (ACTIVE + FREE).
   * Only the vehicle's status/availability columns are written here. The
   * assignment-side reject (first-mile/last-mile refusing MAINTENANCE vehicles)
   * lives in those excluded mile modules, not here.
   */
  private async setVehicleMaintenanceState(
    vehicleId: string,
    underMaintenance: boolean,
  ): Promise<void> {
    await this.dataSource.getRepository(Vehicle).update(vehicleId, {
      status: underMaintenance ? VehicleStatus.MAINTENANCE : VehicleStatus.ACTIVE,
      availability: underMaintenance
        ? VehicleAvailability.BUSY
        : VehicleAvailability.FREE,
    });
  }

  async scheduleMaintenanceAsync(dto: CreateMaintenanceScheduleDto): Promise<MaintenanceSchedule> {
    const schedule = this.scheduleRepository.create({
      ...dto,
      scheduledDate: new Date(dto.scheduledDate),
      nextDueDate: dto.nextDueDate ? new Date(dto.nextDueDate) : undefined,
    });
    const saved = await this.scheduleRepository.save(schedule);
    // Scheduling maintenance takes the vehicle out of the available pool.
    await this.setVehicleMaintenanceState(saved.vehicleId, true);
    return saved;
  }

  async recordMaintenanceCost(dto: CreateMaintenanceCostDto): Promise<MaintenanceCost> {
    const cost = this.costRepository.create({
      ...dto,
      incurredDate: new Date(dto.incurredDate),
    });
    return this.costRepository.save(cost);
  }

  async updateMaintenanceSchedule(
    id: string,
    dto: UpdateMaintenanceScheduleDto,
  ): Promise<MaintenanceSchedule> {
    // Status BEFORE the write: completing an already-COMPLETED schedule again
    // must not auto-create a second "next" schedule.
    const before = await this.scheduleRepository.findOneBy({ id });

    await this.scheduleRepository.update(id, {
      ...dto,
      completedDate: dto.completedDate ? new Date(dto.completedDate) : undefined,
    });
    const updated = await this.scheduleRepository.findOneBy({ id });

    // Keep the vehicle's status/availability in step with the schedule status.
    if (updated && dto.status) {
      if (
        dto.status === MaintenanceStatus.COMPLETED ||
        dto.status === MaintenanceStatus.CANCELLED
      ) {
        // Maintenance finished/aborted → vehicle back in service.
        await this.setVehicleMaintenanceState(updated.vehicleId, false);

        // First transition into COMPLETED with an odometer → auto-schedule next.
        if (
          dto.status === MaintenanceStatus.COMPLETED &&
          before?.status !== MaintenanceStatus.COMPLETED &&
          updated.odometerReading != null
        ) {
          await this.scheduleNextMaintenance(updated);
        }
      } else if (dto.status === MaintenanceStatus.IN_PROGRESS) {
        // Maintenance started → keep the vehicle out of service.
        await this.setVehicleMaintenanceState(updated.vehicleId, true);
      }
    }

    return updated!;
  }

  /** Define/adjust how often a vehicle needs a service ("oil change every 10,000 km"). */
  async upsertInterval(dto: UpsertMaintenanceIntervalDto) {
    return this.intervalRepository.upsertInterval(
      dto.vehicleId,
      dto.maintenanceType,
      dto.serviceItem ?? null,
      dto.intervalKm ?? null,
      dto.intervalDays ?? null,
      dto.description ?? null,
    );
  }

  async getIntervals(vehicleId: string) {
    return this.intervalRepository.getActiveIntervals(vehicleId);
  }

  async deactivateInterval(id: string): Promise<{ id: string; deactivated: boolean }> {
    await this.intervalRepository.deactivate(id);
    return { id, deactivated: true };
  }

  /**
   * Auto-schedule the next service after a completion: matched on the
   * completed schedule's (type, serviceItem) interval; one SCHEDULED row
   * carrying BOTH thresholds when the interval defines km and days —
   * whichever is crossed first makes it due.
   */
  private async scheduleNextMaintenance(completed: MaintenanceSchedule): Promise<void> {
    try {
      const interval = await this.intervalRepository.getByVehicleAndType(
        completed.vehicleId,
        completed.maintenanceType as MaintenanceType,
        completed.serviceItem,
      );

      if (!interval) return; // No interval defined, skip auto-scheduling

      const now = new Date();
      const completedKm = Number(completed.odometerReading ?? 0);
      const intervalKm = Number(interval.intervalKm ?? 0);
      const intervalDays = Number(interval.intervalDays ?? 0);
      if (intervalKm <= 0 && intervalDays <= 0) return;

      const nextDueKm = intervalKm > 0 ? completedKm + intervalKm : undefined;
      const nextDueDate =
        intervalDays > 0
          ? new Date(now.getTime() + intervalDays * 24 * 60 * 60 * 1000)
          : undefined;

      const label = interval.serviceItem ? `${interval.serviceItem}: ` : '';
      const due = [
        nextDueKm != null ? `${nextDueKm} km` : null,
        nextDueDate != null ? nextDueDate.toISOString().slice(0, 10) : null,
      ]
        .filter(Boolean)
        .join(' / ');

      await this.scheduleRepository.save(
        this.scheduleRepository.create({
          vehicleId: completed.vehicleId,
          maintenanceType: completed.maintenanceType,
          serviceItem: completed.serviceItem ?? interval.serviceItem ?? null,
          description: `${label}${interval.description || completed.description} (next due: ${due})`,
          scheduledDate: now,
          nextDueKm,
          nextDueDate,
          status: MaintenanceStatus.SCHEDULED,
        }),
      );
    } catch (err) {
      this.logger.error(
        `Failed to schedule next maintenance for vehicle ${completed.vehicleId}: ${(err as Error).message}`,
        (err as Error).stack,
      );
    }
  }

  async getUpcomingMaintenance(vehicleId: string) {
    return this.maintenanceRepository.getUpcomingMaintenance(vehicleId);
  }

  async getMaintenanceHistory(vehicleId: string, monthsBack: number = 12) {
    const endDate = new Date();
    const startDate = new Date(endDate.getFullYear(), endDate.getMonth() - monthsBack, 1);
    return this.maintenanceRepository.getMaintenanceCosts(vehicleId, startDate, endDate);
  }

  async getFleetMaintenanceStats(monthsBack: number = 12) {
    const endDate = new Date();
    const startDate = new Date(endDate.getFullYear(), endDate.getMonth() - monthsBack, 1);

    const costs = await this.costRepository
      .createQueryBuilder('cost')
      .where('cost.incurredDate BETWEEN :startDate AND :endDate', { startDate, endDate })
      .getMany();

    const totalCost = costs.reduce((sum: number, c: MaintenanceCost) => sum + Number(c.costAmount), 0);

    return {
      totalCost,
      numberOfMaintenanceItems: costs.length,
      averageCostPerMaintenance: costs.length > 0 ? totalCost / costs.length : 0,
      costByType: this.groupCostsByType(costs),
    };
  }

  async getVehicleMaintenanceStats(vehicleId: string, monthsBack: number = 12) {
    const endDate = new Date();
    const startDate = new Date(endDate.getFullYear(), endDate.getMonth() - monthsBack, 1);

    const costs = await this.maintenanceRepository.getMaintenanceCosts(vehicleId, startDate, endDate);
    const totalCost = costs.reduce((sum: number, c: MaintenanceCost) => sum + Number(c.costAmount), 0);

    return {
      vehicleId,
      totalCost,
      numberOfMaintenanceItems: costs.length,
      averageCostPerMaintenance: costs.length > 0 ? totalCost / costs.length : 0,
      costByType: this.groupCostsByType(costs),
    };
  }

  private groupCostsByType(costs: MaintenanceCost[]) {
    const grouped: Record<string, number> = {};
    costs.forEach((c) => {
      if (!grouped[c.costType]) grouped[c.costType] = 0;
      grouped[c.costType] += Number(c.costAmount);
    });
    return grouped;
  }
}
