/* wing-agent — the brain behind Mr. Wing. Three real tiers, no fakes:
   1) live Anthropic Messages API (browser-direct) using the key saved in Admin
   2) window.claude.complete fallback (claude.ai artifact runtime)
   3) deterministic local NLU over the live inventory — so the agent fully works
      with NO key and NO window.claude (search / book / cancel / modify / escalate).
   Every tier returns the SAME { clean, card } shape the chat UI confirms. */

/* ---------- system prompt (sees the real generated inventory) ---------- */
function buildWingSystem(store, lang) {
  const ctx = store.searchCtx || {};
  const fl = (window.generateFlights ? generateFlights({ from: ctx.from, to: ctx.to, date: ctx.depart }) : []).slice(0, 6);
  const flStr = fl.map(f => `${f.id} ${f.airline} ${f.from}->${f.to} dep ${f.dep} arr ${f.arr} ${f.stops ? f.stops + "stop" : "nonstop"} ${f.cabin} SAR${f.price}`).join("; ");
  const hCity = ctx.destination || ctx.to;
  const ht = (window.generateHotels ? generateHotels(hCity, ctx.depart) : []).slice(0, 6);
  const htStr = ht.map(h => `${h.name} (${h.area}) ${h.rating}* corp SAR${h.corp}/night`).join("; ");
  const cards = (store.cards || []).map(c => `${c.name} ${c.brand} ••${c.last4}${c.primary ? " (primary)" : ""}`).join("; ");
  const today = new Date().toISOString().slice(0, 10);
  return `You are Mr. Wing, the autonomous AI travel agent for Thirdwing — an IATA-certified premium Saudi corporate travel agency (est. 2021, Riyadh HQ, Egypt 24/7 support hub).

PERSONA: Professional, efficient, warm — the cultural hospitality of a native Saudi travel representative. Concise. Never use emoji. A brief Arabic greeting like "Ahlan wa sahlan" is welcome when natural.

SCOPE: STRICTLY Flights, Hotels and tour packages. Politely decline anything else.

TODAY: ${today}. CURRENT SEARCH CONTEXT: route ${ctx.from} -> ${ctx.to} on ${ctx.depart}, hotel destination ${hCity}.
LIVE FLIGHTS you can quote (use these exact codes/prices): ${flStr || "(run a new search by route)"}
LIVE HOTELS in ${window.cityName ? cityName(hCity) : hCity}: ${htStr || "(none cached)"}
SAVED PAYMENT PROFILES (Tap Payments, tokenized): ${cards || "(none)"}

CAPABILITIES: SEARCH (natural language → specific options from inventory), BOOK (1-click with a saved Tap profile), UPDATE/CANCEL (change hotel dates or cancel flights; refunds follow supplier policy: flights ~80% refundable, hotels ~90%).

SAFETY PROTOCOL — CRITICAL:
- Before finalizing ANY booking or cancellation you MUST output an ACTION block so the interface shows a visual summary card the user explicitly confirms. Never claim something is booked/cancelled without that confirmation.
- VVIP / government-protocol requests (private transfers, discretion, heads of state, large delegations) must INSTANTLY escalate to the human Egypt Support Hub via an escalate action.

ACTION PROTOCOL: To ask the user to confirm an action, END your message with a fenced block exactly like:
\`\`\`action
{"type":"book","mode":"flight","title":"RUH → JED · Saudia SV1021","detail":"06:15–07:55 · Business · non-stop","price":940,"pay":"Mada ••4421"}
\`\`\`
Valid types: "book" (mode flight|hotel|package; include price and pay), "cancel" (include title and refund number), "escalate" (include reason). Only ONE action block per message, only when you genuinely need confirmation. For plain questions or search results do NOT include an action block. Keep the conversational text BEFORE the block short (1-3 sentences).${lang === "ar" ? "\n\nRESPOND IN ARABIC (Modern Standard Arabic, professional)." : ""}`;
}

