import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TaxSlabEntity } from '../../entities/tax-slab.entity';
import { TaxTypeEntity } from '../../entities/tax-type.entity';
import { TaxComponentEntity } from '../../entities/tax-component.entity';
import { DestinationEntity } from '../../entities/destination.entity';
import { TransportServiceEntity } from '../../entities/transport-service.entity';
import { ActivityEntity } from '../../entities/activity.entity';
import { SupplierEntity } from '../../entities/supplier.entity';
import type { TaxSlabSnapshotInput } from './tax-bifurcation.helper';

/**
 * TaxSlabResolverService — translates a `tax_slabs.id` into the
 * `TaxSlabSnapshotInput` shape the bifurcation helper consumes, AND
 * walks the effective-slab resolver hierarchy for each line type.
 *
 * Hierarchy (stakeholder-locked 2026-06-12):
 *
 *   hotel       → destinations.tax_slab_id
 *   special     → destinations.tax_slab_id
 *   transport   → transport_services.tax_slab_id ?? destinations.tax_slab_id
 *   activity    → activities.tax_slab_id        ?? destinations.tax_slab_id
 *   supplier    → suppliers.tax_slab_id         ?? destinations.tax_slab_id
 *
 * Inactive slabs (`is_active = false`) and soft-deleted slabs/components
 * are skipped — when a service-master override points at an inactive
 * slab, the resolver falls through to the destination default rather
 * than refusing the save.
 *
 * Snapshots are returned by value — component names + rates captured
 * at resolve time. Stays consistent even if the underlying slab gets
 * renamed or has components added/removed later (the line snapshot
 * the caller writes is frozen).
 */
@Injectable()
export class TaxSlabResolverService {
  constructor(
    @InjectRepository(TaxSlabEntity) private readonly slabRepo: Repository<TaxSlabEntity>,
    @InjectRepository(TaxTypeEntity) private readonly typeRepo: Repository<TaxTypeEntity>,
    @InjectRepository(TaxComponentEntity) private readonly compRepo: Repository<TaxComponentEntity>,
    @InjectRepository(DestinationEntity) private readonly destRepo: Repository<DestinationEntity>,
    @InjectRepository(TransportServiceEntity)
    private readonly transportRepo: Repository<TransportServiceEntity>,
    @InjectRepository(ActivityEntity) private readonly activityRepo: Repository<ActivityEntity>,
    @InjectRepository(SupplierEntity) private readonly supplierRepo: Repository<SupplierEntity>,
  ) {}

  /**
   * Load the full snapshot input from a slab id. Returns null when:
   *   - slabId is null/undefined,
   *   - the slab isn't found, is_deleted, or is_active=false,
   *   - the slab's parent tax_type is is_deleted.
   *
   * Component_rates keys reference TaxComponent.id; the load joins the
   * active components by id and emits them in `sort_order` so the
   * snapshot's component order matches what ops sees in the UI.
   */
  async loadSnapshot(slabId: string | null | undefined): Promise<TaxSlabSnapshotInput | null> {
    if (!slabId) return null;
    const slab = await this.slabRepo.findOne({
      where: { id: slabId, is_deleted: false, is_active: true },
    });
    if (!slab) return null;
    const taxType = await this.typeRepo.findOne({
      where: { id: slab.tax_type_id, is_deleted: false },
    });
    if (!taxType) return null;
    const components = await this.compRepo.find({
      where: { tax_type_id: slab.tax_type_id, is_deleted: false, is_active: true },
      order: { sort_order: 'ASC', created_at: 'ASC' },
    });
    const rates = slab.component_rates || {};
    const out: TaxSlabSnapshotInput = {
      slab_id: slab.id,
      slab_name: taxType.name,
      total_rate: Number(slab.total_rate) || 0,
      components: components
        .filter((c) => c.id in rates)
        .map((c) => ({ name: c.name, rate: Number(rates[c.id]) || 0 })),
    };
    return out;
  }

  /**
   * Resolve the effective slab for a HOTEL or SPECIAL line — neither
   * has a service-master, so the destination is the only source.
   *
   * Two-tier lookup:
   *   1. `destinations.tax_slab_id` — explicit "default" FK on the
   *      destination. When set, this single slab wins and we don't
   *      look at the destination's slab list.
   *   2. Otherwise, roll up EVERY active `tax_slabs.destination_id =
   *      this destination` row into one snapshot — mirrors the FE chip
   *      query (`useTaxSlabsForDestination`) and the QuotePriceSummary
   *      bifurcation aggregator so the saved snapshot and the displayed
   *      bifurcation agree. Without this fallback, lines silently save
   *      with `tax_snapshot = null` whenever ops creates slabs via
   *      Settings → Tax Types without "promoting" one as the
   *      destination default (the current FE has no UI for that), and
   *      the invoice bifurcation reads as base=total / tax=0.
   *
   * Returns null only when neither layer finds anything.
   */
  async resolveForHotelOrSpecial(destinationId: string): Promise<TaxSlabSnapshotInput | null> {
    const dest = await this.destRepo.findOne({
      where: { id: destinationId, is_deleted: false },
      select: { id: true, tax_slab_id: true } as any,
    });
    if (dest?.tax_slab_id) {
      const explicit = await this.loadSnapshot(dest.tax_slab_id);
      if (explicit) return explicit;
    }
    return this.rollupActiveDestinationSlabs(destinationId);
  }

