import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Freight } from '@edr/types';

import {
  BillingService,
  InvoiceEventPayload,
} from '../billing/billing.service';
import { Invoice } from '../billing/entities/invoice.entity';
import { FirstMileRepository } from './first-mile.repository';
import { FirstMile } from './entities/first-mile.entity';

/**
 * Owns the first-mile ⇄ invoice mapping — the one place that knows how a
 * first-mile record turns into invoices, which type to use, and how it
 * advances when paid. First-mile records are billable entities, so they
 * generate their own invoices directly via {@link BillingService}.
 */
@Injectable()
export class FirstMileInvoiceService {
  private readonly logger = new Logger(FirstMileInvoiceService.name);

  constructor(
    private readonly billing: BillingService,
    private readonly firstMileRepo: FirstMileRepository,
  ) {}

  /**
   * Ensure the first-mile record has its invoice, generating one from the
   * remaining payment if absent. Called when a first-mile record reaches a
   * billable state. Idempotent — returns the existing open invoice instead
   * of a duplicate. Returns `null` (and logs) when the record is not billable:
   * no company to bill.
   */
  async ensureInvoiceFor(record: FirstMile): Promise<Invoice | null> {
    const existing = await this.billing.findPayable(
      'first_mile' as Freight.InvoiceSource,
      record.id,
      'DELIVERY_FEE',
    );
    if (existing) return existing;

    if (!record.bookingId) {
      this.logger.warn(
        `Skipping invoice for first-mile record ${record.id}: no booking to reference.`,
      );
      return null;
    }

    // Fetch the booking to get the companyId and companyProfileId
    const fm = record.booking ? record : (await this.firstMileRepo.findById(record.id, { relations: { booking: true } }));
    if (!fm) return null;
    if (!fm.booking?.companyId) {
      this.logger.warn(
        `Skipping invoice for first-mile record ${record.id}: no company to bill.`,
      );
      return null;
    }

    // numeric columns come back as strings — coerce before the finite/>0 check.
    const totalAmount = Number(record.remainingPayment) || 0;
    if (!Number.isFinite(totalAmount) || totalAmount <= 0) {
      this.logger.warn(
        `Skipping invoice for first-mile record ${record.id}: no remaining payment.`,
      );
      return null;
    }

    // Reject mixed-currency truck sets — a single invoice can only be one
    // currency, and amounts across currencies can't be summed.
    const billableTrucks = (record.vehicleAssignments ?? []).filter(
      (a) => Number(a.distanceKm) > 0,
    );
    const currencies = [
      ...new Set(
        billableTrucks
          .map((a) => (a.vehicle as { currency?: string } | undefined)?.currency)
          .filter((c): c is string => Boolean(c)),
      ),
    ];
    if (currencies.length > 1) {
      throw new BadRequestException(
        `Cannot generate invoice: assigned trucks use mixed currencies (${currencies.join(', ')}). Assign trucks that share one currency.`,
      );
    }

    // Currency follows the truck (price/km is quoted per vehicle), falling back
    // to the booking's currency, then ETB.
    const truckCurrency =
      (record.vehicle as { currency?: string } | undefined)?.currency ||
      (record.vehicleAssignments?.[0]?.vehicle as { currency?: string } | undefined)?.currency;

    return this.billing.generateInvoice({
      source: 'first_mile' as Freight.InvoiceSource,
      sourceId: record.id,
      type: 'DELIVERY_FEE',
      companyId: fm.booking!.companyId,
      companyProfileId: fm.booking!.companyProfileId || '',
      currency: truckCurrency || fm.booking!.paymentCurrency || 'ETB',
      lines: [
        {
          chargeType: 'DELIVERY',
          description: 'First-mile delivery',
          quantity: 1,
          unitRate: totalAmount,
          amount: totalAmount,
        },
      ],
      totalAmount,
    });
  }

  /**
   * React to a first-mile invoice being paid — the settlement branch point.
   * Mark the first-mile record as having completed post-payment processing.
   */
  @OnEvent('first_mile.invoice.paid')
  async onPaid(payload: InvoiceEventPayload): Promise<void> {
    if (payload.type === 'DELIVERY_FEE') {
      const record = await this.firstMileRepo.findById(payload.sourceId);
      if (!record) {
        this.logger.warn(
          `Cannot mark unknown first-mile record ${payload.sourceId} as paid.`,
        );
        return;
      }

      this.logger.log(`First-mile invoice paid for record ${payload.sourceId}.`);
    }
  }
}
