/**
 * Travel tool adapters.
 *
 * Four read-only tools so agents can shop for flights and hotels and look up
 * live flight status:
 *
 *   - flightSearch(input, options)   — Amadeus Self-Service /v2/shopping/flight-offers
 *   - flightStatus(input, options)   — Aviationstack /v1/flights
 *   - hotelSearch(input, options)    — Xotelo /api/list (no key required)
 *   - hotelRates(input, options)     — Xotelo /api/rates (no key required)
 *
 * Credential resolution: vault first (getSecret), falling back to process.env
 * for backwards compatibility during the migration window — same contract as
 * the Resend and GitHub adapters.
 *
 * Security model:
 *   - All four tools are risk_level: 'low' — they are read-only shopping/lookup
 *     calls with no side effects on the user's account at the provider.
 *   - No allowlist gate (allowlist_default_allow: true). Travel research is the
 *     point of the tool; scoping which airline/destination would be hostile.
 *   - Rate-limited at 60/min per tool, per the existing read-tool convention.
 *   - wrapFetch records every outbound call to tool_egress.
 *   - API keys never appear in error messages or response payloads.
 *
 * Amadeus token caching:
 *   The Amadeus OAuth token is short-lived (~30 min). We cache it in-process
 *   keyed by environment so we do not mint a new token on every flight search.
 *   The cache is invalidated 60s before expiry to avoid edge-case 401s mid-flight.
 */

import { getSecret } from '../../secrets.js';
import { wrapFetch } from '../../egress.js';
import { googleFlightsSearch } from './google-flights.js';

// ── Credential helpers ─────────────────────────────────────────────────────

function resolveCredential(name, options = {}) {
  try {
    const secret = getSecret(name, {
      agent_id: options.agent_id,
      project_id: options.project_id,
    });
    if (secret) return secret;
  } catch {
    // Vault not ready — fall through to process.env.
  }
  return process.env[name] || null;
}

function requireCredential(name, options = {}) {
  const value = resolveCredential(name, options);
  if (!value) throw new Error(`${name} not configured`);
  return value;
}

// ── Shared helpers ─────────────────────────────────────────────────────────

function clampLimit(value, fallback, min, max) {
  const n = Number(value);
  if (!Number.isFinite(n)) return fallback;
  return Math.min(Math.max(Math.trunc(n), min), max);
}

function normIata(value, name) {
  const str = String(value || '').trim().toUpperCase();
  if (!/^[A-Z]{3}$/.test(str)) {
    throw new Error(`"${name}" must be a 3-letter IATA code (got: ${value ?? ''})`);
  }
  return str;
}

function normDate(value, name) {
  const str = String(value || '').trim();
  if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) {
    throw new Error(`"${name}" must be ISO date YYYY-MM-DD (got: ${value ?? ''})`);
  }
  return str;
}