/* ---------- hardened action-block parser ---------- */
function parseWingAction(text) {
  const m = (text || "").match(/```action\s*([\s\S]*?)```/);
  let card = null;
  if (m) {
    try {
      const obj = JSON.parse(m[1].trim());
      if (obj && typeof obj === "object" && ["book", "cancel", "escalate"].includes(obj.type)) {
        if (obj.type === "book") {
          if (obj.title && (typeof obj.price === "number" || typeof obj.price === "string")) {
            card = { type: "book", mode: obj.mode || "flight", title: String(obj.title), detail: obj.detail ? String(obj.detail) : "", price: Number(obj.price) || 0, pay: obj.pay ? String(obj.pay) : "Saved card" };
          }
        } else if (obj.type === "cancel") {
          if (obj.title) card = { type: "cancel", title: String(obj.title), refund: Number(obj.refund) || 0, bookingId: obj.bookingId };
        } else if (obj.type === "escalate") {
          card = { type: "escalate", reason: obj.reason ? String(obj.reason) : "" };
        }
      }
    } catch (e) { card = null; }
  }
  const clean = (text || "").replace(/```action[\s\S]*?```/g, "").trim();
  return { clean, card };
}

/* ---------- tier 1: browser-direct AI client (OpenAI-compatible + Anthropic) ----------
   Used in static / no-backend mode and as a fallback when the /api/wing proxy is
   unreachable. CORS permitting — most managed providers do NOT send CORS headers
   for /chat/completions, so the SECURE path is always the server proxy. These
   helpers mirror api/_lib/ai.js so the admin panel can also fetch models locally. */
const AI_ANTHROPIC_VERSION = "2023-06-01";

function aiKindOf(baseUrl) { return /anthropic\.com/i.test(String(baseUrl || "")) ? "anthropic" : "openai"; }
function aiNormBase(baseUrl, kind) {
  let u = String(baseUrl || "").trim().replace(/\/+$/, "");
  if (!u) return kind === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com/v1";
  if (kind === "anthropic") u = u.replace(/\/v1$/i, "");
  return u;
}
function aiHeadersBrowser(kind, key) {
  return kind === "anthropic"
    ? { "content-type": "application/json", "x-api-key": key, "anthropic-version": AI_ANTHROPIC_VERSION, "anthropic-dangerous-direct-browser-access": "true" }
    : { "content-type": "application/json", Authorization: "Bearer " + key };
}
function aiNormalizeModels(data, kind) {
  let arr = [];
  if (Array.isArray(data)) arr = data;
  else if (data && Array.isArray(data.data)) arr = data.data;
  else if (data && Array.isArray(data.models)) arr = data.models;
  const out = [], seen = new Set();
  for (const m of arr) {
    const id = typeof m === "string" ? m : (m && (m.id || m.name || m.model));
    if (!id || seen.has(String(id))) continue;
    seen.add(String(id));
    const display = m && typeof m === "object" ? (m.display_name || m.name) : "";
    out.push({ id: String(id), label: display && display !== id ? String(display) : "" });
  }
  out.sort((a, b) => a.id.localeCompare(b.id));
  return out;
}

/* fetch the provider's model catalogue (browser-direct). Throws on non-2xx. */
async function fetchAIModels({ key, baseUrl, kind }) {
  kind = kind || aiKindOf(baseUrl);
  const base = aiNormBase(baseUrl, kind);
  const url = kind === "anthropic" ? base + "/v1/models?limit=1000" : base + "/models";
  const r = await fetch(url, { headers: aiHeadersBrowser(kind, key) });
  if (!r.ok) { let d = ""; try { d = await r.text(); } catch (e) {} throw new Error(kind + "_" + r.status + (d ? ": " + d.slice(0, 200) : "")); }
  const data = await r.json();
  return aiNormalizeModels(data, kind);
}

