import { CLEAR_SENTINEL, type CellPriceValue } from './pricing-excel.service';

/**
 * Repo shape both transport-pricing and activity-pricing satisfy —
 * just enough surface area for the importer to do its job. Generic
 * over the entity type because entity classes are nominal (no string
 * index signature) and we don't want to force every entity to declare
 * one. The importer only reads the columns named in the parsed row.
 */
export interface PricingImportRepo<T = unknown> {
  findById(id: string): Promise<T | null>;
  update(id: string, data: Record<string, unknown>): Promise<void>;
}

/** One row's parsed price payload — keyed by entity column. */
export type ParsedPricingRow = { id: string } & Record<string, CellPriceValue>;

export interface PricingImportResult {
  message: string;
  results: { id: string; action: string }[];
  changedFields: Record<string, string[]>;
  skipped: { id: string; reason: string }[];
}

/**
 * Optional bifurcation callback. Pricing services pass this so the
 * importer derives `*_base_price` + `*_tax_amount` for every cell
 * the user touched, using the row's effective tax slab + currency
 * rounding. Kept generic via the callback so the importer doesn't
 * have to depend on TaxSlabResolverService or know which service
 * (transport vs activity) is calling it.
 *
 *   - `item`   — the loaded entity, used to read destination_id /
 *                transport_service_id / activity_id / currency_id.
 *   - `prices` — the keys the importer is about to write, AFTER the
 *                CLEAR sentinel has been mapped to `null` and blanks
 *                stripped. The callback returns the derived columns
 *                to merge in.
 */
export interface PricingImportOptions<T> {
  bifurcate?: (item: T, prices: Record<string, number | null>) => Promise<Record<string, unknown>>;
}

/**
 * Shared importer used by transport-pricing and activity-pricing.
 * Behavior:
 *
 *   - `null`  cell  → no change (skip that field)
 *   - number cell   → write the number
 *   - CLEAR_SENTINEL → write explicit `null` (lets ops reset a price
 *                     they previously set; previously impossible because
 *                     blank cells couldn't be distinguished from "no edit")
 *   - missing row id (not found in DB) → tracked in `skipped[]` so the
 *     FE can surface "N rows skipped, see report" rather than the old
 *     silent `continue` that pretended everything was fine.
 *
 * `changedFields` is the diff against the persisted row, compared as
 * numbers — Postgres returns `numeric(12,2)` columns as strings via
 * TypeORM, so the previous string-vs-number `!==` always reported every
 * cell as changed.
 */
export async function applyParsedPricingImport<T>(
  rows: ParsedPricingRow[],
  repo: PricingImportRepo<T>,
  opts: PricingImportOptions<T> = {},
): Promise<PricingImportResult> {
  const results: { id: string; action: string }[] = [];
  const skipped: { id: string; reason: string }[] = [];
  const changedFields: Record<string, string[]> = {};

  for (const row of rows) {
    const item = await repo.findById(row.id);
    if (!item) {
      skipped.push({ id: row.id, reason: 'row id not found' });
      continue;
    }
    // Treat the entity as a string-indexable record for the column-by-
    // column comparison below. Safe because rows only touch the named
    // price columns the parser produced.
    const itemRow = item as unknown as Record<string, unknown>;
    const prices: Record<string, number | null> = {};
    const changed: string[] = [];
    for (const [k, v] of Object.entries(row)) {
      if (k === 'id') continue;
      if (v === null || v === undefined) continue; // blank cell → no edit
      const newVal: number | null = v === CLEAR_SENTINEL ? null : (v as number);
      prices[k] = newVal;
      const existing = itemRow[k];
      const existingNum = existing == null ? null : Number(existing);
      if (existingNum !== newVal) changed.push(k);
    }
    if (Object.keys(prices).length === 0) {
      skipped.push({ id: row.id, reason: 'no price cells filled' });
      continue;
    }
    // Pricing services pass a bifurcate callback so the derived
    // *_base_price + *_tax_amount columns stay in sync with the
    // inclusive *_price values the importer is about to write. Result
    // is merged into the same `prices` object so it lands in one
    // UPDATE — no second round-trip and no chance of partial state.
    const payload: Record<string, unknown> = { ...prices };
    if (opts.bifurcate) {
      const derived = await opts.bifurcate(item, prices);
      Object.assign(payload, derived);
    }
    await repo.update(row.id, payload);
    results.push({ id: row.id, action: 'updated' });
    if (changed.length > 0) changedFields[row.id] = changed;
  }

  const summary = [
    `${results.length} pricing row${results.length === 1 ? '' : 's'} updated from Excel`,
    skipped.length > 0 ? `${skipped.length} skipped` : null,
  ]
    .filter(Boolean)
    .join(' · ');

  return { message: summary, results, changedFields, skipped };
}
