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

import {
  WarehouseFeeInvoice,
  WarehouseInvoiceStatus,
  WarehouseInvoiceType,
} from './entities/warehouse-fee-invoice.entity';
import { WarehouseFeeType } from './entities/warehouse-fee-invoice-item.entity';
import { WarehouseFeeInvoiceItemRepository } from './warehouse-fee-invoice-item.repository';
import { WarehouseFeeInvoiceRepository } from './warehouse-fee-invoice.repository';
import { WarehouseFeeService } from './warehouse-fee.service';

interface GenerateOptions {
  confirmZero?: boolean;
  performedBy?: string;
}

export interface PayInvoiceDto {
  amount: number;
  method?: string;
  reference?: string;
}

/** Invoices that still owe money and therefore block terminal release. */
const BLOCKING_STATUSES: WarehouseInvoiceStatus[] = ['ISSUED', 'PARTIALLY_PAID'];
const ACTIVE_STATUSES: WarehouseInvoiceStatus[] = ['ISSUED', 'PARTIALLY_PAID', 'PAID'];

@Injectable()
export class WarehouseInvoiceService {
  constructor(
    private readonly dataSource: DataSource,
    private readonly invoiceRepository: WarehouseFeeInvoiceRepository,
    private readonly itemRepository: WarehouseFeeInvoiceItemRepository,
    private readonly feeService: WarehouseFeeService,
  ) {}

  // ── Generation ───────────────────────────────────────────────────────────
  async generateForInventory(inventoryId: string, opts: GenerateOptions = {}): Promise<WarehouseFeeInvoice> {
    const [item] = await this.dataSource.query(
      `SELECT inv.id, inv.booking_id AS "bookingId", inv.warehouse_id AS "warehouseId",
              inv.yard_id AS "yardId", inv.zone_id AS "zoneId", inv.arrived_at AS "arrivedAt",
              w.facility_id AS "facilityId",
              b.company_id AS "customerId", b.freight_type AS "freightType"
         FROM freight.warehouse_inventory inv
         LEFT JOIN freight.warehouses w ON w.id = inv.warehouse_id
         LEFT JOIN freight.bookings b ON b.id = inv.booking_id
        WHERE inv.id = $1 AND inv.deleted_at IS NULL`,
      [inventoryId],
    );
    if (!item) throw new NotFoundException(`Inventory item ${inventoryId} not found`);

    // Dedup: only one active (non-cancelled) invoice per inventory item.
    const active = await this.invoiceRepository.findAll({ where: { inventoryId } });
    if (active.some((inv) => ACTIVE_STATUSES.includes(inv.status))) {
      throw new ConflictException(
        'An active warehouse fee invoice already exists for this item. Cancel it before generating a new one.',
      );
    }

    const previews = await this.feeService.previewForInventory(inventoryId);
    const isContainer = (item.freightType ?? '').toUpperCase() === 'CONTAINER';

    const items = previews
      .filter((p) => p.amount > 0)
      .map((p) => {
        const feeType: WarehouseFeeType =
          p.ruleType === 'STORAGE_FEE'
            ? 'STORAGE_FEE'
            : isContainer
              ? 'CONTAINER_DEMURRAGE'
              : 'BULK_DEMURRAGE';
        return {
          feeRuleId: p.ruleId,
          feeType,
          description:
            p.ruleType === 'STORAGE_FEE'
              ? `Storage fee — ${p.chargeableDays} chargeable day(s) after ${p.freeDays} free`
              : `${isContainer ? 'Container' : 'Bulk'} demurrage — ${p.chargeableDays} chargeable day(s) after ${p.freeDays} free`,
          quantity: p.chargeableDays,
          unitRate: p.ratePerDay,
          amount: p.amount,
          currency: p.currency,
          chargeableDays: p.chargeableDays,
          freeDays: p.freeDays,
        };
      });

    const subtotal = items.reduce((s, i) => s + i.amount, 0);
    const total = subtotal; // tax model can be layered on later

    if (total <= 0 && !opts.confirmZero) {
      throw new BadRequestException('No payable warehouse fee found for this item.');
    }

    const hasDemurrage = items.some((i) => i.feeType !== 'STORAGE_FEE');
    const hasStorage = items.some((i) => i.feeType === 'STORAGE_FEE');
    const invoiceType: WarehouseInvoiceType =
      hasDemurrage && hasStorage ? 'MIXED_WAREHOUSE_FEES' : hasStorage ? 'STORAGE_FEE' : 'DEMURRAGE';

    const currency = items[0]?.currency ?? 'USD';
    const now = new Date();
    const periodEnd = previews[0] ? new Date(previews[0].endDate) : now;

    const invoice = await this.invoiceRepository.create({
      invoiceNumber: await this.nextInvoiceNumber(),
      bookingId: item.bookingId ?? null,
      customerId: item.customerId ?? null,
      inventoryId,
      facilityId: item.facilityId ?? null,
      warehouseId: item.warehouseId ?? null,
      yardId: item.yardId ?? null,
      zoneId: item.zoneId ?? null,
      invoiceType,
      status: 'ISSUED',
      subtotalAmount: subtotal,
      taxAmount: 0,
      totalAmount: total,
      paidAmount: 0,
      balanceAmount: total,
      currency,
      periodStart: item.arrivedAt ?? null,
      periodEnd,
      issuedAt: now,
      payments: [],
      notes: opts.performedBy ? `Generated by ${opts.performedBy}` : null,
    });

    for (const it of items) {
      await this.itemRepository.create({ invoiceId: invoice.id, ...it });
    }

    return this.findById(invoice.id);
  }