/* native Anthropic Messages API (browser-direct, streaming) */
async function callAnthropic({ apiKey, model, system, messages, onToken, base }) {
  const res = await fetch(aiNormBase(base, "anthropic") + "/v1/messages", {
    method: "POST",
    headers: aiHeadersBrowser("anthropic", apiKey),
    body: JSON.stringify({ model: model || "claude-haiku-4-5-20251001", max_tokens: 1024, system, messages, stream: !!onToken }),
  });
  if (!res.ok) {
    let detail = ""; try { detail = await res.text(); } catch (e) {}
    throw new Error("anthropic_" + res.status + (detail ? ": " + detail.slice(0, 240) : ""));
  }
  if (onToken && res.body && res.body.getReader) {
    const reader = res.body.getReader(), dec = new TextDecoder();
    let buf = "", full = "";
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buf += dec.decode(value, { stream: true });
      let nl;
      while ((nl = buf.indexOf("\n")) >= 0) {
        const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
        if (!line.startsWith("data:")) continue;
        const payload = line.slice(5).trim();
        if (!payload || payload === "[DONE]") continue;
        try {
          const ev = JSON.parse(payload);
          if (ev.type === "content_block_delta" && ev.delta && typeof ev.delta.text === "string") { full += ev.delta.text; onToken(ev.delta.text); }
        } catch (e) {}
      }
    }
    return full;
  }
  const data = await res.json();
  return (data.content || []).filter(b => b.type === "text").map(b => b.text).join("");
}

/* OpenAI-compatible Chat Completions (browser-direct, streaming) */
async function callOpenAI({ apiKey, baseUrl, model, system, messages, onToken }) {
  const base = aiNormBase(baseUrl, "openai");
  const msgs = [];
  if (system) msgs.push({ role: "system", content: system });
  for (const m of (messages || [])) msgs.push({ role: m.role, content: String(m.content || "") });
  const res = await fetch(base + "/chat/completions", {
    method: "POST",
    headers: aiHeadersBrowser("openai", apiKey),
    body: JSON.stringify({ model, max_tokens: 1024, messages: msgs, stream: !!onToken }),
  });
  if (!res.ok) {
    let detail = ""; try { detail = await res.text(); } catch (e) {}
    throw new Error("openai_" + res.status + (detail ? ": " + detail.slice(0, 240) : ""));
  }
  if (onToken && res.body && res.body.getReader) {
    const reader = res.body.getReader(), dec = new TextDecoder();
    let buf = "", full = "";
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buf += dec.decode(value, { stream: true });
      let nl;
      while ((nl = buf.indexOf("\n")) >= 0) {
        const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
        if (!line.startsWith("data:")) continue;
        const payload = line.slice(5).trim();
        if (!payload || payload === "[DONE]") continue;
        try {
          const ev = JSON.parse(payload);
          const delta = ev.choices && ev.choices[0] && ev.choices[0].delta && ev.choices[0].delta.content;
          if (typeof delta === "string") { full += delta; onToken(delta); }
        } catch (e) {}
      }
    }
    return full;
  }
  const data = await res.json();
  const choice = (data.choices && data.choices[0]) || {};
  const c = (choice.message && choice.message.content) != null ? choice.message.content : choice.text;
  return typeof c === "string" ? c : (Array.isArray(c) ? c.map((x) => (x && (x.text || x.content)) || "").join("") : "");
}

/* dispatch a browser-direct completion by provider family */
async function callAIBrowser({ key, baseUrl, kind, model, system, messages, onToken }) {
  kind = kind || aiKindOf(baseUrl);
  if (kind === "anthropic") return callAnthropic({ apiKey: key, base: baseUrl, model, system, messages, onToken });
  return callOpenAI({ apiKey: key, baseUrl, model, system, messages, onToken });
}

