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

import { CreateFeeRuleDto, UpdateFeeRuleDto } from './dto/fee-rule.dto';
import { FeeRuleType, WarehouseFeeRule } from './entities/warehouse-fee-rule.entity';
import { WarehouseFeeRuleRepository } from './warehouse-fee-rule.repository';

interface ItemAttributes {
  arrivedAt: Date | null;
  gateClearedAt: Date | null;
  releaseDate: Date | null;
  freightType: string | null;
  tradeDirection: string | null;
  cargoTypeCode: string | null;
  containerTypeCode: string | null;
  facilityId: string | null;
  warehouseId: string | null;
  yardId: string | null;
  zoneId: string | null;
}

export interface FeePreview {
  ruleType: FeeRuleType;
  ruleId: string | null;
  ruleName: string | null;
  freeDays: number;
  ratePerDay: number;
  currency: string;
  startDate: string | null;
  endDate: string;
  endIsOpen: boolean; // true when still accruing (no release/gate-clear yet)
  elapsedDays: number;
  chargeableDays: number;
  amount: number;
}

const MS_PER_DAY = 24 * 60 * 60 * 1000;

@Injectable()
export class WarehouseFeeService {
  constructor(
    private readonly dataSource: DataSource,
    private readonly feeRuleRepository: WarehouseFeeRuleRepository,
  ) {}

  // ── Rule CRUD ──────────────────────────────────────────────────────────────
  listRules(): Promise<WarehouseFeeRule[]> {
    return this.feeRuleRepository.findAll({ order: { ruleType: 'ASC', priority: 'ASC' } });
  }

  createRule(dto: CreateFeeRuleDto): Promise<WarehouseFeeRule> {
    return this.feeRuleRepository.create({ isActive: true, priority: 100, currency: 'USD', ...dto });
  }

  async updateRule(id: string, dto: UpdateFeeRuleDto): Promise<WarehouseFeeRule> {
    const updated = await this.feeRuleRepository.update(id, dto);
    if (!updated) throw new NotFoundException(`Fee rule ${id} not found`);
    return updated;
  }

  deleteRule(id: string): Promise<void> {
    return this.feeRuleRepository.softDelete(id);
  }

  private async loadItem(inventoryId: string): Promise<ItemAttributes> {
    const [row] = await this.dataSource.query(
      `SELECT inv.arrived_at        AS "arrivedAt",
              inv.gate_cleared_at   AS "gateClearedAt",
              inv.release_date      AS "releaseDate",
              inv.warehouse_id      AS "warehouseId",
              inv.yard_id           AS "yardId",
              inv.zone_id           AS "zoneId",
              w.facility_id         AS "facilityId",
              b.freight_type        AS "freightType",
              b.trade_direction     AS "tradeDirection",
              cgt.code              AS "cargoTypeCode",
              ctt.code              AS "containerTypeCode"
         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
         LEFT JOIN freight.cargoes cg ON cg.id = inv.cargo_id
         LEFT JOIN freight.cargo_types cgt ON cgt.id = cg.cargo_type_id
         LEFT JOIN freight.containers ct ON ct.id = inv.container_id
         LEFT JOIN freight.container_types ctt ON ctt.id = ct.container_type_id
        WHERE inv.id = $1 AND inv.deleted_at IS NULL`,
      [inventoryId],
    );
    if (!row) throw new NotFoundException(`Inventory item ${inventoryId} not found`);
    return row;
  }

  private matchScore(rule: WarehouseFeeRule, item: ItemAttributes): number | null {
    // Returns specificity score (#matched non-null scope fields), or null if any constraint fails.
    let score = 0;
    const check = (ruleVal: string | null | undefined, itemVal: string | null) => {
      if (ruleVal == null) return true;
      if (itemVal != null && ruleVal.toUpperCase() === itemVal.toUpperCase()) {
        score += 1;
        return true;
      }
      return false;
    };
    if (!check(rule.freightType, item.freightType)) return null;
    if (!check(rule.tradeDirection, item.tradeDirection)) return null;
    if (!check(rule.cargoTypeCode, item.cargoTypeCode)) return null;
    if (!check(rule.containerType, item.containerTypeCode)) return null;
    if (!check(rule.facilityId, item.facilityId)) return null;
    if (!check(rule.warehouseId, item.warehouseId)) return null;
    if (!check(rule.yardId, item.yardId)) return null;
    if (!check(rule.zoneId, item.zoneId)) return null;
    return score;
  }

  private bestRule(rules: WarehouseFeeRule[], item: ItemAttributes): WarehouseFeeRule | null {
    let best: WarehouseFeeRule | null = null;
    let bestScore = -1;
    for (const rule of rules) {
      const score = this.matchScore(rule, item);
      if (score == null) continue;
      if (score > bestScore || (score === bestScore && best && rule.priority < best.priority)) {
        best = rule;
        bestScore = score;
      }
    }
    return best;
  }

  private compute(ruleType: FeeRuleType, rule: WarehouseFeeRule | null, item: ItemAttributes, now: Date): FeePreview {
    const start = item.arrivedAt ? new Date(item.arrivedAt) : null;
    const endDate = item.gateClearedAt ?? item.releaseDate ?? now;
    const endIsOpen = !item.gateClearedAt && !item.releaseDate;
    const freeDays = rule?.freeDays ?? 0;
    const ratePerDay = Number(rule?.ratePerDay ?? 0);

    const elapsedDays = start
      ? Math.max(0, Math.ceil((new Date(endDate).getTime() - start.getTime()) / MS_PER_DAY))
      : 0;
    const chargeableDays = Math.max(0, elapsedDays - freeDays);
    const amount = Math.round(chargeableDays * ratePerDay * 100) / 100;

    return {
      ruleType,
      ruleId: rule?.id ?? null,
      ruleName: rule?.name ?? null,
      freeDays,
      ratePerDay,
      currency: rule?.currency ?? 'USD',
      startDate: start ? start.toISOString() : null,
      endDate: new Date(endDate).toISOString(),
      endIsOpen,
      elapsedDays,
      chargeableDays,
      amount,
    };
  }

  /** Preview demurrage + storage fees for an inventory item using the most specific active rules. */
  async previewForInventory(inventoryId: string): Promise<FeePreview[]> {
    const item = await this.loadItem(inventoryId);
    const rules = await this.feeRuleRepository.findAll({ where: { isActive: true } });
    const now = new Date();

    const byType: FeeRuleType[] = ['DEMURRAGE_FEE', 'STORAGE_FEE'];
    return byType.map((type) =>
      this.compute(type, this.bestRule(rules.filter((r) => r.ruleType === type), item), item, now),
    );
  }
}
