import {
  Injectable,
  ConflictException,
  ForbiddenException,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ActivityPricingRepository } from './repositories/activity-pricing.repository';
import { ActivityPricingEntity } from '../../entities/activity-pricing.entity';
import { CurrencyEntity } from '../../entities/currency.entity';
import { CreateActivityPricingDto, UpdateActivityPricingDto } from './dto';
import { serviceMessages } from '../../common/constants/messages';
import {
  buildActivitySample,
  parseActivityImport,
} from '../../common/services/pricing-excel.service';
import { applyParsedPricingImport } from '../../common/services/pricing-import.helper';
import { bifurcatePricingRowBoth } from '../../common/services/pricing-bifurcation.helper';
import { TaxSlabResolverService } from '../../common/services/tax-slab-resolver.service';
import { PermissionContextService } from '../../common/auth/permission-context.service';
import { AccessLevel, ModuleName } from '../../entities/role-permission.entity';

const MSG = serviceMessages('Activity pricing');

/**
 * Per-row pricing fields — selling matrix (default + 5 seasonal) and
 * the parallel floor matrix added in 1777600000000-ActivityFloorPrice.
 * Listed once so create / bulkSave / update can map dto → save payload
 * without drift when columns get added.
 */
const PRICE_FIELDS = [
  'default_price',
  'season_1_price',
  'season_2_price',
  'season_3_price',
  'season_4_price',
  'season_5_price',
  'floor_default_price',
  'floor_season_1_price',
  'floor_season_2_price',
  'floor_season_3_price',
  'floor_season_4_price',
  'floor_season_5_price',
] as const;

type PricingDtoLike = Partial<Record<(typeof PRICE_FIELDS)[number], number | null | undefined>>;

// Repo internals (protected on TenantAwareRepository) — used by remove*
// to issue hard deletes (no soft-delete on pricing rows). Narrow the
// cast through this shape rather than `any`.
interface RepoInternals {
  getTenantId(): string;
  repo: Repository<ActivityPricingEntity>;
}

function pickPriceFields(dto: PricingDtoLike): Record<string, number | null> {
  const out: Record<string, number | null> = {};
  for (const f of PRICE_FIELDS) out[f] = dto[f] ?? null;
  return out;
}

function pickPriceUpdates(dto: PricingDtoLike): Record<string, number | null> {
  const out: Record<string, number | null> = {};
  for (const f of PRICE_FIELDS) {
    const v = dto[f];
    if (v !== undefined) out[f] = v;
  }
  return out;
}

@Injectable()
export class ActivityPricingService {
  constructor(
    private readonly repo: ActivityPricingRepository,
    private readonly perms: PermissionContextService,
    private readonly resolver: TaxSlabResolverService,
    @InjectRepository(CurrencyEntity)
    private readonly currencyRepo: Repository<CurrencyEntity>,
  ) {}

  /**
   * Derive `*_base_price` + `*_tax_amount` for every inclusive cell
   * in `prices` using the activity's effective slab (activity master
   * override → destination default) + the currency's rounding step.
   * Same shape as TransportPricingService.bifurcatePricesForSave —
   * kept service-local so each side can swap the resolver method
   * without touching the other.
   */
  private async bifurcatePricesForSave(
    prices: Record<string, unknown>,
    activityId: string,
    destinationId: string,
    currencyId: string,
  ): Promise<Record<string, number | null>> {
    const [slab, currency] = await Promise.all([
      this.resolver.resolveForActivity(activityId, destinationId),
      this.currencyRepo.findOne({ where: { id: currencyId } }),
    ]);
    const rounding = Number(currency?.rounding) || 1;
    return bifurcatePricingRowBoth(prices, slab, rounding);
  }

  /**
   * Drop floor_* fields from a payload when the caller lacks
   * activity_floor_pricing:edit — so a user without floor perms can
   * never overwrite floor values (silently keeps the existing ones).
   */
  private async stripFloorFromPayload<T extends Record<string, unknown>>(payload: T): Promise<T> {
    const canEditFloor = await this.perms.hasPermission(
      ModuleName.ACTIVITY_FLOOR_PRICING,
      AccessLevel.EDIT,
    );
    if (canEditFloor) return payload;
    const cleaned: Record<string, unknown> = { ...payload };
    for (const k of Object.keys(cleaned)) {
      if (k.startsWith('floor_')) delete cleaned[k];
    }
    return cleaned as T;
  }

  async findByDestination(destinationId: string, currencyId?: string) {
    return this.repo.findByDestinationAndCurrency(destinationId, currencyId);
  }

  async findPaginated(query: {
    destinationId?: string;
    currencyId?: string;
    search?: string;
    page?: number;
    limit?: number;
  }) {
    return this.repo.findPaginatedByActivities(query);
  }

