'use strict';

/**
 * unifiedMergeEngine.v2.js
 * ------------------------
 * Rewrite of the merge engine with two goals:
 *   (1) LOSSLESS — surface everything the raw CPO/LFS responses contain that the
 *       previous engine dropped (LFS brand content, APIS requirements, CPO
 *       surcharges, full brand shelf, NDC refundable/penalties/ticketing, etc.)
 *   (2) CLEAN — emit ONE identical normalized per-flight schema for both the
 *       gdsFlights[] and ndcFlights[] buckets, with numeric money + currency,
 *       normalized durations and dateTimes, and consistent leg/fare shapes.
 *
 * The parsing core (FlightDetails / AirSegment / FareInfo / HostToken /
 * AirPricePoint maps, and the CPO ReferenceList maps) is preserved from the
 * original engine. New extractions and the clean output layer are marked NEW.
 */

const FLIGHT_SOURCE = { GDS_CPO: 'GDS_CPO', GDS_LFS: 'GDS_LFS', CPO_ONLY: 'CPO_ONLY', NDC_ACH: 'NDC_ACH' };

// Provider-based routing — NOT carrier-based. Any carrier from a GDS provider goes
// to the GDS bucket; any carrier from an NDC provider goes to the NDC bucket.
// Override per call: buildUnifiedResponse(..., { gdsProviders, ndcProviders }).
const DEFAULT_GDS_PROVIDERS = ['1G'];
const DEFAULT_NDC_PROVIDERS = ['ACH'];

// Carrier display names are DATA, not code. Supply your airline master via
// buildUnifiedResponse(..., { carrierNames: { AI:'Air India', '6E':'IndiGo', UK:'Vistara', … } }).
// Unknown codes resolve to null so the frontend can fill them from its own table.
const carrierName = (code, carrierNames) => (code && carrierNames && carrierNames[code]) || null;

/* ───────────────────────── helpers ───────────────────────── */

const toArray = (v) => (!v ? [] : Array.isArray(v) ? v : [v]);

const toHHMM = (t) => (!t ? '' : t.includes('T') ? t.slice(11, 16) : t.slice(0, 5));
const toDateOnly = (t) => (!t ? null : t.slice(0, 10));

const buildFlightKey = (carrier, number, origin, destination, depTime) =>
  `${carrier}|${number}|${origin}|${destination}|${toHHMM(depTime)}`;

// NEW: money parsing. Accepts number (7545) or string ("INR9499.00", "INR4323").
const parseMoney = (value, fallbackCurrency = null) => {
  if (value === null || value === undefined || value === '') return null;
  if (typeof value === 'number') return { currency: fallbackCurrency, amount: value, raw: value };
  const s = String(value).trim();
  const m = s.match(/^([A-Za-z]{3})?\s*([\d.,]+)$/);
  if (!m) return { currency: fallbackCurrency, amount: null, raw: value };
  const amount = Number(m[2].replace(/,/g, ''));
  return { currency: m[1] || fallbackCurrency, amount: Number.isFinite(amount) ? amount : null, raw: value };
};
const moneyAmount = (v) => { const p = parseMoney(v); return p ? p.amount : null; };
const moneyCurrency = (v) => { const p = parseMoney(v); return p ? p.currency : null; };