  /**
   * Aggregate every active slab on a destination into a single
   * TaxSlabSnapshotInput. Multiple slabs (e.g. VAT + Tourism Fee on
   * Dubai) sum their `total_rate` and flatten their components into
   * one list. Mirrors the FE rollup in QuotePriceSummary and
   * BookingPriceSummary so the saved snapshot matches what ops sees
   * on screen.
   *
   * Returns null when the destination has zero active slabs.
   * `slab_id` is null when more than one slab contributes (the
   * snapshot is no longer a single-slab reference).
   */
  private async rollupActiveDestinationSlabs(
    destinationId: string,
  ): Promise<TaxSlabSnapshotInput | null> {
    const slabs = await this.slabRepo.find({
      where: { destination_id: destinationId, is_deleted: false, is_active: true },
      order: { created_at: 'ASC' },
    });
    if (slabs.length === 0) return null;
    let totalRate = 0;
    const allComponents: Array<{ name: string; rate: number }> = [];
    const names: string[] = [];
    for (const slab of slabs) {
      const taxType = await this.typeRepo.findOne({
        where: { id: slab.tax_type_id, is_deleted: false },
      });
      if (!taxType) continue;
      const rate = Number(slab.total_rate) || 0;
      totalRate += rate;
      names.push(taxType.name);
      const rates = slab.component_rates || {};
      const components = await this.compRepo.find({
        where: { tax_type_id: slab.tax_type_id, is_deleted: false, is_active: true },
        order: { sort_order: 'ASC', created_at: 'ASC' },
      });
      const matched = components.filter((c) => c.id in rates);
      if (matched.length > 0) {
        allComponents.push(
          ...matched.map((c) => ({ name: c.name, rate: Number(rates[c.id]) || 0 })),
        );
      } else {
        // Slab without per-component breakdown — emit the slab as a
        // single component so bifurcation still has a row to split.
        allComponents.push({ name: taxType.name, rate });
      }
    }
    return {
      slab_id: slabs.length === 1 ? slabs[0].id : null,
      slab_name: names.join(' + ') || null,
      total_rate: totalRate,
      components: allComponents,
    };
  }

  /**
   * Resolve the effective slab for a TRANSPORT line. Service-master
   * override wins over the destination default; both are silently
   * skipped if they point at an inactive/deleted slab so the line
   * resolves at zero rather than crashing.
   */
  async resolveForTransport(
    transportServiceId: string | null,
    destinationId: string,
  ): Promise<TaxSlabSnapshotInput | null> {
    if (transportServiceId) {
      const svc = await this.transportRepo.findOne({
        where: { id: transportServiceId, is_deleted: false },
        select: { id: true, tax_slab_id: true } as any,
      });
      const snap = await this.loadSnapshot(svc?.tax_slab_id ?? null);
      if (snap) return snap;
    }
    return this.resolveForHotelOrSpecial(destinationId);
  }

  /**
   * Resolve the effective slab for an ACTIVITY line. Same hierarchy as
   * transport — activity-master override → destination default.
   */
  async resolveForActivity(
    activityId: string | null,
    destinationId: string,
  ): Promise<TaxSlabSnapshotInput | null> {
    if (activityId) {
      const a = await this.activityRepo.findOne({
        where: { id: activityId, is_deleted: false },
        select: { id: true, tax_slab_id: true } as any,
      });
      const snap = await this.loadSnapshot(a?.tax_slab_id ?? null);
      if (snap) return snap;
    }
    return this.resolveForHotelOrSpecial(destinationId);
  }

  /**
   * Resolve the SUPPLIER's slab for the cost-side bifurcation written
   * via Movement Dashboard's actual_supplier_cost. Falls back to the
   * destination default when the supplier has no slab — the customer-
   * side defaults are intentionally separate so reports can keep them
   * apart.
   */
  async resolveForSupplier(
    supplierId: string | null,
    destinationId: string,
  ): Promise<TaxSlabSnapshotInput | null> {
    if (supplierId) {
      const s = await this.supplierRepo.findOne({
        where: { id: supplierId, is_deleted: false },
        select: { id: true, tax_slab_id: true } as any,
      });
      const snap = await this.loadSnapshot(s?.tax_slab_id ?? null);
      if (snap) return snap;
    }
    return this.resolveForHotelOrSpecial(destinationId);
  }
}
