import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Brackets } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { TenantAwareRepository } from '../../../common/repositories/tenant-aware.repository';
import {
  PaymentSettlementEntity,
  SettlementStatus,
  SettlementType,
} from '../../../entities/payment-settlement.entity';
import { SettlementQueryDto } from '../dto/settlement-query.dto';

/**
 * Repository for `payment_settlements`. Inherits TenantAware-scoped
 * find/save/update/softDelete from the base; adds the lookups the
 * settlement service needs that the generic API doesn't cover.
 *
 * Booking-level rollup (`findActiveForBooking`) is the hot path for
 * `recomputeBookingPaymentStatus` — runs after every payment, every
 * itinerary delta and every Settle action. Indexed by
 * (tenant_id, booking_id) where is_deleted = false.
 */
@Injectable()
export class PaymentSettlementRepository extends TenantAwareRepository<PaymentSettlementEntity> {
  constructor(
    @InjectRepository(PaymentSettlementEntity)
    repo: Repository<PaymentSettlementEntity>,
    cls: ClsService,
  ) {
    super(repo, cls);
  }

  /**
   * Active settlements for a booking, ordered by creation. Used by
   * recomputeBookingPaymentStatus and by the detail page.
   *
   * Tenant scope is applied automatically by the base `find`.
   */
  async findActiveForBooking(bookingId: string): Promise<PaymentSettlementEntity[]> {
    return this.find({
      where: { booking_id: bookingId },
      relations: { currency: true },
      order: { created_at: 'ASC' },
    });
  }

  /**
   * Detail-page hydration — includes the change-request relation so
   * the FE can render the "Triggered by Change Request" card when
   * `triggered_by_change_request_id` is set. Also includes currency
   * and supplier for the existing detail rendering.
   *
   * Tenant scope is applied automatically by the base `findOne`.
   */
  async findByIdWithDetail(id: string): Promise<PaymentSettlementEntity | null> {
    return this.findOne({
      where: { id },
      relations: {
        currency: true,
        supplier: true,
        triggered_by_change_request: true,
      },
    });
  }

  /**
   * Counts pending settlements with a non-cancelled link to the booking.
   * Used by the lock contract that prevents two settle actions racing.
   */
  async countPendingForBooking(bookingId: string): Promise<number> {
    const rows = await this.find({
      where: { booking_id: bookingId, status: SettlementStatus.PENDING },
    });
    return rows.length;
  }

  /**
   * Sum the active settled amount for a booking by type. Used by
   * recomputeBookingPaymentStatus to derive UNPAID / PARTIAL / PAID /
   * REFUND_DUE without a second query.
   */
  async sumActiveByType(
    bookingId: string,
    type: SettlementType,
  ): Promise<{ expected: number; received: number }> {
    const rows = await this.findActiveForBooking(bookingId);
    let expected = 0;
    let received = 0;
    for (const r of rows) {
      if (r.type !== type) continue;
      expected += Number(r.amount);
      received += Number(r.amount_settled);
    }
    return { expected, received };
  }

  /**
   * Paginated list with filters for the Settlements page. Joins
   * booking → query so the search box can match booking_number,
   * guest_name, guest_mobile, and the destination filter can drop
   * out-of-scope tenants automatically (tenant scope on settlements is
   * the parent guarantee).
   *
   * Returns the raw shape paginatedResponse() expects:
   * `{ items, meta: { total, page, limit, totalPages } }`.
   */
  async findPaginatedList(query: SettlementQueryDto) {
    const tenantId = this.currentTenantId();
    const page = Math.max(1, Number(query.page) || 1);
    const limit = Math.min(100, Math.max(1, Number(query.limit) || 10));
    const offset = (page - 1) * limit;

    const qb = this.repo
      .createQueryBuilder('s')
      .leftJoinAndSelect('s.booking', 'b')
      .leftJoinAndSelect('b.query', 'q')
      .leftJoinAndSelect('s.currency', 'c')
      // SUPPLIER-side rows render the supplier name on the list page
      // in place of the guest name. Joined here so the row payload
      // already carries it and the FE doesn't need a lookup.
      .leftJoinAndSelect('s.supplier', 'sup')
      .where('s.tenant_id = :tenantId', { tenantId })
      .andWhere('s.is_deleted = false')
      // Exclude rows whose booking was soft-deleted — a settlement can
      // survive a deleted booking (delete preserves rows with recorded
      // payments), so filter on the parent booking, not just s.is_deleted.
      .andWhere('b.is_deleted = false');

    if (query.status) qb.andWhere('s.status = :status', { status: query.status });
    if (query.type) qb.andWhere('s.type = :type', { type: query.type });
    if (query.party_type) qb.andWhere('s.party_type = :pt', { pt: query.party_type });
    if (query.supplier_id) qb.andWhere('s.supplier_id = :sid', { sid: query.supplier_id });
    if (query.destination_id) {
      qb.andWhere('q.destination_id = :destId', { destId: query.destination_id });
    }
    if (query.date_from) qb.andWhere('s.due_date >= :df', { df: query.date_from });
    if (query.date_to) qb.andWhere('s.due_date <= :dt', { dt: query.date_to });
    if (query.search) {
      qb.andWhere(
        new Brackets((b2) => {
          b2.where('b.booking_number ILIKE :s', { s: `%${query.search}%` })
            .orWhere('q.guest_name ILIKE :s', { s: `%${query.search}%` })
            .orWhere('q.guest_mobile ILIKE :s', { s: `%${query.search}%` })
            // SUPPLIER-side: search by supplier name so finance can
            // type a supplier's name to surface every booking they're
            // owed money on.
            .orWhere('sup.name ILIKE :s', { s: `%${query.search}%` })
            .orWhere('CAST(s.id AS TEXT) ILIKE :s', { s: `%${query.search}%` });
        }),
      );
    }

    qb.orderBy('s.created_at', 'DESC').skip(offset).take(limit);
    const [items, total] = await qb.getManyAndCount();
    return {
      items,
      meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
    };
  }