/* ---------- NLU helpers ---------- */
function _iso(d) { return d.toISOString().slice(0, 10); }
function _findCities(text) {
  const lc = " " + text.toLowerCase() + " ";
  const hits = [];
  (window.AIRPORTS || []).forEach(a => {
    const needles = [a.city.toLowerCase(), a.ar, a.code.toLowerCase()];
    let best = -1;
    needles.forEach(n => { if (!n) return; const i = lc.indexOf(n.length <= 3 ? " " + n + " " : n); if (i >= 0 && (best < 0 || i < best)) best = i; });
    if (best >= 0) hits.push({ code: a.code, idx: best });
  });
  hits.sort((x, y) => x.idx - y.idx);
  // dedupe keep first
  const seen = new Set(), out = [];
  for (const h of hits) { if (!seen.has(h.code)) { seen.add(h.code); out.push(h); } }
  return out;
}
function _resolveDate(text, fallback) {
  const lc = text.toLowerCase();
  const today = new Date(); today.setHours(0, 0, 0, 0);
  if (/\btomorrow\b|غدًا|غدا|بكره|بكرة/.test(lc)) { const d = new Date(today); d.setDate(d.getDate() + 1); return _iso(d); }
  if (/\btoday\b|tonight|اليوم|الليلة/.test(lc)) return _iso(today);
  const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
  for (let i = 0; i < 7; i++) { if (lc.includes(days[i])) { const d = new Date(today); const delta = ((i - d.getDay()) + 7) % 7 || 7; d.setDate(d.getDate() + delta); return _iso(d); } }
  const md = lc.match(/(\d{4}-\d{2}-\d{2})/); if (md) return md[1];
  return fallback || null;
}
function _timeBand(text) {
  const lc = text.toLowerCase();
  if (/morning|صباح/.test(lc)) return [300, 720];
  if (/afternoon|ظهر|عصر/.test(lc)) return [720, 1020];
  if (/evening|مساء/.test(lc)) return [1020, 1320];
  if (/\bnight\b|tonight|ليل/.test(lc)) return [1140, 1440];
  return null;
}
function _airlineIn(text) {
  const lc = text.toLowerCase();
  for (const c of (window.CARRIERS || [])) { if (lc.includes(c.name.toLowerCase()) || (c.ar && text.includes(c.ar))) return c; }
  return null;
}
function _payLabel(store) {
  const c = (store.cards || []).find(x => x.primary) || (store.cards || [])[0];
  if (!c) return "saved card";
  const brand = { mada: "Mada", visa: "Visa", mc: "Mastercard", apple: "Apple Pay" }[c.brand] || c.brand;
  return brand + " ••" + c.last4;
}
function _nights(text, def) { const m = text.match(/(\d+)\s*(night|nights|ليلة|ليالٍ|ليالي)/); return m ? Math.max(1, +m[1]) : (def || 2); }