  /** WHF-YYYYMMDD-00001 — sequential per day. */
  private async nextInvoiceNumber(): Promise<string> {
    const now = new Date();
    const ymd = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
    const prefix = `WHF-${ymd}-`;
    const [row] = await this.dataSource.query(
      `SELECT COALESCE(MAX(CAST(split_part(invoice_number, '-', 3) AS int)), 0) AS seq
         FROM freight.warehouse_fee_invoices WHERE invoice_number LIKE $1`,
      [`${prefix}%`],
    );
    const next = Number(row?.seq ?? 0) + 1;
    return `${prefix}${String(next).padStart(5, '0')}`;
  }

  // ── Reads ────────────────────────────────────────────────────────────────
  async findById(id: string): Promise<WarehouseFeeInvoice & { items: unknown[] }> {
    const invoice = await this.invoiceRepository.findById(id);
    if (!invoice) throw new NotFoundException(`Invoice ${id} not found`);
    const items = await this.itemRepository.findAll({ where: { invoiceId: id } });
    return { ...invoice, items } as WarehouseFeeInvoice & { items: unknown[] };
  }

  listForInventory(inventoryId: string): Promise<WarehouseFeeInvoice[]> {
    return this.invoiceRepository.findAll({ where: { inventoryId }, order: { createdAt: 'DESC' } });
  }

  listForBooking(bookingId: string): Promise<WarehouseFeeInvoice[]> {
    return this.invoiceRepository.findAll({ where: { bookingId }, order: { createdAt: 'DESC' } });
  }

  findAll(filter: Partial<Pick<WarehouseFeeInvoice, 'status' | 'invoiceType' | 'warehouseId' | 'facilityId' | 'customerId' | 'bookingId'>>): Promise<WarehouseFeeInvoice[]> {
    const where = Object.fromEntries(Object.entries(filter).filter(([, v]) => v != null));
    return this.invoiceRepository.findAll({ where, order: { createdAt: 'DESC' } });
  }

  // ── State changes ────────────────────────────────────────────────────────
  async cancel(id: string): Promise<WarehouseFeeInvoice> {
    const invoice = await this.invoiceRepository.findById(id);
    if (!invoice) throw new NotFoundException(`Invoice ${id} not found`);
    if (invoice.status === 'PAID') throw new BadRequestException('A paid invoice cannot be cancelled.');
    const updated = await this.invoiceRepository.update(id, { status: 'CANCELLED', cancelledAt: new Date() });
    return updated as WarehouseFeeInvoice;
  }

  /** Record a payment against the invoice and sync status (links to existing payment flow). */
  async pay(id: string, dto: PayInvoiceDto): Promise<WarehouseFeeInvoice> {
    const invoice = await this.invoiceRepository.findById(id);
    if (!invoice) throw new NotFoundException(`Invoice ${id} not found`);
    if (invoice.status === 'CANCELLED') throw new BadRequestException('Cannot pay a cancelled invoice.');
    if (invoice.status === 'PAID') throw new BadRequestException('Invoice is already fully paid.');
    if (!(dto.amount > 0)) throw new BadRequestException('Payment amount must be greater than zero.');

    const paidAmount = Number(invoice.paidAmount) + dto.amount;
    const total = Number(invoice.totalAmount);
    const balance = Math.max(0, Math.round((total - paidAmount) * 100) / 100);
    const fullyPaid = paidAmount >= total;

    const payments = [
      ...(invoice.payments ?? []),
      { amount: dto.amount, method: dto.method ?? null, reference: dto.reference ?? null, paidAt: new Date().toISOString() },
    ];

    const updated = await this.invoiceRepository.update(id, {
      paidAmount: Math.round(paidAmount * 100) / 100,
      balanceAmount: balance,
      status: fullyPaid ? 'PAID' : 'PARTIALLY_PAID',
      paidAt: fullyPaid ? new Date() : invoice.paidAt ?? null,
      payments,
    });
    return updated as WarehouseFeeInvoice;
  }

  // ── Release blocking ──────────────────────────────────────────────────────
  /** Returns the first unpaid invoice that blocks terminal release, or null. */
  async findBlockingInvoice(inventoryId: string): Promise<WarehouseFeeInvoice | null> {
    const invoices = await this.invoiceRepository.findAll({ where: { inventoryId } });
    return invoices.find((inv) => BLOCKING_STATUSES.includes(inv.status)) ?? null;
  }
}