  /**
   * Aggregates for the dashboard cash card + the list page header.
   * Pending bucket = active rows in PENDING/PARTIAL status. Settled
   * bucket = SETTLED rows whose settled_date falls in the window
   * (default = current calendar month).
   */
  async statsForDashboard(from?: string, to?: string, destinationId?: string) {
    const tenantId = this.currentTenantId();
    const today = new Date().toISOString().slice(0, 10);
    const monthStart = today.slice(0, 8) + '01';
    const dateFrom = from || monthStart;
    const dateTo = to || today;

    // Window applies to booking travel start_date (q.start_date) —
    // stakeholder direction 2026-06-30 so all settlement KPIs are
    // anchored to the trip date the same way as the analytical tabs.
    // Pending bucket gets the same window so the "in this period"
    // narrative is consistent across all four KPI cards.
    const applyWindow = <T extends { andWhere: (sql: string, params?: object) => T }>(qb: T): T => {
      qb.andWhere('q.start_date BETWEEN :df AND :dt', { df: dateFrom, dt: dateTo });
      if (destinationId) {
        qb.andWhere('q.destination_id = :destinationId', { destinationId });
      }
      // Cancelled bookings must not inflate pending cash — the trip
      // will not collect / refund in the operational sense.
      qb.andWhere('b.status <> :cancelled', { cancelled: 'cancelled' });
      return qb;
    };

    const attachActiveBillScope = <
      T extends {
        innerJoin: (a: string, b: string, c: string) => T;
        leftJoin: (a: string, b: string, c: string) => T;
        andWhere: (sql: string, params?: object) => T;
      },
    >(
      qb: T,
    ): T => {
      qb.innerJoin('bookings', 'b', 'b.id = s.booking_id AND b.is_deleted = false')
        .innerJoin('queries', 'q', 'q.id = b.query_id AND q.is_deleted = false')
        .leftJoin(
          'booking_settlement_masters',
          'bsm',
          'bsm.id = s.master_id AND bsm.tenant_id = s.tenant_id AND bsm.is_deleted = false',
        )
        // Drop settlement lines whose master bill was soft-deleted.
        .andWhere('(s.master_id IS NULL OR bsm.id IS NOT NULL)');
      return qb;
    };

    const pending = (await applyWindow(
      attachActiveBillScope(
        this.repo
          .createQueryBuilder('s')
          .select('s.type', 'type')
          .addSelect('COALESCE(SUM(s.amount - s.amount_settled), 0)', 'outstanding')
          .where('s.tenant_id = :tenantId', { tenantId })
          .andWhere('s.is_deleted = false')
          .andWhere('s.status IN (:...statuses)', {
            statuses: [SettlementStatus.PENDING, SettlementStatus.PARTIAL],
          }),
      ),
    )
      .groupBy('s.type')
      .getRawMany()) as { type: SettlementType; outstanding: string }[];

    const settled = (await applyWindow(
      attachActiveBillScope(
        this.repo
          .createQueryBuilder('s')
          .select('s.type', 'type')
          .addSelect('COALESCE(SUM(s.amount_settled), 0)', 'received')
          .where('s.tenant_id = :tenantId', { tenantId })
          .andWhere('s.is_deleted = false')
          .andWhere('s.status = :status', { status: SettlementStatus.SETTLED }),
      ),
    )
      .groupBy('s.type')
      .getRawMany()) as { type: SettlementType; received: string }[];

    const pick = (rows: { type: SettlementType }[], t: SettlementType, key: string) => {
      const found = rows.find((r) => r.type === t) as Record<string, unknown> | undefined;
      return Number((found as { [k: string]: string } | undefined)?.[key] ?? 0);
    };

    return {
      pending_collections: pick(pending, SettlementType.COLLECTION, 'outstanding'),
      pending_refunds: pick(pending, SettlementType.REFUND, 'outstanding'),
      settled_collections: pick(settled, SettlementType.COLLECTION, 'received'),
      settled_refunds: pick(settled, SettlementType.REFUND, 'received'),
      window: { from: dateFrom, to: dateTo },
    };
  }
}
