import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { TaxSlabEntity } from '../../entities/tax-slab.entity';
import { TaxComponentEntity } from '../../entities/tax-component.entity';
import { UpsertTaxSlabDto, TaxComponentDto } from './dto/tax-slab.dto';
import { CLS_TENANT_ID, CLS_USER_ID } from '../../common/cls/cls.constants';

/**
 * TaxSlabService
 * - Component (structure) CRUD on TaxType.
 * - Slab (per-destination rate) upsert + resolve.
 *
 * Component_rates on a slab stays consistent with the parent
 * TaxType's component list: rates for removed components are
 * pruned on read; components added after slab creation simply
 * show up as missing in the rate dict until the user fills them.
 */
@Injectable()
export class TaxSlabService {
  constructor(
    @InjectRepository(TaxSlabEntity) private readonly slabRepo: Repository<TaxSlabEntity>,
    @InjectRepository(TaxComponentEntity) private readonly compRepo: Repository<TaxComponentEntity>,
    private readonly cls: ClsService,
  ) {}

  private tenantId(): string {
    return this.cls.get<string>(CLS_TENANT_ID);
  }
  private userId(): string | null {
    return this.cls.get<string>(CLS_USER_ID) || null;
  }

  // ── Components (structure under TaxType) ───────────────────────

  async listComponentsFor(tax_type_id: string): Promise<TaxComponentEntity[]> {
    const tenantId = this.tenantId();
    return this.compRepo.find({
      where: { tenant_id: tenantId, is_deleted: false, tax_type_id, is_active: true },
      order: { sort_order: 'ASC', created_at: 'ASC' },
    });
  }

  /**
   * Replace the TaxType's component list. Components matched by id
   * are updated in place; new ones created; removed ones soft-deleted.
   * Mirrors the supplier-cost bulk-apply pattern so the frontend
   * can send the whole list in one round-trip.
   */
  async upsertComponents(
    tax_type_id: string,
    components: TaxComponentDto[],
  ): Promise<{ applied: number }> {
    const tenantId = this.tenantId();
    const userId = this.userId();

    const existing = await this.compRepo.find({
      where: { tenant_id: tenantId, is_deleted: false, tax_type_id },
    });

    const keepIds = new Set(components.map((c) => c.id).filter(Boolean) as string[]);
    const removedIds: string[] = [];
    for (const prior of existing) {
      if (!keepIds.has(prior.id)) {
        await this.compRepo.update(prior.id, { is_deleted: true, updated_by: userId });
        removedIds.push(prior.id);
      }
    }

    let applied = 0;
    for (let i = 0; i < components.length; i++) {
      const c = components[i];
      if (c.id) {
        await this.compRepo.update(c.id, {
          name: c.name,
          sort_order: c.sort_order ?? i,
          updated_by: userId,
        });
      } else {
        await this.compRepo.save({
          tenant_id: tenantId,
          tax_type_id,
          name: c.name,
          sort_order: c.sort_order ?? i,
          is_active: true,
          created_by: userId,
          updated_by: userId,
        } as Partial<TaxComponentEntity>);
      }
      applied += 1;
    }

    // Sweep slab.component_rates for the removed component ids so
    // tax math doesn't keep summing orphaned rates after a parent
    // deletes a sub-component (e.g. CGST removed from GST → every
    // Goa/Mumbai slab's CGST entry is dropped + total recomputed).
    if (removedIds.length > 0) {
      await this.pruneStaleComponentRates(tax_type_id, removedIds);
    }

    return { applied };
  }

  /**
   * Strip orphaned component_id keys from every slab under the
   * given tax_type and recompute total_rate. Called after
   * upsertComponents removes one or more components so reports +
   * inline displays don't keep summing rates that no longer have
   * a matching component label.
   */
  private async pruneStaleComponentRates(tax_type_id: string, removedIds: string[]): Promise<void> {
    if (removedIds.length === 0) return;
    const tenantId = this.tenantId();
    const userId = this.userId();
    const slabs = await this.slabRepo.find({
      where: { tenant_id: tenantId, is_deleted: false, tax_type_id },
    });
    for (const slab of slabs) {
      const rates = slab.component_rates || {};
      let mutated = false;
      for (const id of removedIds) {
        if (id in rates) {
          delete rates[id];
          mutated = true;
        }
      }
      if (!mutated) continue;
      const remainingValues = Object.values(rates).filter((v) => typeof v === 'number');
      const newTotal =
        remainingValues.length > 0 ? remainingValues.reduce((s, v) => s + Number(v || 0), 0) : 0;
      await this.slabRepo.update(slab.id, {
        component_rates: Object.keys(rates).length > 0 ? rates : null,
        total_rate: newTotal,
        updated_by: userId,
      });
    }
  }