// NEW: ISO-8601 duration <-> minutes
const isoToMinutes = (iso) => {
  if (iso == null) return null;
  if (typeof iso === 'number') return iso;
  const m = String(iso).match(/^P(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i);
  if (!m) return null;
  return Number(m[1] || 0) * 1440 + Number(m[2] || 0) * 60 + Number(m[3] || 0);
};
const minsToPT = (mins) => {
  if (mins == null || isNaN(Number(mins))) return null;
  const t = Number(mins);
  return `PT${Math.floor(t / 60)}H${t % 60}M`;
};

// NEW: split date + (time | isoDateTime) -> { date, time, dateTime, offset }
const splitDateTime = (date, t) => {
  if (t == null) return { date: date || null, time: null, dateTime: null, offset: null };
  const s = String(t);
  if (s.includes('T')) {
    const [d, rest = ''] = s.split('T');
    const off = (rest.match(/([+-]\d{2}:\d{2}|Z)$/) || [])[1] || null;
    const time = rest.replace(/([+-]\d{2}:\d{2}|Z)$/, '').replace(/\.\d+$/, '');
    return { date: d || date || null, time: time || null, dateTime: s, offset: off };
  }
  return { date: date || null, time: s, dateTime: date ? `${date}T${s}` : s, offset: null };
};

const diffMinutes = (depIso, arrIso) => {
  try {
    const d = new Date(depIso).getTime();
    const a = new Date(arrIso).getTime();
    if (isNaN(d) || isNaN(a)) return null;
    return Math.round((a - d) / 60000);
  } catch { return null; }
};

/* ═══════════════════════ 1. PARSE LFS ═══════════════════════ */

const parseLfsResponse = (rawLfs, options = {}) => {
  const rsp = rawLfs?.data?.['SOAP:Envelope']?.['SOAP:Body']?.['air:LowFareSearchRsp'];
  if (!rsp) { console.error('[merge] LFS root not found'); return empties(); }

  const flightDetailsMap = buildFlightDetailsMap(rsp['air:FlightDetailsList']?.['air:FlightDetails']);
  const fareInfoMap = buildFareInfoMap(rsp['air:FareInfoList']?.['air:FareInfo']);
  const hostTokenMap = buildHostTokenMap(rsp['air:HostTokenList']?.['common_v54_0:HostToken']);
  const lfsBrandMap = buildLfsBrandMap(rsp['air:BrandList']?.['air:Brand']);              // NEW
  const apisMap = buildApisMap(rsp['air:APISRequirementsList']?.['air:APISRequirements']); // NEW
  const segmentMap = buildSegmentMap(rsp['air:AirSegmentList']?.['air:AirSegment'], flightDetailsMap);

  const gdsProviders = new Set(options.gdsProviders || DEFAULT_GDS_PROVIDERS);
  const ndcProviders = new Set(options.ndcProviders || DEFAULT_NDC_PROVIDERS);
  const { gdsLfsFlights, ndcLfsFlights } = groupByFlight(
    rsp['air:AirPricePointList']?.['air:AirPricePoint'], segmentMap, fareInfoMap, hostTokenMap, gdsProviders, ndcProviders,
  );
  return { gdsLfsFlights, ndcLfsFlights, lfsBrandMap, apisMap };
};
const empties = () => ({ gdsLfsFlights: {}, ndcLfsFlights: {}, lfsBrandMap: {}, apisMap: {} });

const buildFlightDetailsMap = (raw) => {
  const map = {};
  for (const d of toArray(raw)) {
    const a = d.$ || {};
    map[a.Key] = {
      key: a.Key, origin: a.Origin, destination: a.Destination,
      departureTime: a.DepartureTime, arrivalTime: a.ArrivalTime,
      flightTime: a.FlightTime ? Number(a.FlightTime) : null,
      travelTime: a.TravelTime ? Number(a.TravelTime) : null,
      equipment: a.Equipment || null,
      originTerminal: a.OriginTerminal || null,
      destinationTerminal: a.DestinationTerminal || null,
    };
  }
  return map;
};

const buildFareInfoMap = (raw) => {
  const map = {};
  for (const fi of toArray(raw)) {
    const a = fi.$ || {}, su = fi['air:FareSurcharge'], bg = fi['air:BaggageAllowance'],
      rk = fi['air:FareRuleKey'], br = fi['air:Brand'];
    map[a.Key] = {
      key: a.Key, fareBasis: a.FareBasis || null, passengerTypeCode: a.PassengerTypeCode || null,
      origin: a.Origin, destination: a.Destination, effectiveDate: a.EffectiveDate || null,
      departureDate: a.DepartureDate || null, amount: a.Amount || null,
      negotiatedFare: a.NegotiatedFare === 'true', notValidBefore: a.NotValidBefore || null,
      notValidAfter: a.NotValidAfter || null, promotionalFare: a.PromotionalFare === 'true',
      fareFamily: a.FareFamily || null, supplierCode: a.SupplierCode || null,
      fareSurcharge: su ? { key: su?.$?.Key || null, type: su?.$?.Type || null, amount: su?.$?.Amount || null } : null,
      baggageAllowance: bg ? { maxWeight: bg['air:MaxWeight'] ? { value: Number(bg['air:MaxWeight']?.$?.Value || 0), unit: bg['air:MaxWeight']?.$?.Unit || null } : null } : null,
      fareRuleKey: rk ? { value: rk._ || null, providerCode: rk?.$?.ProviderCode || null } : null,
      brandRef: br ? { key: br?.$?.Key || null, brandID: br?.$?.BrandID || null, brandTier: br?.$?.BrandTier || null } : null,
    };
  }
  return map;
};

const buildHostTokenMap = (raw) => {
  const map = {};
  for (const t of toArray(raw)) if (t.$ && t._) map[t.$.Key] = t._;
  return map;
};

// NEW: LFS brand content keyed by BrandID (matches NDC fareInfo.brandRef.brandID
// and CPO brand.brandedId). Captures name, titles, texts (incl. descriptions), images.
const buildLfsBrandMap = (raw) => {
  const byId = {}, byKey = {};
  const pickText = (list, type) => toArray(list).find((t) => t?.type === type)?.value || null;
  for (const b of toArray(raw)) {
    const a = b.$ || {};
    const titles = toArray(b['air:Title']).map((t) => ({ type: t?.$?.Type || null, language: t?.$?.LanguageCode || null, value: t._ ?? null }));
    const texts = toArray(b['air:Text']).map((t) => ({ type: t?.$?.Type || null, language: t?.$?.LanguageCode || null, value: t._ ?? null }));
    const images = toArray(b['air:ImageLocation']).map((im) => ({ type: im?.$?.Type || null, width: im?.$?.ImageWidth || null, url: im._ ?? null }));
    const brand = {
      key: a.Key || null, id: a.BrandID || null, name: a.Name || null,
      carrier: a.Carrier || null, brandedDetailsAvailable: a.BrandedDetailsAvailable === 'true',
      titleExternal: titles.find((t) => t.type === 'External')?.value || a.Name || null,
      titleShort: titles.find((t) => t.type === 'Short')?.value || null,
      strapline: pickText(texts, 'Strapline'),
      upsellText: pickText(texts, 'Upsell'),
      description: pickText(texts, 'Description') || pickText(texts, 'Detail') || null,
      titles, texts, images,
    };
    if (a.BrandID) byId[a.BrandID] = brand;
    if (a.Key) byKey[a.Key] = brand;
  }
  return { byId, byKey };
};

// NEW: APIS (passport / document) requirements keyed by Key.
const buildApisMap = (raw) => {
  const map = {};
  for (const r of toArray(raw)) {
    const a = r.$ || {};
    map[a.Key] = {
      key: a.Key || null, level: a.Level || null,
      documents: toArray(r['air:Document']).map((d) => ({
        sequence: d?.$?.Sequence || null, type: d?.$?.Type || null, level: d?.$?.Level || null,
      })),
    };
  }
  return map;
};

const buildSegmentMap = (raw, flightDetailsMap) => {
  const map = {};
  for (const seg of toArray(raw)) {
    const a = seg.$ || {};
    const detKey = seg['air:FlightDetailsRef']?.$.Key;
    const detail = detKey ? flightDetailsMap[detKey] : null;
    const cs = seg['air:CodeshareInfo'];
    const depTime = detail?.departureTime || a.DepartureTime || '';
    map[a.Key] = {
      key: a.Key, carrier: a.Carrier, flightNumber: a.FlightNumber, origin: a.Origin, destination: a.Destination,
      group: a.Group || '0', providerCode: seg['air:AirAvailInfo']?.$.ProviderCode || null, supplierCode: a.SupplierCode || null,
      departureTime: depTime, arrivalTime: detail?.arrivalTime || a.ArrivalTime || '',
      flightTime: a.FlightTime ? Number(a.FlightTime) : (detail?.flightTime ?? null),
      travelTime: detail?.travelTime || null, distance: a.Distance ? Number(a.Distance) : null,
      equipment: detail?.equipment || a.Equipment || null,
      originTerminal: detail?.originTerminal || null, destinationTerminal: detail?.destinationTerminal || null,
      flightDetailsRef: detKey || null,
      operatingCarrier: cs?.$?.OperatingCarrier || null, operatingFlightNumber: cs?.$?.OperatingFlightNumber || null,
      operatingCarrierName: typeof cs === 'object' ? (cs._ || null) : null,
      eTicketability: a.ETicketability || null, availabilitySource: a.AvailabilitySource || null,
      changeOfPlane: a.ChangeOfPlane === 'true', participantLevel: a.ParticipantLevel || null, status: a.Status || null,
      apisRequirementsRef: a.APISRequirementsRef || null,
      polledAvailabilityOption: a.PolledAvailabilityOption || null,   // NEW (was dropped)
      availabilityDisplayType: a.AvailabilityDisplayType || null,     // NEW (was dropped)
      linkAvailability: a.LinkAvailability === 'true', optionalServicesIndicator: a.OptionalServicesIndicator === 'true',
      flightKey: buildFlightKey(a.Carrier, a.FlightNumber, a.Origin, a.Destination, depTime),
    };
  }
  return map;
};

// Sum verbatim money values (e.g. ["INR5", "INR3"]) -> "INR8.00"; null if none.
const sumMoney = (rows, field) => {
  let cur = null, n = 0, any = false;
  for (const r of rows) { const m = moneyAmount(r[field]); if (m != null) { any = true; n += m; cur = cur || moneyCurrency(r[field]); } }
  return any ? (cur ? `${cur}${n.toFixed(2)}` : n) : null;
};

const groupByFlight = (rawPps, segmentMap, fareInfoMap, hostTokenMap, gdsProviders, ndcProviders) => {
  const gdsLfsFlights = {}, ndcLfsFlights = {};
  for (const pp of toArray(rawPps)) {
    const ppA = pp.$ || {};
    const pis = toArray(pp['air:AirPricingInfo']).filter(Boolean);
    if (!pis.length) continue;

    // Each AirPricingInfo is ONE passenger type (ADT/CHD/INF). Build the per-passenger
    // breakdown, and use the price-point totals as the party-level headline price.
    const passengerBreakdown = pis.map((pi) => {
      const a = pi.$ || {};
      return {
        passengerType: pi['air:PassengerType']?.$?.Code ?? null, passengerAge: pi['air:PassengerType']?.$?.Age ?? null,
        baseFare: a.BasePrice ?? null, taxes: a.Taxes ?? null, fees: a.Fees ?? null, totalFare: a.TotalPrice ?? null,
        pricingInfoKey: a.Key ?? null,
      };
    });
    const feesAgg = ppA.Fees ?? sumMoney(passengerBreakdown, 'fees'); // price point rarely exposes Fees

    // Representative pricing info (adult if present) drives flight/family/penalties/refs.
    const primary = pis.find((pi) => pi['air:PassengerType']?.$?.Code === 'ADT') || pis[0];
    const piA = primary.$ || {}, providerCode = piA.ProviderCode;
    const taxBreakdown = toArray(primary['air:TaxInfo']).map((t) => ({ category: t?.$?.Category || null, amount: t?.$?.Amount || null, key: t?.$?.Key || null, supplierCode: t?.$?.SupplierCode || null }));
    const fareInfoRefs = toArray(primary['air:FareInfoRef']).map((f) => ({ key: f?.$?.Key || null, fareInfo: fareInfoMap[f?.$?.Key] || null }));
    const chg = primary['air:ChangePenalty'], cnl = primary['air:CancelPenalty'];

    for (const fo of toArray(primary['air:FlightOptionsList']?.['air:FlightOption'])) {
      const foA = fo.$ || {}, legRef = foA.LegRef || null;
      for (const opt of toArray(fo['air:Option'])) {
        const optA = opt.$ || {};
        const bookingInfos = toArray(opt['air:BookingInfo']).map((bi) => {
          const biA = bi.$ || {};
          return {
            bookingCode: biA.BookingCode, bookingCount: biA.BookingCount ? Number(biA.BookingCount) : null,
            cabinClass: biA.CabinClass, segmentRef: biA.SegmentRef || null, fareInfoRef: biA.FareInfoRef || null,
            hostTokenRef: biA.HostTokenRef || null, taxInfoRef: biA.TaxInfoRef || null,
            hostToken: biA.HostTokenRef ? (hostTokenMap[biA.HostTokenRef] || null) : null,
            segment: segmentMap[biA.SegmentRef] || null, fareInfo: fareInfoMap[biA.FareInfoRef] || null,
          };
        });
        if (!bookingInfos.length) continue;
        const firstSeg = bookingInfos[0]?.segment; if (!firstSeg) continue;
        const compoundKey = bookingInfos.map((bi) => bi.segment?.flightKey).filter(Boolean).join('::');
        if (!compoundKey) continue;
        const isConnecting = bookingInfos.length > 1, conn = opt['air:Connection'];

        const priceOption = {
          pricePointKey: ppA.Key, pricingInfoKey: piA.Key,
          // PARTY-LEVEL totals from the price point (verbatim source values).
          totalPrice: ppA.TotalPrice ?? piA.TotalPrice, basePrice: ppA.BasePrice ?? piA.BasePrice,
          taxes: ppA.Taxes ?? piA.Taxes, fees: feesAgg, approximateTotalPrice: ppA.ApproximateTotalPrice ?? piA.ApproximateTotalPrice,
          passengerBreakdown,
          taxBreakdown, latestTicketingTime: piA.LatestTicketingTime || null, pricingMethod: piA.PricingMethod || null,
          refundable: piA.Refundable === 'true', eTicketability: piA.ETicketability || null, platingCarrier: piA.PlatingCarrier || null,
          providerCode, cat35Indicator: piA.Cat35Indicator === 'true', completeItinerary: ppA.CompleteItinerary === 'true',
          fareCalc: primary['air:FareCalc'] || null, passengerType: pis.length === 1 ? (primary['air:PassengerType']?.$?.Code || null) : null,
          fareInfoRefs,
          changePenalty: chg ? { penaltyApplies: chg?.$?.PenaltyApplies || null, amount: chg['air:Amount'] || null } : null,
          cancelPenalty: cnl ? { penaltyApplies: cnl?.$?.PenaltyApplies || null, amount: cnl['air:Amount'] || null } : null,
          legRef, travelTime: optA.TravelTime || null, isConnecting,
          connection: conn ? { segmentIndex: conn?.$?.SegmentIndex || null } : null, bookingInfos,
        };

        // Route by PROVIDER, not carrier — any carrier from a GDS/NDC provider flows through.
        if (gdsProviders.has(providerCode)) {
          (gdsLfsFlights[compoundKey] ||= { segments: bookingInfos.map((b) => b.segment).filter(Boolean), priceOptions: [] }).priceOptions.push(priceOption);
        } else if (ndcProviders.has(providerCode)) {
          const bucket = (ndcLfsFlights[compoundKey] ||= { segments: bookingInfos.map((b) => b.segment).filter(Boolean), fareOptions: [] });
          const fareFamily = fareInfoRefs[0]?.fareInfo?.fareFamily || 'Unknown';
          const dup = bucket.fareOptions.some((f) => f.fareFamily === fareFamily && f.totalPrice === priceOption.totalPrice);
          if (!dup) bucket.fareOptions.push({ ...priceOption, fareFamily, supplierCode: fareInfoRefs[0]?.fareInfo?.supplierCode || firstSeg.carrier });
        }
      }
    }
  }
  return { gdsLfsFlights, ndcLfsFlights };
};

/* ═══════════════════════ 2. PARSE CPO ═══════════════════════ */

const parseCpoResponse = (rawCpo) => {
  const root = rawCpo?.rawTravelportResponse?.CatalogProductOfferingsResponse;
  if (!root) { console.error('[merge] CPO root not found'); return { flightMap: {}, sidMap: {}, productMap: {}, termsMap: {}, brandMap: {}, offeringList: [] }; }
  const flightMap = {}, sidMap = {}, productMap = {}, termsMap = {}, brandMap = {};

  for (const ref of (root.ReferenceList || [])) {
    const type = ref['@type'];
    if (type === 'ReferenceListFlight') {
      for (const f of (ref.Flight || [])) {
        const detail = {
          id: f.id, type: f['@type'] || null, carrier: f.carrier, flightNumber: f.number,
          operatingCarrier: f.operatingCarrier || null, operatingCarrierName: f.operatingCarrierName || null,
          operatingFlightNumber: f.operatingCarrierNumber || null, equipment: f.equipment || null,
          distance: f.distance || null, duration: f.duration || null, availabilitySourceCode: f.AvailabilitySourceCode || null,
          departure: { location: f.Departure?.location, terminal: f.Departure?.terminal || null, date: f.Departure?.date, time: f.Departure?.time },
          arrival: { location: f.Arrival?.location, terminal: f.Arrival?.terminal || null, date: f.Arrival?.date, time: f.Arrival?.time },
        };
        const key = buildFlightKey(f.carrier, f.number, f.Departure?.location, f.Arrival?.location, f.Departure?.time);
        if (!flightMap[key]) flightMap[key] = detail;
        sidMap[f.id] = detail;
      }
    }
    if (type === 'ReferenceListProduct') {
      for (const p of (ref.Product || [])) {
        productMap[p.id] = {
          id: p.id, totalDuration: p.totalDuration || null, seatsAvailable: p.Quantity ?? null,
          segments: (p.FlightSegment || []).map((s) => ({ sequence: s.sequence, flightRef: s.Flight?.FlightRef, connectionDuration: s.connectionDuration || null, boundFlightsInd: s.boundFlightsInd || false })),
          passengerFlights: (p.PassengerFlight || []).map((pf) => ({
            passengerTypeCode: pf.passengerTypeCode, passengerQuantity: pf.passengerQuantity,
            flightProducts: (pf.FlightProduct || []).map((fp) => ({ segmentSequence: fp.segmentSequence, classOfService: fp.classOfService, cabin: fp.cabin, fareBasisCode: fp.fareBasisCode || null, fareType: fp.fareType || null, fareTypeCode: fp.fareTypeCode || null, brandRef: fp.Brand?.BrandRef || null })),
          })),
        };
      }
    }
    if (type === 'ReferenceListTermsAndConditions') {
      for (const t of (ref.TermsAndConditions || [])) {
        termsMap[t.id] = {
          id: t.id, paymentTimeLimit: t.PaymentTimeLimit || null,
          validatingAirlines: (t.ValidatingAirline || []).map((v) => v.ValidatingAirline),
          baggage: (t.BaggageAllowance || []).map((b) => ({
            baggageType: b.baggageType, validatingAirlineCode: b.validatingAirlineCode, passengerTypeCodes: b.passengerTypeCodes || [],
            productRefs: b.ProductRef || [], url: b.url || null, segmentSequenceList: b.SegmentSequenceList || [],
            items: (b.BaggageItem || []).map((item) => ({ type: item['@type'] || null, includedInOfferPrice: item.includedInOfferPrice, soldByWeightInd: item.soldByWeightInd || false, measurements: (item.Measurement || []).map((m) => ({ type: m.measurementType, unit: m.unit, value: m.value })), description: item.Text || null })),
          })),
          penalties: (t.Penalties || []).map((pen) => ({
            type: pen['@type'] || null, passengerTypeCodes: pen.PassengerTypeCodes || [],
            change: (pen.Change || []).map((c) => ({ type: c['@type'] || null, penaltyAppliesTo: c.PenaltyAppliesTo || null, penaltyTypes: c.penaltyTypes || [], amounts: (c.Penalty || []).map((p) => ({ type: p['@type'] || null, currency: p.Amount?.code || null, minorUnit: p.Amount?.minorUnit ?? null, value: p.Amount?.value || null })) })),
            cancel: (pen.Cancel || []).map((c) => ({ type: c['@type'] || null, penaltyAppliesTo: c.PenaltyAppliesTo || null, penaltyTypes: c.penaltyTypes || [], amounts: (c.Penalty || []).map((p) => ({ type: p['@type'] || null, currency: p.Amount?.code || null, minorUnit: p.Amount?.minorUnit ?? null, value: p.Amount?.value || null })) })),
          })),
        };
      }
    }
    if (type === 'ReferenceListBrand') {
      for (const b of (ref.Brand || [])) {
        brandMap[b.id] = {
          id: b.id, type: b['@type'] || null, name: b.name, tier: b.tier || null, code: b.code || null,
          brandedId: b?.Identifier?.value || null, shelfNumbers: b.shelfNumbers || [], imageURLs: toArray(b.ImageURL), // NEW imageURLs
          attributes: (b.BrandAttribute || []).map((attr) => ({ type: attr['@type'] || null, classification: attr.classification, inclusion: attr.inclusion, groupCode: attr.groupCode || null, subGroupCode: attr.subGroupCode || null, subCode: attr.subCode || null })),
          additionalAttributes: (b.AdditionalBrandAttribute || []).map((attr) => ({ type: attr['@type'] || null, groupCode: attr.groupCode || null, subCode: attr.subCode || null, classification: attr.Classification || null, inclusion: attr.Inclusion || null })),
        };
      }
    }
  }
  const offeringList = root.CatalogProductOfferings?.CatalogProductOffering || [];
  return { flightMap, sidMap, productMap, termsMap, brandMap, offeringList };
};

/* ═══════════════════════ 3. MERGE + CLEAN OUTPUT ═══════════════════════ */

const merge = (lfsData, cpoData, config = {}) => {
  const { gdsLfsFlights, ndcLfsFlights, lfsBrandMap, apisMap } = lfsData;
  const { flightMap, sidMap, productMap, termsMap, brandMap, offeringList } = cpoData;
  const cpoIndex = buildCpoIndex(offeringList, sidMap); // now includes full shelf + surcharges
  const ctx = { brandMap, termsMap, productMap, lfsBrandMap, apisMap, carrierNames: config.carrierNames || {} };

  const gdsFlights = [];
  const processed = new Set();

  for (const [compoundKey, offeringInfo] of Object.entries(cpoIndex)) {
    const flightKeys = compoundKey.split('::');
    const cpoLegs = flightKeys.map((fk) => flightMap[fk] || null).filter(Boolean);
    if (cpoLegs.length !== flightKeys.length) continue;
    const lfsAiData = gdsLfsFlights[compoundKey];
    const source = lfsAiData ? FLIGHT_SOURCE.GDS_CPO : FLIGHT_SOURCE.CPO_ONLY;
    gdsFlights.push(buildCleanGdsFromCpo({ source, compoundKey, flightKeys, cpoLegs, offeringInfo, lfsAiData, ctx }));
    processed.add(compoundKey);
  }
  for (const [compoundKey, data] of Object.entries(gdsLfsFlights)) {
    if (processed.has(compoundKey) || !data.segments.length) continue;
    gdsFlights.push(buildCleanGdsFromLfs(compoundKey, data, ctx));
    processed.add(compoundKey);
  }
  const ndcFlights = buildCleanNdc(ndcLfsFlights, ctx);
  return { gdsFlights, ndcFlights };
};

const buildCpoIndex = (offeringList, sidMap) => {
  const index = {};
  for (const offering of offeringList) {
    for (const pbo of (offering.ProductBrandOptions || [])) {
      const flightRefs = pbo.flightRefs || [];
      const flightKeys = flightRefs.map((sid) => { const f = sidMap[sid]; return f ? buildFlightKey(f.carrier, f.flightNumber, f.departure.location, f.arrival.location, f.departure.time) : null; }).filter(Boolean);
      if (flightKeys.length !== flightRefs.length) continue;
      const compoundKey = flightKeys.join('::');
      const entry = (index[compoundKey] ||= { offeringIds: new Set(), shelf: new Set(), brandGroups: [] });
      if (offering.id) entry.offeringIds.add(offering.id);
      for (const br of (offering.Brand || [])) if (br?.BrandRef) entry.shelf.add(br.BrandRef); // NEW full brand shelf
      for (const bo of (pbo.ProductBrandOffering || [])) {
        const price = bo.BestCombinablePrice || {};
        entry.brandGroups.push({
          brandRef: bo.Brand?.BrandRef || null, productRefs: (bo.Product || []).map((p) => p.productRef),
          termsRef: bo.TermsAndConditions?.termsAndConditionsRef || null, combinabilityCodes: bo.CombinabilityCode || [],
          contentSource: bo.ContentSource || null, flightRefs,
          price: {
            currency: (typeof price.CurrencyCode === 'object' ? price.CurrencyCode?.value : price.CurrencyCode) || null,
            base: price.Base || 0, totalTaxes: price.TotalTaxes || 0, totalFees: price.TotalFees || 0, totalPrice: price.TotalPrice || 0,
            priceBreakdown: (price.PriceBreakdown || []).map((pb) => ({
              type: pb['@type'] || null, passengerType: pb.requestedPassengerType || pb.passengerType || null, quantity: pb.quantity || 0,
              base: pb?.Amount?.Base || 0, totalTaxes: pb?.Amount?.Taxes?.TotalTaxes || 0,
              taxBreakdown: (pb?.Amount?.Taxes?.Tax || []).map((t) => ({ taxCode: t.taxCode, value: t.value })),
              totalFees: pb?.Amount?.Fees?.TotalFees || 0,
              totalSurcharges: pb?.Surcharges?.TotalSurcharges ?? 0,            // NEW
              surcharges: (pb?.Surcharges?.Surcharge || []).map((s) => ({ currency: s.currencyCode || null, purpose: s.purpose || null, value: s.value ?? null })), // NEW
              total: pb?.Amount?.Total || 0,
            })),
          },
        });
      }
    }
  }
  for (const e of Object.values(index)) { e.offeringIds = [...e.offeringIds]; e.shelf = [...e.shelf]; }
  return index;
};

/* ── clean builders ────────────────────────────────────────── */

const resolveShelf = (shelfRefs, brandMap, pricedRefs, lfsBrandMap) =>
  toArray(shelfRefs).map((ref) => {
    const b = brandMap[ref] || null;
    const merged = b ? mergeBrand(b, lfsBrandMap) : { code: null, name: null, id: null };
    return { brandRef: ref, ...merged, priced: pricedRefs.has(ref) };
  });

const resolveApis = (segs, apisMap) => {
  const out = [];
  for (const s of segs) { const ref = s?.apisRequirementsRef; if (ref && apisMap[ref]) out.push({ segmentKey: s.key, ...apisMap[ref] }); }
  return out.length ? out : null;
};

// Merge CPO brand (attributes) with LFS brand content (name/title/text/images).
const mergeBrand = (cpoBrand, lfsBrandMap) => {
  if (!cpoBrand) return null;
  const lfs = (cpoBrand.brandedId && lfsBrandMap?.byId?.[cpoBrand.brandedId]) || null;
  return {
    code: cpoBrand.code ?? null, name: cpoBrand.name ?? lfs?.name ?? null, id: cpoBrand.brandedId ?? null,
    tier: cpoBrand.tier ?? null, type: cpoBrand.type ?? null, shelfNumbers: cpoBrand.shelfNumbers ?? [],
    title: lfs?.titleExternal ?? cpoBrand.name ?? null, shortTitle: lfs?.titleShort ?? null,
    strapline: lfs?.strapline ?? null, description: lfs?.upsellText ?? lfs?.description ?? null,
    images: lfs?.images ?? (cpoBrand.imageURLs || []).map((u) => ({ type: null, width: null, url: u })),
    attributes: cpoBrand.attributes ?? [], additionalAttributes: cpoBrand.additionalAttributes ?? [],
  };
};

// NDC brand: only an id is known from the fare; enrich from the LFS brand map.
const ndcBrand = (brandRef, lfsBrandMap) => {
  if (!brandRef) return null;
  const lfs = brandRef.brandID && lfsBrandMap?.byId?.[brandRef.brandID];
  if (!lfs) return { code: null, name: null, id: brandRef.brandID || null, tier: brandRef.brandTier || null, type: null, shelfNumbers: [], title: null, shortTitle: null, strapline: null, description: null, images: [], attributes: [], additionalAttributes: [] };
  return { code: null, name: lfs.name, id: lfs.id, tier: brandRef.brandTier || null, type: null, shelfNumbers: [], title: lfs.titleExternal, shortTitle: lfs.titleShort, strapline: lfs.strapline, description: lfs.upsellText || lfs.description, images: lfs.images, attributes: [], additionalAttributes: [] };
};

const normLeg = (leg, carrierNames) => {
  if (!leg) return null;
  const isCpo = 'duration' in leg; // CPO legs use ISO duration; LFS segments use flightTime
  const depRaw = isCpo ? leg.departure?.time : leg.departureTime;
  const arrRaw = isCpo ? leg.arrival?.time : leg.arrivalTime;
  const depDate = isCpo ? leg.departure?.date : toDateOnly(leg.departureTime);
  const arrDate = isCpo ? leg.arrival?.date : toDateOnly(leg.arrivalTime);
  const dep = splitDateTime(depDate, depRaw), arr = splitDateTime(arrDate, arrRaw);
  const durationMinutes = leg.flightTime != null ? Number(leg.flightTime) : isoToMinutes(leg.duration);
  return {
    sequence: leg.sequence ?? null, carrier: leg.carrier ?? null, carrierName: carrierName(leg.carrier, carrierNames), flightNumber: leg.flightNumber ?? null,
    operatingCarrier: leg.operatingCarrier ?? null, operatingCarrierName: leg.operatingCarrierName ?? null, operatingFlightNumber: leg.operatingFlightNumber ?? null,
    equipment: leg.equipment ?? null, distance: leg.distance ?? null,
    durationMinutes: durationMinutes ?? null, durationIso: leg.duration ?? minsToPT(durationMinutes),
    departure: { location: isCpo ? leg.departure?.location : leg.origin, terminal: isCpo ? leg.departure?.terminal : leg.originTerminal, date: dep.date, time: dep.time, dateTime: dep.dateTime, offset: dep.offset },
    arrival: { location: isCpo ? leg.arrival?.location : leg.destination, terminal: isCpo ? leg.arrival?.terminal : leg.destinationTerminal, date: arr.date, time: arr.time, dateTime: arr.dateTime, offset: arr.offset },
    providerCode: leg.providerCode ?? null, supplierCode: leg.supplierCode ?? null, group: leg.group ?? null, status: leg.status ?? null, changeOfPlane: leg.changeOfPlane ?? null,
    refs: { flightRef: leg.id ?? leg.flightRef ?? null, segmentRef: leg.key ?? leg.segmentRef ?? null, flightDetailsRef: leg.flightDetailsRef ?? null, apisRequirementsRef: leg.apisRequirementsRef ?? null },
    attributes: {
      type: leg.type ?? null, travelTime: leg.travelTime ?? null, availabilitySource: leg.availabilitySource ?? null, availabilitySourceCode: leg.availabilitySourceCode ?? null,
      eTicketability: leg.eTicketability ?? null, participantLevel: leg.participantLevel ?? null, linkAvailability: leg.linkAvailability ?? null,
      optionalServicesIndicator: leg.optionalServicesIndicator ?? null, polledAvailabilityOption: leg.polledAvailabilityOption ?? null, availabilityDisplayType: leg.availabilityDisplayType ?? null,
    },
  };
};

const normTaxBreakdown = (taxes, currency) => toArray(taxes).map((t) => {
  const code = t.taxCode ?? t.category ?? null;
  const amount = t.value != null ? t.value : (t.amount ?? null); // exact source value (number for CPO, "INR…" for LFS)
  return { code, amount, currency, key: t.key ?? null, supplierCode: t.supplierCode ?? null };
});

const normBaggage = (baggage) => toArray(baggage).map((b) => ({ baggageType: b.baggageType ?? null, validatingAirlineCode: b.validatingAirlineCode ?? null, passengerTypeCodes: toArray(b.passengerTypeCodes), segmentSequenceList: toArray(b.segmentSequenceList), productRefs: toArray(b.productRefs), url: b.url ?? null, items: toArray(b.items) }));

// Shared fare core — identical on EVERY fare regardless of source. Source-specific
// fields go in `fare.gds` (CPO branded shape) or `fare.ndc` (LFS-priced shape),
// added by the builder. Exactly one of those blocks is present per fare.
const commonFare = ({ index, contentSource, fareFamily, brand, cabin, fareBasis, passengerType,
  currency, base, taxes, fees = 0, surcharges = 0, total, approximateTotal = null, taxBreakdown = [], passengerBreakdown = [] }) => ({
  index, contentSource, fareFamily: fareFamily ?? null, brandName: brand?.name ?? null, brand: brand ?? null,
  cabin: cabin ?? null, fareBasis: fareBasis ?? null, passengerType: passengerType ?? null,
  price: { currency: currency ?? null, baseFare: base ?? null, taxes: taxes ?? null, fees, surcharges, totalFare: total ?? null, approximateTotalFare: approximateTotal, passengerBreakdown, taxBreakdown },
});

const normBookingInfo = (bi) => ({
  bookingCode: bi.bookingCode ?? null, bookingCount: bi.bookingCount ?? null, cabinClass: bi.cabinClass ?? null,
  fareBasis: bi.fareBasis ?? bi.fareInfo?.fareBasis ?? null, fareFamily: bi.fareFamily ?? bi.fareInfo?.fareFamily ?? null,
  baggageAllowance: (bi.baggageAllowance ?? bi.fareInfo?.baggageAllowance)?.maxWeight ? { weight: (bi.baggageAllowance ?? bi.fareInfo?.baggageAllowance).maxWeight.value, unit: (bi.baggageAllowance ?? bi.fareInfo?.baggageAllowance).maxWeight.unit } : null,
  fareSurcharge: bi.fareSurcharge ?? bi.fareInfo?.fareSurcharge ?? null, hostToken: bi.hostToken ?? null, hostTokenRef: bi.hostTokenRef ?? null,
  refs: { segmentRef: bi.segmentRef ?? null, fareInfoRef: bi.fareInfoRef ?? null, taxInfoRef: bi.taxInfoRef ?? null, fareRuleKey: bi.fareRuleKey ?? bi.fareInfo?.fareRuleKey ?? null },
});

// The LFS-priced `ndc` block — shared by GDS_LFS (AI) and NDC_ACH (6E) fares,
// since both come from air:AirPricingInfo and carry the same kind of fields.
const lfsPricedBlock = (src, currency) => ({
  refundable: src.refundable ?? null, latestTicketingTime: src.latestTicketingTime ?? null,
  pricingMethod: src.pricingMethod ?? null, cat35Indicator: src.cat35Indicator ?? null, eTicketability: src.eTicketability ?? null,
  penalties: {
    change: src.changePenalty ? { amount: src.changePenalty.amount ?? null, currency, when: src.changePenalty.penaltyApplies } : null,
    cancel: src.cancelPenalty ? { amount: src.cancelPenalty.amount ?? null, currency, when: src.cancelPenalty.penaltyApplies } : null,
  },
  bookingInfos: toArray(src.bookingInfos).map(normBookingInfo),
});

// GDS branded fare (CPO): shared core + `gds` block (relational / brand specifics).
const cpoBrandFare = (bg, index, ctx) => {
  const brand = ctx.brandMap[bg.brandRef] || null;
  const terms = ctx.termsMap[bg.termsRef] || null;
  const products = bg.productRefs.map((pr) => ctx.productMap[pr]).filter(Boolean);
  const segmentProducts = [];
  for (const p of products) for (const pf of (p.passengerFlights || [])) for (const fp of (pf.flightProducts || [])) segmentProducts.push({ ...fp, passengerTypeCode: pf.passengerTypeCode, passengerQuantity: pf.passengerQuantity });
  const first = segmentProducts[0] || null;
  const currency = bg.price.currency;
  const fare = commonFare({
    index, contentSource: 'GDS_CPO', fareFamily: brand?.name ?? null, brand: mergeBrand(brand, ctx.lfsBrandMap),
    cabin: first?.cabin, fareBasis: first?.fareBasisCode, passengerType: first?.passengerTypeCode,
    currency, base: bg.price.base, taxes: bg.price.totalTaxes, fees: bg.price.totalFees,
    surcharges: (bg.price.priceBreakdown || []).reduce((a, pb) => a + (pb.totalSurcharges || 0), 0),
    total: bg.price.totalPrice, taxBreakdown: [],
    passengerBreakdown: (bg.price.priceBreakdown || []).map((pb) => ({ passengerType: pb.passengerType, passengerAge: null, baseFare: pb.base, taxes: pb.totalTaxes, fees: pb.totalFees, totalFare: pb.total })),
  });
  fare.gds = {
    contentSourceRaw: bg.contentSource ?? null,
    classOfService: first?.classOfService ?? null, fareType: first?.fareType ?? null, fareTypeCode: first?.fareTypeCode ?? null,
    seatsAvailable: products[0]?.seatsAvailable ?? null, segmentProducts,
    baggage: terms ? normBaggage(terms.baggage) : [], penaltiesDetailed: terms ? terms.penalties : [],
    paymentTimeLimit: terms?.paymentTimeLimit ?? null, validatingAirlines: terms?.validatingAirlines ?? [],
    combinabilityCodes: bg.combinabilityCodes ?? [],
    priceBreakdown: (bg.price.priceBreakdown || []).map((pb) => ({ type: pb.type, passengerType: pb.passengerType, quantity: pb.quantity, baseFare: pb.base, taxes: pb.totalTaxes, fees: pb.totalFees, surcharges: pb.totalSurcharges, surchargeDetail: pb.surcharges, totalFare: pb.total, taxBreakdown: normTaxBreakdown(pb.taxBreakdown, currency) })),
    refs: { brandRef: bg.brandRef, productRefs: bg.productRefs, termsRef: bg.termsRef },
  };
  return fare;
};

// LFS AI fare carried on a GDS flight: shared core + `ndc` (LFS-priced) block.
// Money is returned EXACTLY as the source sends it (e.g. "INR5000.00" for LFS) — no
// numeric conversion. Base may be absent on some responses; fall back to the filed
// FareInfo.Amount, then to total-taxes (formatted to match), but the common case is verbatim.
const resolveBaseFare = (rawBase, fareInfo, totalRaw, taxesRaw) => {
  if (rawBase != null && rawBase !== '') return rawBase;
  if (fareInfo?.amount != null && fareInfo.amount !== '') return fareInfo.amount;
  const t = moneyAmount(totalRaw);
  if (t == null) return null;
  const cur = moneyCurrency(totalRaw), n = t - (moneyAmount(taxesRaw) || 0);
  return cur ? `${cur}${n.toFixed(2)}` : n;
};

const lfsFare = (po, index, ctx) => {
  const currency = moneyCurrency(po.totalPrice);
  const fareInfo = (po.fareInfoRefs || [])[0]?.fareInfo || null;
  const fare = commonFare({
    index, contentSource: 'GDS_LFS', fareFamily: fareInfo?.fareFamily, brand: fareInfo?.brandRef ? ndcBrand(fareInfo.brandRef, ctx.lfsBrandMap) : null,
    cabin: (po.bookingInfos || [])[0]?.cabinClass, fareBasis: (po.bookingInfos || [])[0]?.fareInfo?.fareBasis, passengerType: po.passengerType,
    currency, base: resolveBaseFare(po.basePrice, fareInfo, po.totalPrice, po.taxes), taxes: po.taxes ?? null, fees: po.fees ?? 0, total: po.totalPrice ?? null,
    approximateTotal: po.approximateTotalPrice ?? null, taxBreakdown: normTaxBreakdown(po.taxBreakdown, currency), passengerBreakdown: po.passengerBreakdown || [],
  });
  fare.ndc = {
    ...lfsPricedBlock(po, currency),
    negotiatedFare: fareInfo?.negotiatedFare ?? null, promotionalFare: fareInfo?.promotionalFare ?? null,
    validatingAirlines: po.platingCarrier ? [po.platingCarrier] : [],
    bookingKeys: { pricePointKey: po.pricePointKey, pricingInfoKey: po.pricingInfoKey, providerCode: po.providerCode, platingCarrier: po.platingCarrier, fareCalc: po.fareCalc, fareInfoRefs: po.fareInfoRefs },
    fareRuleKeys: (po.fareInfoRefs || []).map((fir) => fir?.fareInfo?.fareRuleKey).filter(Boolean),
    refs: { travelTime: po.travelTime, legRef: po.legRef },
  };
  return fare;
};

// NDC 6E fare: shared core + `ndc` (LFS-priced) block.
const ndcFare = (fo, index, ctx) => {
  const currency = moneyCurrency(fo.totalPrice);
  const fareInfo = (fo.fareInfoRefs || [])[0]?.fareInfo || null;
  const fare = commonFare({
    index, contentSource: 'NDC_ACH', fareFamily: fo.fareFamily ?? fareInfo?.fareFamily, brand: fareInfo?.brandRef ? ndcBrand(fareInfo.brandRef, ctx.lfsBrandMap) : null,
    cabin: (fo.bookingInfos || [])[0]?.cabinClass, fareBasis: fareInfo?.fareBasis, passengerType: fo.passengerType,
    currency, base: resolveBaseFare(fo.basePrice, fareInfo, fo.totalPrice, fo.taxes), taxes: fo.taxes ?? null, fees: fo.fees ?? 0, total: fo.totalPrice ?? null,
    approximateTotal: fo.approximateTotalPrice ?? null, taxBreakdown: normTaxBreakdown(fo.taxBreakdown, currency), passengerBreakdown: fo.passengerBreakdown || [],
  });
  fare.ndc = {
    ...lfsPricedBlock(fo, currency),
    negotiatedFare: fareInfo?.negotiatedFare ?? null, promotionalFare: fareInfo?.promotionalFare ?? null,
    validatingAirlines: fo.supplierCode ? [fo.supplierCode] : [],
    bookingKeys: { providerCode: fo.providerCode, supplierCode: fo.supplierCode, travelTime: fo.travelTime, fareInfo, completeItinerary: fo.completeItinerary ?? null },
    fareRuleKeys: (fo.fareInfoRefs || []).map((fir) => fir?.fareInfo?.fareRuleKey).filter(Boolean),
    refs: { legRef: fo.legRef },
  };
  return fare;
};

const fareSummary = (fares) => {
  const priced = fares.map((f) => f.price).filter((p) => p && moneyAmount(p.totalFare) != null);
  if (!priced.length) return { fareOptionCount: fares.length, lowestTotalFare: null, currency: null };
  const low = priced.reduce((m, p) => (moneyAmount(p.totalFare) < moneyAmount(m.totalFare) ? p : m), priced[0]);
  return { fareOptionCount: fares.length, currency: low.currency, lowestTotalFare: low.totalFare, lowestBaseFare: low.baseFare ?? null, lowestTaxes: low.taxes ?? null };
};

const connectingInfoFromCpo = (legs) => {
  if (legs.length < 2) return null;
  const layovers = [];
  for (let i = 0; i < legs.length - 1; i++) {
    const a = legs[i].arrival, d = legs[i + 1].departure;
    layovers.push({ airport: a.location, layoverDuration: minsToPT(diffMinutes(`${a.date}T${a.time}`, `${d.date}T${d.time}`)), layoverMinutes: diffMinutes(`${a.date}T${a.time}`, `${d.date}T${d.time}`), arrivalDate: a.date, arrivalTime: a.time, departureDate: d.date, departureTime: d.time });
  }
  const tot = diffMinutes(`${legs[0].departure.date}T${legs[0].departure.time}`, `${legs[legs.length - 1].arrival.date}T${legs[legs.length - 1].arrival.time}`);
  return { numberOfStops: legs.length - 1, totalJourneyDuration: minsToPT(tot), totalJourneyMinutes: tot, layovers };
};
const connectingInfoFromLfs = (segs) => {
  if (segs.length < 2) return null;
  const layovers = [];
  for (let i = 0; i < segs.length - 1; i++) {
    const m = diffMinutes(segs[i].arrivalTime, segs[i + 1].departureTime);
    layovers.push({ airport: segs[i].destination, layoverDuration: minsToPT(m), layoverMinutes: m, arrivalDate: toDateOnly(segs[i].arrivalTime), arrivalTime: segs[i].arrivalTime, departureDate: toDateOnly(segs[i + 1].departureTime), departureTime: segs[i + 1].departureTime });
  }
  const tot = diffMinutes(segs[0].departureTime, segs[segs.length - 1].arrivalTime);
  return { numberOfStops: segs.length - 1, totalJourneyDuration: minsToPT(tot), totalJourneyMinutes: tot, layovers };
};

const buildCleanGdsFromCpo = ({ source, compoundKey, flightKeys, cpoLegs, offeringInfo, lfsAiData, ctx }) => {
  const first = cpoLegs[0], last = cpoLegs[cpoLegs.length - 1], isConnecting = cpoLegs.length > 1;
  const fares = offeringInfo.brandGroups.map((bg, i) => cpoBrandFare(bg, i, ctx));
  // Carry the LFS bookable AI fares as their own options (distinct fare products
  // from the CPO brands — they get `ndc` blocks since they're LFS-priced).
  if (lfsAiData?.priceOptions?.length) lfsAiData.priceOptions.forEach((po) => fares.push(lfsFare(po, fares.length, ctx)));
  const pricedRefs = new Set(offeringInfo.brandGroups.map((bg) => bg.brandRef));
  const dep = splitDateTime(first.departure.date, first.departure.time), arr = splitDateTime(last.arrival.date, last.arrival.time);
  const productTotalDuration = ctx.productMap[offeringInfo.brandGroups[0]?.productRefs?.[0]]?.totalDuration || null;
  return {
    flightId: compoundKey, source, sourceType: 'GDS', priceAvailable: source !== FLIGHT_SOURCE.CPO_ONLY, isConnecting,
    carrier: first.carrier, carrierName: carrierName(first.carrier, ctx.carrierNames), flightNumber: first.flightNumber,
    operatingCarrier: first.operatingCarrier, operatingCarrierName: first.operatingCarrierName, operatingFlightNumber: first.operatingFlightNumber,
    origin: first.departure.location, destination: last.arrival.location,
    departure: { date: dep.date, time: dep.time, dateTime: dep.dateTime, offset: dep.offset, terminal: first.departure.terminal },
    arrival: { date: arr.date, time: arr.time, dateTime: arr.dateTime, offset: arr.offset, terminal: last.arrival.terminal },
    totalDuration: isConnecting ? (connectingInfoFromCpo(cpoLegs.map((l) => normLeg(l, ctx.carrierNames)))?.totalJourneyDuration) : first.duration,
    totalDurationMinutes: isConnecting ? connectingInfoFromCpo(cpoLegs.map((l) => normLeg(l, ctx.carrierNames)))?.totalJourneyMinutes : isoToMinutes(first.duration),
    segments: cpoLegs.map((l, i) => normLeg({ ...l, sequence: i + 1 }, ctx.carrierNames)),
    connectingInfo: isConnecting ? connectingInfoFromCpo(cpoLegs.map((l) => normLeg(l, ctx.carrierNames))) : null,
    apisRequirements: lfsAiData ? resolveApis(lfsAiData.segments, ctx.apisMap) : null,
    gds: { brandShelf: resolveShelf(offeringInfo.shelf, ctx.brandMap, pricedRefs, ctx.lfsBrandMap), offeringIds: offeringInfo.offeringIds, productTotalDuration }, // GDS-only flight extension
    provenance: { cpo: { flightKeys, offeringPresent: true, offeringIds: offeringInfo.offeringIds }, lfs: { present: !!lfsAiData, totalPriceOptions: lfsAiData?.priceOptions?.length ?? 0 } },
    fareOptions: fares, fareSummary: fareSummary(fares),
  };
};

const buildCleanGdsFromLfs = (compoundKey, data, ctx) => {
  const segs = data.segments, first = segs[0], last = segs[segs.length - 1], isConnecting = segs.length > 1;
  const fares = data.priceOptions.map((po, i) => lfsFare(po, i, ctx));
  const dep = splitDateTime(toDateOnly(first.departureTime), first.departureTime), arr = splitDateTime(toDateOnly(last.arrivalTime), last.arrivalTime);
  const tot = isConnecting ? diffMinutes(first.departureTime, last.arrivalTime) : (first.flightTime || first.travelTime);
  return {
    flightId: compoundKey, source: FLIGHT_SOURCE.GDS_LFS, sourceType: 'GDS', priceAvailable: true, isConnecting,
    carrier: first.carrier, carrierName: carrierName(first.carrier, ctx.carrierNames), flightNumber: first.flightNumber,
    operatingCarrier: first.operatingCarrier, operatingCarrierName: first.operatingCarrierName, operatingFlightNumber: first.operatingFlightNumber,
    origin: first.origin, destination: last.destination,
    departure: { date: dep.date, time: dep.time, dateTime: dep.dateTime, offset: dep.offset, terminal: first.originTerminal },
    arrival: { date: arr.date, time: arr.time, dateTime: arr.dateTime, offset: arr.offset, terminal: last.destinationTerminal },
    totalDuration: minsToPT(tot), totalDurationMinutes: tot ?? null,
    segments: segs.map((s, i) => normLeg({ ...s, sequence: i + 1 }, ctx.carrierNames)),
    connectingInfo: isConnecting ? connectingInfoFromLfs(segs) : null,
    apisRequirements: resolveApis(segs, ctx.apisMap),
    gds: { brandShelf: [], offeringIds: [], productTotalDuration: null }, // GDS bucket: kept for shape consistency (no CPO offering)
    provenance: { cpo: { flightKeys: compoundKey.split('::'), offeringPresent: false, offeringIds: [] }, lfs: { present: true, totalPriceOptions: data.priceOptions.length } },
    fareOptions: fares, fareSummary: fareSummary(fares),
  };
};

const buildCleanNdc = (indigoFlights, ctx) => {
  const flights = [];
  for (const [compoundKey, data] of Object.entries(indigoFlights)) {
    if (!data.segments.length) continue;
    const segs = data.segments, first = segs[0], last = segs[segs.length - 1], isConnecting = segs.length > 1;
    const sorted = [...data.fareOptions].sort((a, b) => moneyAmount(a.totalPrice) - moneyAmount(b.totalPrice));
    const fares = sorted.map((fo, i) => ndcFare(fo, i, ctx));
    const dep = splitDateTime(toDateOnly(first.departureTime), first.departureTime), arr = splitDateTime(toDateOnly(last.arrivalTime), last.arrivalTime);
    const tot = isConnecting ? diffMinutes(first.departureTime, last.arrivalTime) : segs.reduce((a, s) => a + (s.flightTime || 0), 0);
    flights.push({
      flightId: compoundKey, source: FLIGHT_SOURCE.NDC_ACH, sourceType: 'NDC', priceAvailable: true, isConnecting,
      carrier: first.carrier, carrierName: carrierName(first.carrier, ctx.carrierNames), flightNumber: first.flightNumber,
      operatingCarrier: first.operatingCarrier || first.carrier, operatingCarrierName: first.operatingCarrierName || null, operatingFlightNumber: first.operatingFlightNumber || first.flightNumber,
      origin: first.origin, destination: last.destination,
      departure: { date: dep.date, time: dep.time, dateTime: dep.dateTime, offset: dep.offset, terminal: first.originTerminal },
      arrival: { date: arr.date, time: arr.time, dateTime: arr.dateTime, offset: arr.offset, terminal: last.destinationTerminal },
      totalDuration: minsToPT(tot), totalDurationMinutes: tot ?? null,
      segments: segs.map((s, i) => normLeg({ ...s, sequence: i + 1 }, ctx.carrierNames)),
      connectingInfo: isConnecting ? connectingInfoFromLfs(segs) : null,
      apisRequirements: resolveApis(segs, ctx.apisMap),
      provenance: { cpo: { offeringPresent: false, offeringIds: [] }, lfs: { present: true, totalFareOptions: sorted.length } },
      fareOptions: fares, fareSummary: fareSummary(fares),
    });
  }
  flights.sort((a, b) => ((a.departure.dateTime || '') > (b.departure.dateTime || '') ? 1 : -1));
  return flights;
};

/* ═══════════════════════ public API ═══════════════════════ */

const buildUnifiedResponse = (rawLfs, rawCpo, meta = {}) => {
  const lfsData = parseLfsResponse(rawLfs, meta);
  const cpoData = parseCpoResponse(rawCpo);
  const { gdsFlights, ndcFlights } = merge(lfsData, cpoData, meta);
  return {
    success: true, traceId: meta.traceId ?? null, travelerRefs: meta.travelerRefs ?? null, searchParams: meta.searchParams ?? null,
    summary: { totalGdsFlights: gdsFlights.length, totalNdcFlights: ndcFlights.length, totalFlights: gdsFlights.length + ndcFlights.length },
    gdsFlights: { source: 'GDS', totalFlights: gdsFlights.length, flights: gdsFlights },
    ndcFlights: { source: 'NDC', totalFlights: ndcFlights.length, flights: ndcFlights },
  };
};

module.exports = { parseLfsResponse, parseCpoResponse, merge, buildUnifiedResponse };