  async findById(id: string) {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);
    return item;
  }

  async create(dto: CreateActivityPricingDto) {
    const existing = await this.repo.findExisting(
      dto.activity_id,
      dto.ticket_id || null,
      dto.age_group,
      dto.currency_id,
    );
    if (existing)
      throw new ConflictException(
        'Pricing for this activity + ticket + age group + currency already exists',
      );

    const prices = await this.stripFloorFromPayload(pickPriceFields(dto));
    const bifurcation = await this.bifurcatePricesForSave(
      prices,
      dto.activity_id,
      dto.destination_id,
      dto.currency_id,
    );
    const item = await this.repo.save({
      destination_id: dto.destination_id,
      activity_id: dto.activity_id,
      ticket_id: dto.ticket_id || null,
      age_group: dto.age_group,
      currency_id: dto.currency_id,
      ...prices,
      ...bifurcation,
    });

    return { id: item.id, message: MSG.CREATED };
  }

  async bulkSave(items: CreateActivityPricingDto[]) {
    const results: { id: string; action: 'created' | 'updated' }[] = [];
    for (const dto of items) {
      const existing = await this.repo.findExisting(
        dto.activity_id,
        dto.ticket_id || null,
        dto.age_group,
        dto.currency_id,
      );
      const pricesUpd = await this.stripFloorFromPayload(pickPriceUpdates(dto));
      const pricesAll = await this.stripFloorFromPayload(pickPriceFields(dto));
      const bifurcationAll = await this.bifurcatePricesForSave(
        pricesAll,
        dto.activity_id,
        dto.destination_id,
        dto.currency_id,
      );
      // Restrict the bifurcation to the cells the update branch is
      // touching — same key-filter as TransportPricingService.bulkSave.
      const bifurcationUpd: Record<string, number | null> = {};
      for (const k of Object.keys(pricesUpd)) {
        const tier = k.replace(/^floor_/, '').replace(/_price$/, '');
        const baseKey = k.startsWith('floor_')
          ? `floor_${tier}_base_price`
          : `${tier}_base_price`;
        const taxKey = k.startsWith('floor_')
          ? `floor_${tier}_tax_amount`
          : `${tier}_tax_amount`;
        if (baseKey in bifurcationAll) bifurcationUpd[baseKey] = bifurcationAll[baseKey];
        if (taxKey in bifurcationAll) bifurcationUpd[taxKey] = bifurcationAll[taxKey];
      }
      if (existing) {
        await this.repo.update(existing.id, { ...pricesUpd, ...bifurcationUpd });
        results.push({ id: existing.id, action: 'updated' });
      } else {
        const item = await this.repo.save({
          destination_id: dto.destination_id,
          activity_id: dto.activity_id,
          ticket_id: dto.ticket_id || null,
          age_group: dto.age_group,
          currency_id: dto.currency_id,
          ...pricesAll,
          ...bifurcationAll,
        });
        results.push({ id: item.id, action: 'created' });
      }
    }
    return { results, message: `${results.length} pricing entries saved` };
  }

  async update(id: string, dto: UpdateActivityPricingDto) {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);

    const updates = await this.stripFloorFromPayload(pickPriceUpdates(dto));

    // Re-bifurcate any price cell present in the update — uses the
    // existing row's activity_id / destination_id / currency_id since
    // those are locked at create-time and not editable here.
    const priceKeys = Object.keys(updates).filter((k) => k.endsWith('_price'));
    if (priceKeys.length > 0) {
      const bifurcation = await this.bifurcatePricesForSave(
        updates,
        item.activity_id,
        item.destination_id,
        item.currency_id,
      );
      for (const k of priceKeys) {
        const tier = k.replace(/^floor_/, '').replace(/_price$/, '');
        const baseKey = k.startsWith('floor_')
          ? `floor_${tier}_base_price`
          : `${tier}_base_price`;
        const taxKey = k.startsWith('floor_')
          ? `floor_${tier}_tax_amount`
          : `${tier}_tax_amount`;
        if (baseKey in bifurcation) updates[baseKey] = bifurcation[baseKey];
        if (taxKey in bifurcation) updates[taxKey] = bifurcation[taxKey];
      }
    }

    await this.repo.update(id, updates);
    return { id, message: MSG.UPDATED };
  }

  async generateSample(destinationId?: string): Promise<Buffer> {
    const rows = await this.repo.findAllForExport(destinationId);
    // Floor columns only included if caller has activity_floor_pricing:view
    const withFloor = await this.perms.hasPermission(
      ModuleName.ACTIVITY_FLOOR_PRICING,
      AccessLevel.VIEW,
    );
    return buildActivitySample(rows, withFloor);
  }

  async importFromExcel(buffer: Buffer) {
    // Parser drops floor_* fields when caller lacks floor:edit but
    // separately reports the count of floor edits the user attempted
    // so we 403 instead of silently stripping (the previous behaviour
    // returned a success toast while quietly throwing the floor edits
    // away).
    const acceptFloor = await this.perms.hasPermission(
      ModuleName.ACTIVITY_FLOOR_PRICING,
      AccessLevel.EDIT,
    );
    const { rows, floorEditCount } = await parseActivityImport(buffer, acceptFloor);
    if (!acceptFloor && floorEditCount > 0) {
      throw new ForbiddenException(
        `You edited ${floorEditCount} Floor price cell${floorEditCount === 1 ? '' : 's'} but lack permission to change Floor prices. Ask an admin to grant activity_floor_pricing:edit, or remove the Floor edits from the file and re-upload.`,
      );
    }

    // Bifurcate every imported row using its activity + destination
    // resolver chain. Same callback pattern as TransportPricingService
    // so the shared importer stays generic.
    return applyParsedPricingImport(rows, this.repo, {
      bifurcate: (item, prices) =>
        this.bifurcatePricesForSave(
          prices,
          item.activity_id,
          item.destination_id,
          item.currency_id,
        ),
    });
  }

  async remove(id: string) {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);

    // Hard delete pricing record
    await (this.repo as unknown as RepoInternals).repo.delete(id);
    return { message: MSG.DELETED };
  }

  async removeByActivity(activityId: string) {
    const internals = this.repo as unknown as RepoInternals;
    const tenantId = internals.getTenantId();
    // Hard delete all pricing for this activity (including soft-deleted)
    const result = await internals.repo.delete({
      activity_id: activityId,
      tenant_id: tenantId,
    });
    return { message: `${result.affected || 0} pricing entries deleted` };
  }
}