  // ── Slabs (per-destination rate) ──────────────────────────────

  /** Resolve slab for a (tax_type × destination). Null when unset. */
  async findForDestination(
    tax_type_id: string,
    destination_id: string,
  ): Promise<TaxSlabEntity | null> {
    const tenantId = this.tenantId();
    return this.slabRepo.findOne({
      where: { tenant_id: tenantId, is_deleted: false, tax_type_id, destination_id },
    });
  }

  /** List every slab for a destination — feeds the destination's Tax section. */
  async listForDestination(destination_id: string): Promise<TaxSlabEntity[]> {
    const tenantId = this.tenantId();
    return this.slabRepo.find({
      where: { tenant_id: tenantId, is_deleted: false, destination_id },
      relations: {
        tax_type: true,
      },
      order: { created_at: 'ASC' },
    });
  }

  /** List every slab for a tax_type — feeds the tax type admin page. */
  async listForTaxType(tax_type_id: string): Promise<TaxSlabEntity[]> {
    const tenantId = this.tenantId();
    return this.slabRepo.find({
      where: { tenant_id: tenantId, is_deleted: false, tax_type_id },
      relations: {
        destination: true,
      },
      order: { created_at: 'ASC' },
    });
  }

  /**
   * Upsert the slab + its component rates. `total_rate` is derived
   * from the component_rates sum whenever the dict has any entries;
   * falls back to the caller-supplied total_rate for structure-less
   * taxes (e.g. flat tourism tax where the TaxType has a single
   * self-named component but the caller just passes a total).
   */
  async upsertSlab(
    dto: UpsertTaxSlabDto,
  ): Promise<{ id: string; created: boolean; total_rate: number }> {
    const tenantId = this.tenantId();
    const userId = this.userId();

    const rates = dto.component_rates || {};
    const rateEntries = Object.values(rates).filter((v) => typeof v === 'number');
    const derivedTotal =
      rateEntries.length > 0
        ? rateEntries.reduce((s, v) => s + Number(v || 0), 0)
        : Number(dto.total_rate || 0);

    const existing = await this.slabRepo.findOne({
      where: {
        tenant_id: tenantId,
        is_deleted: false,
        tax_type_id: dto.tax_type_id,
        destination_id: dto.destination_id,
      },
    });

    try {
      if (existing) {
        await this.slabRepo.update(existing.id, {
          component_rates: Object.keys(rates).length > 0 ? rates : null,
          total_rate: derivedTotal,
          notes: dto.notes ?? existing.notes,
          is_active: dto.is_active ?? existing.is_active,
          updated_by: userId,
        });
        return { id: existing.id, created: false, total_rate: derivedTotal };
      }
      const row = await this.slabRepo.save({
        tenant_id: tenantId,
        tax_type_id: dto.tax_type_id,
        destination_id: dto.destination_id,
        component_rates: Object.keys(rates).length > 0 ? rates : null,
        total_rate: derivedTotal,
        notes: dto.notes ?? null,
        is_active: dto.is_active ?? true,
        created_by: userId,
        updated_by: userId,
      } as Partial<TaxSlabEntity>);
      return { id: row.id, created: true, total_rate: derivedTotal };
    } catch (e: unknown) {
      const message = e instanceof Error ? e.message : String(e);
      if (message.includes('duplicate')) {
        throw new ConflictException('A slab for this destination + tax type already exists');
      }
      throw e;
    }
  }

  async removeSlab(id: string) {
    const tenantId = this.tenantId();
    const slab = await this.slabRepo.findOne({
      where: { id, tenant_id: tenantId, is_deleted: false },
    });
    if (!slab) throw new NotFoundException('Tax slab not found');
    await this.slabRepo.update(id, { is_deleted: true, updated_by: this.userId() });
    return { message: 'Tax slab removed' };
  }
}