/* ---------- tier 3: deterministic local NLU ---------- */
function localWingReply({ text, store, lang }) {
  const t = (text || "").trim();
  const lc = t.toLowerCase();
  const ar = lang === "ar";
  const ctx = store.searchCtx || {};
  const say = (en, arr) => ar ? arr : en;

  // out of scope guard
  const travel = /flight|fly|plane|hotel|stay|book|cancel|change|move|trip|room|night|package|tour|airport|fare|طيران|رحلة|فندق|احجز|الغ|إلغاء|عدّل|غرفة|باقة|جولة|تذكرة/.test(lc) || _findCities(t).length > 0;

  // escalation (VVIP / protocol)
  if (/\bvvip\b|\bvip\b|royal|protocol|minister|delegation|discreet|discretion|head of state|private transfer|بروتوكول|ملكي|وزير|وفد|كبار|خاص/.test(lc)) {
    return {
      clean: say("This is a sensitive VVIP request — I'm escalating it to our Egypt Support Hub for white-glove handling.",
                 "هذا طلب حساس لكبار الشخصيات — أحوّله إلى مركز الدعم في مصر لمعاملة استثنائية."),
      card: { type: "escalate", reason: say("VVIP / protocol request routed to a human specialist at the Egypt Support Hub.", "طلب كبار شخصيات / بروتوكول مُحوّل إلى مختص بشري في مركز الدعم بمصر.") },
    };
  }

  // cancellation
  if (/\bcancel\b|الغ|إلغاء|الغاء/.test(lc)) {
    const target = (store.bookings || []).find(b => b.status === "Confirmed");
    if (target) {
      const refund = Math.round(target.amount * (target.type === "flight" ? 0.8 : 0.9));
      return {
        clean: say(`I found ${target.title} (${target.id}). Cancelling refunds about SAR ${refund.toLocaleString()} per supplier policy — confirm below.`,
                   `وجدت ${target.title} (${target.id}). الإلغاء يسترد نحو ${refund.toLocaleString()} ريال وفق سياسة المورّد — أكّد أدناه.`),
        card: { type: "cancel", title: target.title, refund, bookingId: target.id },
      };
    }
    return { clean: say("I couldn't find an active booking to cancel. Would you like me to look up a specific itinerary number?", "لم أجد حجزًا نشطًا للإلغاء. هل ترغب أن أبحث برقم رحلة محدد؟"), card: null };
  }

  // modify / change date — hotels & packages can be rescheduled in-chat; flight
  // changes need re-faring + reissue, so they route to a human advisor (matching
  // the customer Trips policy, which hides "reschedule" for flight bookings).
  if (/\b(change|move|modify|reschedule|push|shift)\b|عدّل|عدل|تغيير|أجّل|أجل/.test(lc) && /(hotel|trip|booking|flight|date|day|فندق|رحلة|حجز|تاريخ|يوم)/.test(lc)) {
    const wantsHotel = /(hotel|فندق)/.test(lc);
    const target = (store.bookings || []).find(b => b.status === "Confirmed" && b.type !== "flight" && (wantsHotel ? b.type === "hotel" : true));
    if (target) {
      const newDate = _resolveDate(t, null);
      return {
        clean: say(`I can move ${target.title} (${target.id})${newDate ? " to " + newDate : ""}. No change fee within policy — confirm and I'll update it.`,
                   `يمكنني نقل ${target.title} (${target.id})${newDate ? " إلى " + newDate : ""}. لا رسوم تغيير ضمن السياسة — أكّد وسأحدّثه.`),
        card: { type: "book", mode: "modify", title: say("Reschedule · ", "إعادة جدولة · ") + target.title, detail: newDate ? (say("New date ", "التاريخ الجديد ") + newDate) : say("New date to be set", "سيُحدَّد التاريخ"), price: 0, pay: say("No charge", "بدون رسوم"), bookingId: target.id, newDate },
      };
    }
    // only a flight is confirmed → can't self-reschedule a ticket; route to an advisor
    const flight = (store.bookings || []).find(b => b.status === "Confirmed" && b.type === "flight");
    if (flight) {
      return {
        clean: say(`Flight date changes need a quick re-fare and reissue, so I'm routing ${flight.title} (${flight.id}) to a travel advisor at our Egypt Support Hub — they'll confirm the new options with you shortly.`,
                   `تغيير تاريخ الرحلة يتطلّب إعادة تسعير وإصدار التذكرة، لذا أحوّل ${flight.title} (${flight.id}) إلى مستشار سفر في مركز الدعم بمصر — سيؤكّدون الخيارات الجديدة معك قريبًا.`),
        card: { type: "escalate", reason: say(`Flight reschedule for ${flight.id} routed to a human advisor for re-faring and reissue.`, `إعادة جدولة الرحلة ${flight.id} مُحوّلة إلى مستشار بشري لإعادة التسعير والإصدار.`) },
      };
    }
    return { clean: say("I couldn't find a hotel or package booking to reschedule. Flight changes go through an advisor — share an itinerary number and I'll route it.", "لم أجد حجز فندق أو باقة لإعادة جدولته. تغييرات الرحلات تتم عبر مستشار — أرسل رقم الرحلة وسأحوّله."), card: null };
  }

  // hotel search
  if (/hotel|stay|فندق|إقامة|اقامة|room|غرفة/.test(lc) || (/\d+\s*(night|nights|ليلة|ليال)/.test(lc) && _findCities(t).length)) {
    const cities = _findCities(t);
    const city = cities.length ? cities[0].code : (ctx.destination || ctx.to || "RUH");
    const nights = _nights(t, 2);
    const list = (window.generateHotels ? generateHotels(city, ctx.depart) : []);
    if (list.length) {
      const sorted = list.slice().sort((a, b) => b.rating - a.rating || a.corp - b.corp);
      const pick = sorted[0];
      const total = pick.corp * nights;
      const cName = window.cityName ? cityName(city, lang) : city;
      return {
        clean: say(`For ${cName}, the ${pick.name} (${pick.rating}★) is excellent — SAR ${pick.corp.toLocaleString()}/night corporate, about SAR ${total.toLocaleString()} for ${nights} nights. Shall I hold it on your ${_payLabel(store)}?`,
                   `في ${cName}، ${ar ? pick.ar : pick.name} (${pick.rating}★) خيار ممتاز — ${pick.corp.toLocaleString()} ريال/الليلة لأسعار الشركات، نحو ${total.toLocaleString()} ريال لـ${nights} ليالٍ. أحجزه على ${_payLabel(store)}؟`),
        card: { type: "book", mode: "hotel", title: (ar ? pick.ar : pick.name) + " · " + nights + (ar ? " ليالٍ" : " nights"), detail: (ar ? pick.areaAr : pick.area) + " · " + pick.rating + "★", price: total, pay: _payLabel(store), ref: { kind: "hotel", id: pick.id, city, nights } },
      };
    }
  }

  // flight search (default travel intent)
  if (travel && (/flight|fly|plane|fare|طيران|رحلة|تذكرة/.test(lc) || _findCities(t).length >= 1)) {
    const cities = _findCities(t);
    let from = ctx.from, to = ctx.to;
    // explicit "from X" / "to Y"
    const cityByName = (name) => { const a = (window.AIRPORTS || []).find(x => x.city.toLowerCase() === name || x.ar === name || x.code.toLowerCase() === name); return a ? a.code : null; };
    const mTo = lc.match(/\bto\s+([a-z]+)/) || t.match(/إلى\s+([؀-ۿ]+)/);
    const mFrom = lc.match(/\bfrom\s+([a-z]+)/) || t.match(/من\s+([؀-ۿ]+)/);
    if (cities.length >= 2) { from = cities[0].code; to = cities[1].code; }
    else if (cities.length === 1) { to = cities[0].code; if (from === to) from = (window.AIRPORTS || [])[0].code; }
    if (mFrom && cityByName(mFrom[1])) from = cityByName(mFrom[1]);
    if (mTo && cityByName(mTo[1])) to = cityByName(mTo[1]);
    if (from === to) { from = "RUH"; to = to === "RUH" ? "JED" : to; }

    const date = _resolveDate(t, ctx.depart);
    let list = (window.generateFlights ? generateFlights({ from, to, date }) : []);
    const airline = _airlineIn(t);
    const band = _timeBand(t);
    // graceful degradation: only keep a filter if it still yields results
    let filtered = list.slice();
    let usedAirline = false, usedBand = false;
    if (airline) { const a = filtered.filter(f => f.code === airline.code); if (a.length) { filtered = a; usedAirline = true; } }
    if (band) { const b = filtered.filter(f => f.depMin >= band[0] && f.depMin <= band[1]); if (b.length) { filtered = b; usedBand = true; } }
    filtered.sort((a, b) => (a.stops - b.stops) || (a.price - b.price));
    const pick = filtered[0];
    if (pick) {
      const fromN = window.cityName ? cityName(from, lang) : from, toN = window.cityName ? cityName(to, lang) : to;
      const aLabel = ar ? pick.airlineAr : pick.airline;
      const bandWordEn = usedBand ? (band[0] < 720 ? " morning" : band[1] > 1140 ? " evening" : " midday") : "";
      const bandWordAr = usedBand ? (band[0] < 720 ? " صباحًا" : band[1] > 1140 ? " مساءً" : " ظهرًا") : "";
      return {
        clean: say(`${filtered.length}${bandWordEn} option${filtered.length > 1 ? "s" : ""} ${fromN} → ${toN}${usedAirline ? " on " + airline.name : ""}. Best: ${aLabel} ${pick.id} ${pick.dep}–${pick.arr}, ${pick.cabin}, ${pick.stops ? pick.stops + "-stop" : "non-stop"} at SAR ${pick.price.toLocaleString()}. Hold it on your ${_payLabel(store)}?`,
                   `${filtered.length} خيار${bandWordAr} ${fromN} ← ${toN}${usedAirline ? " على " + airline.ar : ""}. الأفضل: ${aLabel} ${pick.id} ${pick.dep}–${pick.arr}، ${pick.cabin === "Business" ? "رجال الأعمال" : "الاقتصادية"}، ${pick.stops ? "بتوقف" : "مباشرة"} بسعر ${pick.price.toLocaleString()} ريال. أحجزها على ${_payLabel(store)}؟`),
        card: { type: "book", mode: "flight", title: `${from} → ${to} · ${aLabel} ${pick.id}`, detail: `${pick.dep}–${pick.arr} · ${pick.cabin === "Business" ? say("Business", "رجال الأعمال") : say("Economy", "الاقتصادية")} · ${pick.stops ? say(pick.stops + "-stop", "بتوقف") : say("non-stop", "مباشرة")}`, price: pick.price, pay: _payLabel(store), ref: { kind: "flight", id: pick.id, from, to, date } },
      };
    }
  }

  if (!travel) {
    return { clean: say("I'm Mr. Wing — I look after flights, hotels and tour packages. Tell me where you'd like to go and when, and I'll arrange it.", "أنا مستر وينغ — أعتني بالطيران والفنادق والباقات السياحية. أخبرني بوجهتك وموعدها وسأرتّبها لك."), card: null };
  }
  // travel intent but nothing matched → ask
  return { clean: say("Happy to help. Which cities and dates are we looking at?", "بكل سرور. ما المدن والتواريخ التي ننظر فيها؟"), card: null };
}

