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

import {
  BillingService,
  GenerateInvoiceInput,
  InvoiceEventPayload,
} from '../billing/billing.service';
import { Invoice } from '../billing/entities/invoice.entity';
import { LastMileRepository } from './last-mile.repository';
import { LastMile } from './entities/last-mile.entity';

/**
 * Owns the last-mile ⇄ invoice mapping — the one place that knows how a last-mile
 * record turns into invoices, which type to use, and how it advances when paid.
 * Last-mile records are billable business entities for delivery fees, so they
 * generate their own invoices directly via {@link BillingService}. All last-mile-specific
 * type branching lives here, at the two points it belongs: invoice creation and
 * settlement (the paid handler).
 */
@Injectable()
export class LastMileInvoiceService {
  private readonly logger = new Logger(LastMileInvoiceService.name);

  constructor(
    private readonly billing: BillingService,
    private readonly lastMileRepo: LastMileRepository,
  ) {}

  /**
   * Ensure the last-mile record has its invoice, generating one from the
   * remainingPayment if absent. Called when a last-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 (invoices FK requires a companyId).
   */
  async ensureInvoiceFor(record: LastMile): Promise<Invoice | null> {
    // Check if invoice already exists
    const existing = await this.billing.findPayable(
      'last_mile' as Freight.InvoiceSource,
      record.id,
      'DELIVERY_FEE',
    );
    if (existing) return existing;

    // Can't bill without company
    const lm = record.booking ? record : (await this.lastMileRepo.findById(record.id, { relations: { booking: true } }));
    if (!lm) return null;
    if (!lm.booking?.companyId) {
      this.logger.warn(
        `Skipping invoice for last-mile record ${record.id}: no company to bill.`,
      );
      return null;
    }

    // numeric columns come back as strings — coerce before billing.
    const totalAmount = Number(record.remainingPayment) || 0;
    if (!Number.isFinite(totalAmount) || totalAmount <= 0) {
      this.logger.warn(
        `Skipping invoice for last-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;

    // Generate invoice with remainingPayment as totalAmount
    const input: GenerateInvoiceInput = {
      source: 'last_mile' as Freight.InvoiceSource,
      sourceId: record.id,
      type: 'DELIVERY_FEE',
      companyId: lm.booking!.companyId,
      companyProfileId: lm.booking!.companyProfileId || '',
      currency: truckCurrency || lm.booking!.paymentCurrency || 'ETB',
      lines: [
        {
          chargeType: 'DELIVERY',
          description: 'Last-mile delivery',
          quantity: 1,
          unitRate: totalAmount,
          amount: totalAmount,
        },
      ],
      totalAmount,
    };

    return this.billing.generateInvoice(input);
  }

  /**
   * React to a last-mile invoice being paid — the settlement branch point.
   * Advances the last-mile record to mark post-payment as completed.
   */
  @OnEvent('last_mile.invoice.paid')
  async onPaid(payload: InvoiceEventPayload): Promise<void> {
    if (payload.type === 'DELIVERY_FEE') {
      const record = await this.lastMileRepo.findById(payload.sourceId);
      if (record) {
        this.logger.log(`Last-mile invoice paid for record ${payload.sourceId}.`);
      } else {
        this.logger.warn(
          `Cannot mark last-mile record ${payload.sourceId} as paid: not found.`,
        );
      }
    }
  }
}