async function safeErrorMessage(res, defaultMsg) {
  try {
    const text = await res.text();
    if (!text) return defaultMsg;
    // Trim and cap — never let upstream HTML leak past 300 chars.
    return text.slice(0, 300);
  } catch {
    return defaultMsg;
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// Amadeus Self-Service — flight search
// ═══════════════════════════════════════════════════════════════════════════

function amadeusBaseUrl(options = {}) {
  const env = (resolveCredential('AMADEUS_ENV', options) || 'test').toLowerCase();
  if (env === 'production' || env === 'prod') return 'https://api.amadeus.com';
  return 'https://test.api.amadeus.com';
}

// In-process token cache keyed by base URL (so prod/test caches don't collide).
const _amadeusTokenCache = new Map();

async function amadeusToken(options = {}) {
  const baseUrl = amadeusBaseUrl(options);
  const cached = _amadeusTokenCache.get(baseUrl);
  if (cached && cached.expires_at > Date.now() + 60_000) {
    return cached.access_token;
  }

  const clientId = requireCredential('AMADEUS_CLIENT_ID', options);
  const clientSecret = requireCredential('AMADEUS_CLIENT_SECRET', options);

  const wrappedFetch = wrapFetch('flight_search', options.agent_id ?? null);
  const res = await wrappedFetch(`${baseUrl}/v1/security/oauth2/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Accept: 'application/json',
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: clientId,
      client_secret: clientSecret,
    }).toString(),
  });

  if (!res.ok) {
    const detail = await safeErrorMessage(res, `HTTP ${res.status}`);
    // Never surface clientSecret. The body only contains an error code + description.
    throw new Error(`Amadeus auth failed (${res.status}): ${detail}`);
  }

  const data = await res.json();
  const accessToken = String(data.access_token || '');
  const expiresIn = Number(data.expires_in || 1800);
  if (!accessToken) throw new Error('Amadeus auth returned no access_token');

  _amadeusTokenCache.set(baseUrl, {
    access_token: accessToken,
    expires_at: Date.now() + expiresIn * 1000,
  });
  return accessToken;
}

function curateAmadeusOffer(offer = {}) {
  const itineraries = Array.isArray(offer.itineraries) ? offer.itineraries : [];
  const segments = itineraries.flatMap(it => Array.isArray(it?.segments) ? it.segments : []);
  return {
    id: offer.id || null,
    price: {
      total: offer.price?.total ?? null,
      currency: offer.price?.currency ?? null,
      base: offer.price?.base ?? null,
    },
    seats_available: offer.numberOfBookableSeats ?? null,
    last_ticketing_date: offer.lastTicketingDate ?? null,
    itineraries: itineraries.map(it => ({
      duration: it?.duration ?? null,
      stops: Math.max(0, (Array.isArray(it?.segments) ? it.segments.length : 1) - 1),
      segments: (Array.isArray(it?.segments) ? it.segments : []).map(seg => ({
        carrier: seg?.carrierCode ?? null,
        flight_number: seg?.number ?? null,
        departure: {
          iata: seg?.departure?.iataCode ?? null,
          at: seg?.departure?.at ?? null,
          terminal: seg?.departure?.terminal ?? null,
        },
        arrival: {
          iata: seg?.arrival?.iataCode ?? null,
          at: seg?.arrival?.at ?? null,
          terminal: seg?.arrival?.terminal ?? null,
        },
        duration: seg?.duration ?? null,
        aircraft: seg?.aircraft?.code ?? null,
      })),
    })),
    validating_airlines: Array.isArray(offer.validatingAirlineCodes) ? offer.validatingAirlineCodes : [],
    cabin: segments[0]?.cabin || offer.travelerPricings?.[0]?.fareDetailsBySegment?.[0]?.cabin || null,
  };
}

/**
 * Search Amadeus flight offers (internal — called via the router below).
 *
 * Required input:
 *   - origin (IATA)
 *   - destination (IATA)
 *   - departure_date (YYYY-MM-DD)
 *
 * Optional input:
 *   - return_date (YYYY-MM-DD) — round-trip when provided
 *   - adults (1-9, default 1)
 *   - children (0-8)
 *   - travel_class (ECONOMY|PREMIUM_ECONOMY|BUSINESS|FIRST)
 *   - currency (3-letter ISO, e.g. USD)
 *   - non_stop (boolean)
 *   - max_results (1-25, default 5)
 */
async function _flightSearchAmadeus(input = {}, options = {}) {
  const origin = normIata(input.origin, 'origin');
  const destination = normIata(input.destination, 'destination');
  const departureDate = normDate(input.departure_date, 'departure_date');
  const returnDate = input.return_date ? normDate(input.return_date, 'return_date') : null;
  const adults = clampLimit(input.adults, 1, 1, 9);
  const children = clampLimit(input.children, 0, 0, 8);
  const max = clampLimit(input.max_results, 5, 1, 25);

  const params = new URLSearchParams({
    originLocationCode: origin,
    destinationLocationCode: destination,
    departureDate,
    adults: String(adults),
    max: String(max),
    currencyCode: input.currency ? String(input.currency).toUpperCase().slice(0, 3) : 'USD',
  });
  if (returnDate) params.set('returnDate', returnDate);
  if (children > 0) params.set('children', String(children));
  if (input.travel_class) {
    const tc = String(input.travel_class).toUpperCase();
    if (['ECONOMY', 'PREMIUM_ECONOMY', 'BUSINESS', 'FIRST'].includes(tc)) {
      params.set('travelClass', tc);
    }
  }
  if (input.non_stop === true || input.non_stop === 'true') params.set('nonStop', 'true');

  const token = await amadeusToken(options);
  const baseUrl = amadeusBaseUrl(options);
  const wrappedFetch = wrapFetch('flight_search', options.agent_id ?? null);
  const res = await wrappedFetch(`${baseUrl}/v2/shopping/flight-offers?${params.toString()}`, {
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
    },
  });

  if (res.status === 401) {
    // Token was rejected mid-flight — invalidate cache and surface a clean error.
    _amadeusTokenCache.delete(baseUrl);
    const detail = await safeErrorMessage(res, 'unauthorized');
    throw new Error(`Amadeus auth rejected (401): ${detail}`);
  }
  if (!res.ok) {
    const detail = await safeErrorMessage(res, `HTTP ${res.status}`);
    throw new Error(`Amadeus flight offers failed (${res.status}): ${detail}`);
  }

  const data = await res.json();
  const offers = Array.isArray(data.data) ? data.data : [];
  return {
    query: {
      origin,
      destination,
      departure_date: departureDate,
      return_date: returnDate,
      adults,
      children,
      currency: params.get('currencyCode'),
      travel_class: params.get('travelClass') || null,
      non_stop: params.get('nonStop') === 'true',
      max_results: max,
    },
    count: offers.length,
    offers: offers.map(curateAmadeusOffer),
    source: {
      provider: 'amadeus',
      environment: baseUrl.includes('test.api') ? 'test' : 'production',
      fetched_at: new Date().toISOString(),
    },
  };
}

/**
 * Public flight_search router.
 *
 * - If both AMADEUS_CLIENT_ID and AMADEUS_CLIENT_SECRET resolve, hit Amadeus first.
 * - If Amadeus throws, log a warning and fall back to Google Flights — unless
 *   MADARX_FLIGHT_SEARCH_NO_FALLBACK=1 is set, in which case the Amadeus error
 *   is surfaced as-is (useful for operators who want strict provider isolation).
 * - If Amadeus credentials are absent, go straight to Google Flights. The tool
 *   is therefore always available, regardless of credential state.
 *
 * The response shape is identical across providers; callers branch on
 * `source.provider` only when they want to disclose the data origin.
 */
export async function flightSearch(input = {}, options = {}) {
  const hasAmadeus = Boolean(
    resolveCredential('AMADEUS_CLIENT_ID', options) &&
    resolveCredential('AMADEUS_CLIENT_SECRET', options)
  );
  const noFallback = String(process.env.MADARX_FLIGHT_SEARCH_NO_FALLBACK || '').trim() === '1';

  if (hasAmadeus) {
    try {
      return await _flightSearchAmadeus(input, options);
    } catch (err) {
      if (noFallback) throw err;
      console.warn(
        '[flight_search] Amadeus failed, falling back to Google Flights:',
        err?.message || err
      );
    }
  }

  return googleFlightsSearch(input, options);
}

// ═══════════════════════════════════════════════════════════════════════════
// Aviationstack — flight status / tracking
// ═══════════════════════════════════════════════════════════════════════════

// Aviationstack free tier requires HTTP (not HTTPS). We default to HTTPS so paid
// plans get encrypted egress, and let the operator opt into HTTP for the free tier.
function aviationstackBaseUrl(options = {}) {
  const useHttps = String(resolveCredential('AVIATIONSTACK_HTTPS', options) || '').toLowerCase();
  // Default: HTTP (free tier compatibility). Set AVIATIONSTACK_HTTPS=1 for paid plans.
  if (useHttps === '1' || useHttps === 'true' || useHttps === 'yes') {
    return 'https://api.aviationstack.com';
  }
  return 'http://api.aviationstack.com';
}

function curateAviationstackFlight(row = {}) {
  return {
    flight_date: row.flight_date ?? null,
    flight_status: row.flight_status ?? null,
    airline: row.airline?.name ?? null,
    flight: {
      iata: row.flight?.iata ?? null,
      icao: row.flight?.icao ?? null,
      number: row.flight?.number ?? null,
    },
    departure: {
      airport: row.departure?.airport ?? null,
      iata: row.departure?.iata ?? null,
      icao: row.departure?.icao ?? null,
      terminal: row.departure?.terminal ?? null,
      gate: row.departure?.gate ?? null,
      scheduled: row.departure?.scheduled ?? null,
      estimated: row.departure?.estimated ?? null,
      actual: row.departure?.actual ?? null,
      delay_minutes: row.departure?.delay ?? null,
    },
    arrival: {
      airport: row.arrival?.airport ?? null,
      iata: row.arrival?.iata ?? null,
      icao: row.arrival?.icao ?? null,
      terminal: row.arrival?.terminal ?? null,
      gate: row.arrival?.gate ?? null,
      scheduled: row.arrival?.scheduled ?? null,
      estimated: row.arrival?.estimated ?? null,
      actual: row.arrival?.actual ?? null,
      delay_minutes: row.arrival?.delay ?? null,
    },
    aircraft: row.aircraft?.registration ?? null,
    live: row.live ? {
      latitude: row.live.latitude ?? null,
      longitude: row.live.longitude ?? null,
      altitude: row.live.altitude ?? null,
      speed_horizontal: row.live.speed_horizontal ?? null,
      is_ground: row.live.is_ground ?? null,
      updated: row.live.updated ?? null,
    } : null,
  };
}

/**
 * Look up flight status / tracking data via Aviationstack.
 *
 * At least one identifier is required:
 *   - flight_iata (e.g. "BA283")
 *   - flight_icao
 *   - airline_iata + flight_date
 *   - dep_iata + arr_iata
 *
 * Optional:
 *   - flight_date (YYYY-MM-DD)
 *   - limit (1-100, default 5)
 */
export async function flightStatus(input = {}, options = {}) {
  const flightIata = input.flight_iata ? String(input.flight_iata).trim().toUpperCase() : null;
  const flightIcao = input.flight_icao ? String(input.flight_icao).trim().toUpperCase() : null;
  const airlineIata = input.airline_iata ? String(input.airline_iata).trim().toUpperCase() : null;
  const depIata = input.dep_iata ? normIata(input.dep_iata, 'dep_iata') : null;
  const arrIata = input.arr_iata ? normIata(input.arr_iata, 'arr_iata') : null;
  const flightDate = input.flight_date ? normDate(input.flight_date, 'flight_date') : null;
  const limit = clampLimit(input.limit, 5, 1, 100);

  if (!flightIata && !flightIcao && !airlineIata && !depIata && !arrIata) {
    throw new Error(
      'At least one of flight_iata, flight_icao, airline_iata, dep_iata, or arr_iata is required'
    );
  }

  const apiKey = requireCredential('AVIATIONSTACK_API_KEY', options);
  const params = new URLSearchParams({ access_key: apiKey, limit: String(limit) });
  if (flightIata) params.set('flight_iata', flightIata);
  if (flightIcao) params.set('flight_icao', flightIcao);
  if (airlineIata) params.set('airline_iata', airlineIata);
  if (depIata) params.set('dep_iata', depIata);
  if (arrIata) params.set('arr_iata', arrIata);
  if (flightDate) params.set('flight_date', flightDate);

  const baseUrl = aviationstackBaseUrl(options);
  const wrappedFetch = wrapFetch('flight_status', options.agent_id ?? null);
  const res = await wrappedFetch(`${baseUrl}/v1/flights?${params.toString()}`);

  if (!res.ok) {
    const detail = await safeErrorMessage(res, `HTTP ${res.status}`);
    throw new Error(`Aviationstack failed (${res.status}): ${detail}`);
  }

  const data = await res.json();
  // Aviationstack returns errors as 200 with an `error` field on quota/auth issues.
  if (data?.error) {
    const code = data.error.code || 'error';
    const msg = data.error.message || data.error.info || 'unknown error';
    throw new Error(`Aviationstack error (${code}): ${msg}`);
  }

  const flights = Array.isArray(data?.data) ? data.data : [];
  return {
    query: {
      flight_iata: flightIata,
      flight_icao: flightIcao,
      airline_iata: airlineIata,
      dep_iata: depIata,
      arr_iata: arrIata,
      flight_date: flightDate,
      limit,
    },
    count: flights.length,
    pagination: data?.pagination || null,
    flights: flights.map(curateAviationstackFlight),
    source: {
      provider: 'aviationstack',
      fetched_at: new Date().toISOString(),
    },
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// Xotelo — hotel search and rate lookup (no API key)
// ═══════════════════════════════════════════════════════════════════════════

const XOTELO_BASE = 'https://data.xotelo.com';

function curateXoteloHotel(hotel = {}) {
  return {
    key: hotel.key ?? hotel.hotel_key ?? null,
    name: hotel.name ?? null,
    geo: {
      latitude: hotel.geo?.latitude ?? null,
      longitude: hotel.geo?.longitude ?? null,
    },
    address: hotel.address ?? null,
    stars: hotel.stars ?? null,
    review_summary: hotel.review_summary ? {
      score: hotel.review_summary.score ?? null,
      count: hotel.review_summary.count ?? null,
    } : null,
    price: hotel.price ?? null,
    accommodation_type: hotel.accommodation_type ?? null,
  };
}

/**
 * List hotels at a TripAdvisor location via Xotelo /api/list.
 *
 * Required:
 *   - location_key — TripAdvisor geo location key (e.g. "g60763" for New York City)
 *
 * Optional:
 *   - offset (default 0)
 *   - limit (1-30, default 10) — Xotelo returns up to 30 per page
 *   - sort (popularity|price|rating)
 *   - currency (3-letter ISO, default USD)
 */
export async function hotelSearch(input = {}, options = {}) {
  const locationKey = String(input.location_key || '').trim();
  if (!locationKey) throw new Error('"location_key" is required');
  if (!/^[a-z]?[0-9]+$/i.test(locationKey)) {
    throw new Error(`"location_key" must be a TripAdvisor key like "g60763" (got: ${locationKey})`);
  }

  const offset = clampLimit(input.offset, 0, 0, 1000);
  const limit = clampLimit(input.limit, 10, 1, 30);
  const currency = input.currency ? String(input.currency).toUpperCase().slice(0, 3) : 'USD';

  const params = new URLSearchParams({
    location_key: locationKey,
    offset: String(offset),
    limit: String(limit),
    currency,
  });
  if (input.sort) {
    const sort = String(input.sort).toLowerCase();
    if (['popularity', 'price', 'rating'].includes(sort)) params.set('sort', sort);
  }
  if (input.hotel_class) params.set('hotel_class', String(input.hotel_class));

  const wrappedFetch = wrapFetch('hotel_search', options.agent_id ?? null);
  const res = await wrappedFetch(`${XOTELO_BASE}/api/list?${params.toString()}`);

  if (!res.ok) {
    const detail = await safeErrorMessage(res, `HTTP ${res.status}`);
    throw new Error(`Xotelo list failed (${res.status}): ${detail}`);
  }

  const data = await res.json();
  // Xotelo wraps responses in { error: null, result: {...}, timestamp }.
  if (data?.error) {
    throw new Error(`Xotelo error: ${typeof data.error === 'string' ? data.error : JSON.stringify(data.error)}`);
  }

  const result = data?.result || {};
  const hotels = Array.isArray(result.list) ? result.list : [];
  return {
    query: { location_key: locationKey, offset, limit, currency, sort: params.get('sort') || null },
    count: hotels.length,
    total_available: result.number_of_results ?? null,
    hotels: hotels.map(curateXoteloHotel),
    source: {
      provider: 'xotelo',
      fetched_at: new Date().toISOString(),
    },
  };
}

function curateXoteloRate(rate = {}) {
  return {
    vendor: rate.name ?? rate.code ?? null,
    code: rate.code ?? null,
    rate: rate.rate ?? null,
    tax: rate.tax ?? null,
    total: rate.total ?? null,
    currency: rate.currency ?? null,
    deeplink: rate.deeplink ?? null,
    is_official: rate.is_official ?? null,
    free_cancellation: rate.free_cancellation ?? null,
  };
}

/**
 * Get hotel rates from booking partners via Xotelo /api/rates.
 *
 * Required:
 *   - hotel_key — TripAdvisor hotel key (e.g. "g60763-d224867")
 *   - chk_in (YYYY-MM-DD)
 *   - chk_out (YYYY-MM-DD)
 *
 * Optional:
 *   - currency (3-letter ISO, default USD)
 *   - rooms (1-9, default 1)
 *   - adults (1-9, default 2)
 */
export async function hotelRates(input = {}, options = {}) {
  const hotelKey = String(input.hotel_key || '').trim();
  if (!hotelKey) throw new Error('"hotel_key" is required');
  if (!/^[a-z]?[0-9]+-[a-z]?[0-9]+$/i.test(hotelKey)) {
    throw new Error(`"hotel_key" must be a TripAdvisor hotel key like "g60763-d224867" (got: ${hotelKey})`);
  }

  const chkIn = normDate(input.chk_in, 'chk_in');
  const chkOut = normDate(input.chk_out, 'chk_out');
  if (new Date(chkOut) <= new Date(chkIn)) {
    throw new Error('"chk_out" must be after "chk_in"');
  }

  const currency = input.currency ? String(input.currency).toUpperCase().slice(0, 3) : 'USD';
  const rooms = clampLimit(input.rooms, 1, 1, 9);
  const adults = clampLimit(input.adults, 2, 1, 9);

  const params = new URLSearchParams({
    hotel_key: hotelKey,
    chk_in: chkIn,
    chk_out: chkOut,
    currency,
    rooms: String(rooms),
    adults: String(adults),
  });

  const wrappedFetch = wrapFetch('hotel_rates', options.agent_id ?? null);
  const res = await wrappedFetch(`${XOTELO_BASE}/api/rates?${params.toString()}`);

  if (!res.ok) {
    const detail = await safeErrorMessage(res, `HTTP ${res.status}`);
    throw new Error(`Xotelo rates failed (${res.status}): ${detail}`);
  }

  const data = await res.json();
  if (data?.error) {
    throw new Error(`Xotelo error: ${typeof data.error === 'string' ? data.error : JSON.stringify(data.error)}`);
  }

  const result = data?.result || {};
  const rates = Array.isArray(result.rates) ? result.rates : [];
  return {
    query: { hotel_key: hotelKey, chk_in: chkIn, chk_out: chkOut, currency, rooms, adults },
    count: rates.length,
    hotel: {
      name: result.name ?? null,
      stars: result.stars ?? null,
    },
    rates: rates.map(curateXoteloRate),
    source: {
      provider: 'xotelo',
      fetched_at: new Date().toISOString(),
    },
  };
}