/* ---------- dispatcher ---------- */
async function runWing({ store, lang, messages, onToken }) {
  const aiKey = (store.apiKeys || []).find(k => k.id === "ai");
  const secret = aiKey && aiKey.value ? aiKey.value : "";
  const system = buildWingSystem(store, lang);
  const apiMessages = messages.filter(m => m.role === "user" || m.role === "assistant").map(m => ({ role: m.role, content: m.content || "" }));
  const lastUser = [...messages].reverse().find(m => m.role === "user");
  const aiModel = aiKey && aiKey.model;
  const aiBase = aiKey && aiKey.baseUrl;
  const aiKind = (aiKey && aiKey.kind) || aiKindOf(aiBase);

  // tier 0 — secure server proxy (/api/wing); hides the key, used on the live deployment
  if (window.apiWing) {
    try {
      const r = await window.apiWing({ system, messages: apiMessages, model: aiModel });
      if (r && !r.__unreachable && r.configured && r.text) {
        const { clean, card } = parseWingAction(r.text || "");
        return { clean: clean || "…", card, source: "anthropic" };
      }
      if (r && r.configured && r.error) window.__wingLastError = r.error + (r.detail ? ": " + r.detail : "");
    } catch (e) { window.__wingLastError = String(e && e.message || e); }
  }

  // tier 1 — browser-direct provider (key stored client-side / static mode, CORS permitting)
  if (secret && aiModel) {
    try {
      const raw = await callAIBrowser({ key: secret, baseUrl: aiBase, kind: aiKind, model: aiModel, system, messages: apiMessages, onToken });
      const { clean, card } = parseWingAction(raw || "");
      return { clean: clean || "…", card, source: aiKind === "anthropic" ? "anthropic" : "openai" };
    } catch (e) {
      // fall through to next tiers; remember the error for diagnostics
      window.__wingLastError = String(e && e.message || e);
    }
  }
  // tier 2 — artifact runtime
  if (window.claude && typeof window.claude.complete === "function") {
    try {
      const raw = await window.claude.complete({ system, messages: apiMessages });
      const { clean, card } = parseWingAction(raw || "");
      return { clean: clean || "…", card, source: "claude" };
    } catch (e) {
      window.__wingLastError = String(e && e.message || e);
    }
  }
  // tier 3 — deterministic local NLU (always works)
  const out = localWingReply({ text: lastUser ? lastUser.content : "", store, lang });
  return { clean: out.clean, card: out.card, source: "local" };
}

Object.assign(window, { buildWingSystem, parseWingAction, callAnthropic, callOpenAI, callAIBrowser, fetchAIModels, aiKindOf, aiNormBase, aiNormalizeModels, localWingReply, runWing });
