const { useState, useCallback, useRef } = React;

// Session auth token (set after login, used for all API calls)
var authToken = null;

// ─────────────────────────────────────────────────────────────────────────────
// PROMPTS
// ─────────────────────────────────────────────────────────────────────────────

const STRUCTURE_PROMPT = `You are a hotel contract extraction assistant. Extract the structural foundation of a hotel contract and return a single JSON object with exactly ONE top-level key: "system_structure". No markdown, no explanation, no code fences. Raw JSON only.

DO NOT extract price rules, discounts, supplements or offers — those are handled separately.

WHAT TO EXTRACT:

system_structure:
- hotel.name: string
- hotel.currency: ISO currency code (e.g. "EUR")
- hotel.age_categories: age bands with id, label, age_from, age_to. Possible ids: baby, child, adolescent, teenager, adult, elderly. Always include adult. CRITICAL: adult age_to must ALWAYS be null — never use 99, 999 or any invented ceiling. If the contract does not state an upper age limit for adults, set age_to to null.
- hotel.boarding_types: all board types. Included boards: {"id":"BB","label":"...","included":true,"room_types":"all"}. Supplement boards MUST include price_rules with the supplement amount — e.g. {"id":"HB","label":"Half Board","included":false,"room_types":"all","price_rules":[{"trigger_condition":"if","trigger_value":null,"modifier_type":"flat_per_person_per_night","modifier_value":30,"currency":"EUR"}]}. NEVER leave price_rules as an empty array for a supplement board — always extract the amount from the contract.
- hotel.occupancy_combinations: array of {"id":"occ_2a","description":"2 adults"}. ID format: occ_{N}{type}. Abbreviations: b=baby,c=child,ado=adolescent,t=teenager,a=adult,e=elderly. IMPORTANT: if the contract mentions babies or infants as valid room occupants (e.g. "baby counts as 1 pax", "free baby cot on request"), include ALL baby-inclusive combinations that are physically valid — e.g. "2 adults + 1 baby", "1 adult + 1 baby". Also include these in each room's possible_occupancy_rules.
- contract_validity: {"start":"YYYY-MM-DD","end":"YYYY-MM-DD"} — the contract validity period, not booking window.
- payment_terms: {"type":"credit" or "prepay","deadline":"e.g. 30 days from invoice date","commission_pct":0}
- cancellation_policy: {"individual":[{"days_from":N,"days_to":N,"charge_pct":N,"charge_nights":N,"is_no_show":false}],"groups":[...]}. Use null for unused field (charge_pct or charge_nights, not both). Set is_no_show:true for the no-show row.
- room_types: all room types (see below)
- base_prices: compact grid (see below)
- _review_required: flags array — always present, empty if no issues

ROOM TYPE OBJECT:
code, name, size_m2, default_capacity, max_capacity,
allotment: number (e.g. 10) or "OR" if on request,
min_stay: number of nights or null,
release: number of days or null,
deviating_capacities: array of {label, occupancies[]} — ONLY for occupancy configurations with their own separately contracted rate in the rate table (e.g. single_use). Empty [] if none. CRITICAL: the label you use here (e.g. "single_use") must exactly match the key you use in base_prices.rates for that room. Use the same label consistently across all rooms — do not use "single" for some rooms and "single_use" for others.
price_type: "per_person_per_night" or "per_unit_per_night",
default_boarding, possible_boarding_types (omit if room supports all hotel boarding types),
possible_occupancy_rules: array of occupancy combination ids valid for this room — in plain human-readable form e.g. "2 adults + 1 child"

BASE PRICES (compact grid):
{"currency":"EUR","seasons":[["YYYY-MM-DD","YYYY-MM-DD"],...],"rates":{"ROOM_CODE":{"default":[p1,p2,...],"single_use":[p1,p2,...]}},"on_request":["ROOM_CODE"]}
Rate labels must match deviating_capacities labels plus "default".

REVIEW FLAG OBJECT:
id (flag_001...), severity ("blocker" or "warning"), path (exact dot-notation), current_value (smallest meaningful value), message, suggested_value

EXTRACTION RULES:
1. default_capacity = base occupancy rate is built on (usually 2).
2. deviating_capacities = contracted rate columns only. NOT general occupancy combinations.
3. possible_occupancy_rules = plain human-readable strings e.g. ["1 adult","2 adults","2 adults + 1 child"].
4. Only extract what contract states — no assumptions.
5. Raise blocker: room with no rates omitted, invented room code.
6. Raise warning: non-standard default_capacity, inferred age boundary, ambiguous occupancy.`;

const PRICE_RULES_PROMPT = `You are a hotel contract pricing specialist. Read this contract and describe every pricing rule in plain English. One description per rule.

Do NOT format rules into a structured JSON schema. Do NOT invent field names or trigger types. Just describe what the contract says clearly and completely.

CONTEXT PROVIDED:
You will be given a context object with: hotel name, currency, room_codes, boarding_ids, age_categories, capacity_map, occupancy_combinations.
Use these exact values when referencing rooms, occupancies and boards in descriptions.

OUTPUT FORMAT:
Return minified JSON with ONE key "price_rules":
{"hotel":"...","currency":"...","price_rules":[{"id":"rule_001","type":"discount","label":"Short label","description":"Plain English description","applies_to_room_codes":"all","combinable":true,"note":null}]}

TYPE VALUES — pick the closest:
- "discount" — percentage or amount reduction (child discount, extra pax, repeater)
- "early_booking" — booking-window triggered discount (EBD, rolling EB)
- "offer" — free nights, group discount, long stay
- "supplement" — optional board or service addition
- "compulsory" — mandatory charge that cannot be waived (gala dinner, cleaning fee)

HOW TO WRITE DESCRIPTIONS — each must answer:
1. What: the discount/supplement/offer (amount and type)
2. Who: which guests, age category, occupancy or room type
3. When: booking window, stay dates, minimum nights if applicable
4. How: per person, per room, per stay, per night

EXAMPLES:
- "30% discount on the base price per person for any guest beyond the room default capacity, applies to all rooms. Not applicable to gala dinners."
- "50% discount on the base price for the 1st child aged 2-12 when 2 adults are present, applies to all rooms. Discount also applies to board supplements."
- "25% discount on the base price when booked between 01 Jul 2024 and 31 Jan 2025 for stays between 01 May 2025 and 31 Oct 2025, minimum 5 nights, applies to all rooms, not combinable with other offers"
- "5% discount on the base price for stays of 21 nights or more, applies to all rooms"
- "Mandatory final cleaning fee of NOK 400 per stay for TYPE_A rooms, paid at arrival"

SPLIT INTO SEPARATE RULES WHEN:
- Different room types have different amounts
- Different stay periods have different free night ratios (7=5 vs 7=6)
- Different booking windows apply to different stay periods
- Different child positions have different discounts (1st vs 2nd child)
- Single parent occupancy is separate from regular child discount

DO NOT split when the rule is identical across all rooms or seasons.
DO NOT describe boarding type supplement amounts — those are already in the structure.
DO NOT include rules that are purely informational (taxes included, minimum stay, release days).`;

const VALIDATOR_PROMPT = `You are a hotel extraction validator. Receive a JSON envelope with system_structure and price_rules. Validate, normalise and fix it. Return corrected minified JSON only. No markdown. No explanation.

AUTO-FIX SILENTLY:
- price_type "per_villa_per_night" -> "per_unit_per_night"
- occupancy_combinations as plain strings -> convert to {id, description} objects
- applicable_occupancies field name -> rename to possible_occupancy_rules
- deviating_capacities old format {label,adults,children} -> {label,occupancies:[]}
- deviating_capacities entries that are NOT separately contracted rates -> remove
- Missing room_types on boarding_type -> default to "all"
- String prices in base_prices.rates -> convert to numbers
- hotel.currency must exist — if missing, copy from base_prices.currency
- base_prices.currency must equal hotel.currency — auto-fix if they differ
- price_rules.currency must equal hotel.currency — auto-fix if they differ
- adult age category must always be present — add if missing
- adult age_to set to any number (99, 999, 100, etc.) -> set to null. Adult has no upper age limit.
- possible_occupancy_rules: if stored as occupancy ids (occ_2a), convert to plain human-readable strings ("2 adults")
- deviating_capacities label inconsistency: if some rooms use "single" and others use "single_use" for the same concept, normalise all to "single_use". Apply the same normalisation to matching keys in base_prices.rates.
- boarding_type with included:false and empty price_rules [] -> raise a warning flag, cannot auto-fix (amount missing from extraction)

RAISE BLOCKER FLAGS FOR:
- Missing age boundary that is referenced by a price rule
- base_prices not in compact grid format
- Price rule referencing a room code not in room_types

RAISE WARNING FLAGS FOR:
- default_capacity set to non-standard value
- One-sided early booking window
- Inferred age boundary
- Very low allotment (1 room)
- allotment missing for a room type
- boarding_type with included:false and empty or missing price_rules (supplement amount was not extracted)

The price_rules field contains plain text descriptions — do NOT attempt to normalise trigger_value formats or modifier_types in price_rules. Only validate and fix system_structure fields.

Return: {"system_structure":{...},"price_rules":{...}}`;

// ─────────────────────────────────────────────────────────────────────────────
// UTILITIES
// ─────────────────────────────────────────────────────────────────────────────

function expandBasePrices(base_prices) {
  if (!base_prices) return [];
  if (Array.isArray(base_prices)) return base_prices;
  const { currency, seasons = [], rates = {}, on_request = [] } = base_prices;
  const rows = [];
  Object.entries(rates).forEach(([room_code, labels]) => {
    const isOR = on_request.includes(room_code);
    Object.entries(labels).forEach(([capacity_label, prices]) => {
      seasons.forEach(([start_date, end_date], i) => {
        const price = Array.isArray(prices) ? prices[i] : null;
        rows.push({ room_code, capacity_label, start_date, end_date, price: price ?? null, currency, note: isOR ? "On Request" : (price === null ? "no price" : null) });
      });
    });
  });
  return rows;
}

function extractFirstJSON(raw) {
  const start = raw.indexOf("{");
  if (start === -1) return null;
  let depth = 0, inStr = false, esc = false;
  for (let i = start; i < raw.length; i++) {
    const ch = raw[i];
    if (esc) { esc = false; continue; }
    if (ch === "\\" && inStr) { esc = true; continue; }
    if (ch === '"') { inStr = !inStr; continue; }
    if (inStr) continue;
    if (ch === "{") depth++;
    else if (ch === "}") { depth--; if (depth === 0) return raw.slice(start, i + 1); }
  }
  return null;
}

function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result.split(",")[1]);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

async function extractExcelText(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const data = new Uint8Array(e.target.result);
        const workbook = XLSX.read(data, { type: "array" });
        let text = "";
        workbook.SheetNames.forEach(name => {
          const sheet = workbook.Sheets[name];
          text += `\n=== Sheet: ${name} ===\n`;
          text += XLSX.utils.sheet_to_csv(sheet);
        });
        resolve(text);
      } catch (err) { reject(err); }
    };
    reader.onerror = reject;
    reader.readAsArrayBuffer(file);
  });
}

// ─────────────────────────────────────────────────────────────────────────────
// API
// ─────────────────────────────────────────────────────────────────────────────

async function fetchWithRetry(url, options, onPhase, maxRetries = 3) {
  if (authToken && options && options.headers) {
    options.headers["Authorization"] = "Bearer " + authToken;
  }
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);
    if (response.ok) return response;
    if (response.status === 429 && attempt < maxRetries) {
      const retryAfter = parseInt(response.headers.get("retry-after") || "0", 10);
      const wait = Math.max((retryAfter || 30) * 1000, 15000 * (attempt + 1));
      onPhase(`Rate limited — retrying in ${Math.round(wait / 1000)}s…`);
      await new Promise(r => setTimeout(r, wait));
      continue;
    }
    const err = await response.json().catch(() => ({}));
    throw new Error(err.error?.message || `API error ${response.status}`);
  }
  throw new Error(`Failed after ${maxRetries + 1} attempts`);
}

async function streamCall(base64PDF, userPrompt, onChunk, signal, systemOverride = null) {
  const systemText = (systemOverride || STRUCTURE_PROMPT) + "\n\nCRITICAL: Your response must begin with { and end with }. Never use markdown code fences. Output raw JSON only.";
  const body = {
    model: "claude-sonnet-4-6", max_tokens: 16000, stream: true,
    system: [{ type: "text", text: systemText, cache_control: { type: "ephemeral" } }],
    messages: [{ role: "user", content: [
      { type: "document", source: { type: "base64", media_type: "application/pdf", data: base64PDF }, cache_control: { type: "ephemeral" } },
      { type: "text", text: userPrompt }
    ]}]
  };
  const response = await fetchWithRetry("/api/anthropic",
    { method: "POST", signal, headers: { "Content-Type": "application/json", "anthropic-beta": "prompt-caching-2024-07-31" }, body: JSON.stringify(body) }, () => {});
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let fullText = "", buf = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });
    const parts = buf.split("\n"); buf = parts.pop();
    for (const line of parts) {
      if (!line.startsWith("data: ")) continue;
      const d = line.slice(6).trim();
      if (d === "[DONE]") continue;
      try { const evt = JSON.parse(d); if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta") { fullText += evt.delta.text; onChunk(fullText.replace(/^```json?\s*/i, "").replace(/\s*```\s*$/i, "")); } } catch {}
    }
  }
  const clean = fullText.replace(/^```json?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
  const jsonStr = extractFirstJSON(clean);
  if (!jsonStr) throw new Error(`No JSON found in response (${clean.length} chars)`);
  try { return JSON.parse(jsonStr); } catch (e) { throw new Error(`JSON parse failed: ${e.message}`); }
}

async function streamCallText(contractText, userPrompt, onChunk, signal, systemPrompt = null) {
  const resolvedSystem = (systemPrompt || STRUCTURE_PROMPT) + "\n\nCRITICAL: Your response must begin with { and end with }. Never use markdown code fences. Output raw JSON only.";
  const body = {
    model: "claude-sonnet-4-6", max_tokens: 8000, stream: true,
    system: resolvedSystem,
    messages: [{ role: "user", content: [{ type: "text", text: `CONTRACT DATA:\n\n${contractText}\n\n${userPrompt}` }] }]
  };
  const response = await fetchWithRetry("/api/anthropic",
    { method: "POST", signal, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }, () => {});
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let text = "", buf = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });
    const parts = buf.split("\n"); buf = parts.pop();
    for (const line of parts) {
      if (!line.startsWith("data: ")) continue;
      const d = line.slice(6).trim();
      if (d === "[DONE]") continue;
      try { const evt = JSON.parse(d); if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta") { text += evt.delta.text; onChunk(text.replace(/^```json?\s*/i, "").replace(/\s*```\s*$/i, "")); } } catch {}
    }
  }
  const clean = text.replace(/^```json?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
  const s = clean.indexOf("{"), e = clean.lastIndexOf("}");
  if (s === -1 || e === -1) throw new Error(`No JSON found in response`);
  try { return JSON.parse(clean.slice(s, e + 1)); } catch (e) { throw new Error(`JSON parse failed: ${e.message}`); }
}

async function extractContract(base64PDF, onChunk, onPhase, signal) {
  const CALL_DELAY = 5000;
  // ── CALL 1: structure ────────────────────────────────────────────────
  onPhase("Reading hotel structure and rooms…");
  const call1 = await streamCall(base64PDF,
    `Extract the hotel structure from this contract. Return minified JSON with ONE key "system_structure". Set base_prices to empty array []. Do NOT include price_rules. Start with {.`,
    onChunk, signal);

  // ── CALL 2: price rules ──────────────────────────────────────────────
  await new Promise(r => setTimeout(r, CALL_DELAY));
  onPhase("Extracting price rules…");
  const call2Context = buildCall2Context(call1);
  const call2Body = {
    model: "claude-sonnet-4-6", max_tokens: 8000, stream: true,
    system: PRICE_RULES_PROMPT + "\n\nCRITICAL: Return raw JSON only. No markdown. Response must start with {.",
    messages: [{ role: "user", content: [
      { type: "document", source: { type: "base64", media_type: "application/pdf", data: base64PDF }, cache_control: { type: "ephemeral" } },
      { type: "text", text: `Context:\n${JSON.stringify(call2Context, null, 2)}\n\nExtract all pricing rules as plain English descriptions. Return {"price_rules":[...]}` }
    ]}]
  };
  const call2Response = await fetchWithRetry("/api/anthropic",
    { method: "POST", signal, headers: { "Content-Type": "application/json", "anthropic-beta": "prompt-caching-2024-07-31" }, body: JSON.stringify(call2Body) }, onPhase);
  const call2 = await readStream(call2Response, onChunk);
  const call2Parsed = parseStreamResult(call2);
  const prData = normaliseRules(call2Parsed, call2Context);

  // ── CALL 3: base prices ──────────────────────────────────────────────
  await new Promise(r => setTimeout(r, CALL_DELAY));
  onPhase("Extracting base prices…");
  const roomCodes = (call1.system_structure?.room_types || []).map(r => r.code);
  const currency = call1.system_structure?.hotel?.currency || "EUR";
  const call3Body = {
    model: "claude-sonnet-4-6", max_tokens: 8000, stream: true,
    system: "You extract price tables from hotel contracts. Return minified JSON only. Response must start with {.",
    messages: [{ role: "user", content: [
      { type: "document", source: { type: "base64", media_type: "application/pdf", data: base64PDF }, cache_control: { type: "ephemeral" } },
      { type: "text", text: `Extract the rate table as a compact grid.\nRoom codes: ${roomCodes.join(", ")}\nCurrency: ${currency}\nReturn: {"base_prices":{"currency":"${currency}","seasons":[["YYYY-MM-DD","YYYY-MM-DD"],...],"rates":{"ROOM_CODE":{"default":[p1,p2,...]}},"on_request":[]}}\nUse null for unknown prices. Minified JSON only.` }
    ]}]
  };
  const call3Response = await fetchWithRetry("/api/anthropic",
    { method: "POST", signal, headers: { "Content-Type": "application/json", "anthropic-beta": "prompt-caching-2024-07-31" }, body: JSON.stringify(call3Body) }, onPhase);
  const call3 = await readStream(call3Response, onChunk);
  const call3Parsed = parseStreamResult(call3);
  call1.system_structure.base_prices = call3Parsed.base_prices || [];

  // ── CALL 4: validation ───────────────────────────────────────────────
  await new Promise(r => setTimeout(r, CALL_DELAY));
  onPhase("Validating and normalising…");
  const combined = { system_structure: call1.system_structure, price_rules: prData };
  const call4Body = {
    model: "claude-sonnet-4-6", max_tokens: 8000, stream: true,
    system: VALIDATOR_PROMPT + "\n\nCRITICAL: Response must begin with { and end with }. No markdown.",
    messages: [{ role: "user", content: [{ type: "text", text: `Validate and fix this hotel extraction:\n\n${JSON.stringify(combined)}` }] }]
  };
  const call4Response = await fetchWithRetry("/api/anthropic",
    { method: "POST", signal, headers: { "Content-Type": "application/json" }, body: JSON.stringify(call4Body) }, onPhase);
  const call4 = await readStream(call4Response, onChunk);
  const call4Parsed = (() => { try { const j = extractFirstJSON(call4.replace(/^```json?\s*/i,"").replace(/\s*```\s*$/i,"").trim()); return j ? JSON.parse(j) : combined; } catch { return combined; } })();
  return { system_structure: call4Parsed.system_structure || combined.system_structure, price_rules: call4Parsed.price_rules || combined.price_rules };
}

async function extractContractFromText(contractText, onChunk, onPhase, signal) {
  const CALL_DELAY = 5000;
  onPhase("Reading hotel structure and rooms…");
  const call1 = await streamCallText(contractText,
    `Extract from this hotel contract. Return minified JSON with ONE key "system_structure". Set base_prices to empty array []. Start with {.`,
    onChunk, signal);

  await new Promise(r => setTimeout(r, CALL_DELAY));
  onPhase("Extracting price rules…");
  const call2Context = buildCall2Context(call1);
  const call2 = await streamCallText(contractText,
    `Context:\n${JSON.stringify(call2Context, null, 2)}\n\nExtract all pricing rules as plain English descriptions. Return {"price_rules":[...]}`,
    onChunk, signal, PRICE_RULES_PROMPT);
  const prData = normaliseRules(call2, call2Context);

  await new Promise(r => setTimeout(r, CALL_DELAY));
  onPhase("Extracting base prices…");
  const roomCodes = (call1.system_structure?.room_types || []).map(r => r.code);
  const currency = call1.system_structure?.hotel?.currency || "EUR";
  const call3 = await streamCallText(contractText,
    `Extract the rate table as a compact grid.\nRoom codes: ${roomCodes.join(", ")}\nCurrency: ${currency}\nReturn: {"base_prices":{"currency":"${currency}","seasons":[["YYYY-MM-DD","YYYY-MM-DD"],...],"rates":{"ROOM_CODE":{"default":[p1,p2,...]}},"on_request":[]}}`,
    onChunk, signal);
  call1.system_structure.base_prices = call3.base_prices || [];

  await new Promise(r => setTimeout(r, CALL_DELAY));
  onPhase("Validating and normalising…");
  const combined = { system_structure: call1.system_structure, price_rules: prData };
  const call4Body = {
    model: "claude-sonnet-4-6", max_tokens: 8000, stream: true,
    system: VALIDATOR_PROMPT + "\n\nCRITICAL: Response must begin with { and end with }. No markdown.",
    messages: [{ role: "user", content: [{ type: "text", text: `Validate and fix this:\n\n${JSON.stringify(combined)}` }] }]
  };
  const call4Response = await fetchWithRetry("/api/anthropic",
    { method: "POST", signal, headers: { "Content-Type": "application/json" }, body: JSON.stringify(call4Body) }, onPhase);
  const call4 = await readStream(call4Response, onChunk);
  const call4Parsed = (() => { try { const j = extractFirstJSON(call4.replace(/^```json?\s*/i,"").replace(/\s*```\s*$/i,"").trim()); return j ? JSON.parse(j) : combined; } catch { return combined; } })();
  return { system_structure: call4Parsed.system_structure || combined.system_structure, price_rules: call4Parsed.price_rules || combined.price_rules };
}

function buildCall2Context(call1) {
  return {
    hotel: call1.system_structure?.hotel?.name || "",
    currency: call1.system_structure?.hotel?.currency || "EUR",
    room_codes: (call1.system_structure?.room_types || []).map(r => r.code),
    boarding_ids: (call1.system_structure?.hotel?.boarding_types || []).map(b => b.id),
    age_categories: (call1.system_structure?.hotel?.age_categories || []).map(a => ({ id: a.id, age_from: a.age_from, age_to: a.age_to })),
    capacity_map: Object.fromEntries((call1.system_structure?.room_types || []).map(r => [r.code, r.default_capacity])),
    occupancy_combinations: (call1.system_structure?.hotel?.occupancy_combinations || [])
  };
}

async function readStream(response, onChunk) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let text = "", buf = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });
    const parts = buf.split("\n"); buf = parts.pop();
    for (const line of parts) {
      if (!line.startsWith("data: ")) continue;
      const d = line.slice(6).trim();
      if (d === "[DONE]") continue;
      try { const evt = JSON.parse(d); if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta") { text += evt.delta.text; onChunk(text.replace(/^```json?\s*/i,"").replace(/\s*```\s*$/i,"")); } } catch {}
    }
  }
  return text;
}

function parseStreamResult(text) {
  const clean = text.replace(/^```json?\s*/i,"").replace(/\s*```\s*$/i,"").trim();
  const j = extractFirstJSON(clean);
  if (!j) return {};
  try { return JSON.parse(j); } catch { return {}; }
}

function normaliseRules(parsed, ctx) {
  let prData = parsed.price_rules || parsed;
  if (Array.isArray(prData)) prData = { hotel: ctx.hotel, currency: ctx.currency, price_rules: prData };
  else if (Array.isArray(prData.price_rules)) { /* already good */ }
  else prData = { hotel: ctx.hotel, currency: ctx.currency, price_rules: [] };
  return prData;
}

// ─────────────────────────────────────────────────────────────────────────────
// UI — SHARED
// ─────────────────────────────────────────────────────────────────────────────

function Pill({ children, color = "gray" }) {
  const colors = {
    gray: "bg-gray-100 text-gray-500",
    green: "bg-green-50 text-green-700 border border-green-100",
    amber: "bg-amber-50 text-amber-700 border border-amber-100",
    red: "bg-red-50 text-red-700 border border-red-100",
    blue: "bg-blue-50 text-blue-700 border border-blue-100",
    purple: "bg-purple-50 text-purple-700 border border-purple-100",
    or: "bg-amber-50 text-amber-600 border border-amber-100",
  };
  return <span className={`inline-block text-xs font-medium px-2 py-0.5 rounded-full ${colors[color] || colors.gray}`}>{children}</span>;
}

function SectionLabel({ children }) {
  return <div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">{children}</div>;
}

function InlineCard({ children, className = "" }) {
  return <div className={`bg-white border border-gray-100 rounded-xl p-4 ${className}`}>{children}</div>;
}

function OccupancyList({ occupancies }) {
  const [open, setOpen] = useState(false);
  if (!occupancies || occupancies.length === 0) return null;
  return (
    <div>
      <button onClick={() => setOpen(o => !o)}
        className="flex items-center justify-between w-full mt-3 pt-3 border-t border-gray-50 text-xs text-gray-400 hover:text-gray-600 transition-colors">
        <span>{open ? "Hide" : `Occupancies (${occupancies.length})`}</span>
        <span style={{ fontSize: 9 }}>{open ? "▲" : "▼"}</span>
      </button>
      {open && (
        <div className="mt-2 flex flex-col gap-1">
          {occupancies.map((occ, i) => (
            <span key={i} className="text-xs bg-gray-50 text-gray-500 px-2 py-1 rounded-lg">{occ}</span>
          ))}
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// UI — HOTEL INFO PANEL
// ─────────────────────────────────────────────────────────────────────────────

function HotelInfoPanel({ ss, onUpdateSS }) {
  const [subTab, setSubTab] = useState("overview");
  const [priceRoomFilter, setPriceRoomFilter] = useState("all");
  const flags = Array.isArray(ss?._review_required) ? ss._review_required : [];
  const blockers = flags.filter(f => f.severity === "blocker").length;
  const warnings = flags.filter(f => f.severity === "warning").length;

  const SUB_TABS = [
    { id: "overview", label: "Overview" },
    { id: "rooms", label: `Rooms (${ss?.room_types?.length || 0})` },
    { id: "prices", label: "Base prices" },
    { id: "flags", label: `Flags${blockers > 0 ? ` · ${blockers} blocker${blockers > 1 ? "s" : ""}` : warnings > 0 ? ` · ${warnings}` : " ✓"}` },
  ];

  return (
    <div>
      <div className="flex gap-1 mb-5 border-b border-gray-100">
        {SUB_TABS.map(t => (
          <button key={t.id} onClick={() => setSubTab(t.id)}
            className={`px-3 py-2.5 text-xs font-medium border-b-2 -mb-px transition-colors ${subTab === t.id ? "border-blue-500 text-blue-600" : "border-transparent text-gray-400 hover:text-gray-600"}`}>
            {t.label}
          </button>
        ))}
      </div>

      {/* OVERVIEW */}
      {subTab === "overview" && (
        <div className="space-y-5">
          {/* Age categories */}
          <div>
            <SectionLabel>Age categories</SectionLabel>
            <InlineCard className="p-0 overflow-hidden">
              <table className="w-full text-sm">
                <thead>
                  <tr className="border-b border-gray-50">
                    <th className="text-left px-4 py-2.5 text-xs font-semibold text-gray-400">Category</th>
                    <th className="text-left px-4 py-2.5 text-xs font-semibold text-gray-400">Age range</th>
                    <th className="text-right px-4 py-2.5 text-xs font-semibold text-gray-400">Notes</th>
                  </tr>
                </thead>
                <tbody>
                  {(ss?.hotel?.age_categories || []).map((cat, i) => {
                    const bandColor = { baby: "bg-teal-50 text-teal-700", child: "bg-blue-50 text-blue-700", adolescent: "bg-purple-50 text-purple-700", adult: "bg-gray-100 text-gray-600", elderly: "bg-gray-100 text-gray-600" };
                    return (
                      <tr key={i} className="border-b border-gray-50 last:border-0">
                        <td className="px-4 py-2.5"><span className={`text-xs font-medium px-2 py-0.5 rounded-full ${bandColor[cat.id] || "bg-gray-100 text-gray-600"}`}>{cat.label}</span></td>
                        <td className="px-4 py-2.5 text-xs text-gray-600">{cat.age_from ?? "?"} – {cat.age_to ?? "+"}</td>
                        <td className="px-4 py-2.5 text-right text-xs text-gray-400">
                          {cat.age_to === null ? "No upper limit" : ""}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </InlineCard>
          </div>

          {/* Boarding types */}
          <div>
            <SectionLabel>Boarding types</SectionLabel>
            <InlineCard className="p-0 divide-y divide-gray-50">
              {(ss?.hotel?.boarding_types || []).map((bt, i) => {
                const isIncluded = bt.included === true;
                const supplement = bt.price_rules?.[0]?.modifier_value;
                const currency = ss?.hotel?.currency || "";
                return (
                  <div key={i} className="flex items-center justify-between px-4 py-3">
                    <div>
                      <div className="text-sm font-medium text-gray-800">{bt.label} <span className="font-mono text-xs text-gray-400">({bt.id})</span></div>
                      <div className="text-xs text-gray-400 mt-0.5">
                        {bt.room_types === "all" ? "All room types" : Array.isArray(bt.room_types) ? bt.room_types.join(", ") : ""}
                        {supplement !== undefined && ` · ${currency} ${supplement} per person per night`}
                      </div>
                    </div>
                    <Pill color={isIncluded ? "green" : "amber"}>{isIncluded ? "Included" : "Supplement"}</Pill>
                  </div>
                );
              })}
            </InlineCard>
          </div>

          {/* Contract conditions */}
          {(() => {
            const rooms = ss?.room_types || [];
            const minStays = [...new Set(rooms.map(r => r.min_stay).filter(v => v != null))];
            const releases = [...new Set(rooms.map(r => r.release).filter(v => v != null))];
            const minStayLabel = minStays.length === 1 ? `${minStays[0]} nights (all rooms)` : minStays.length > 1 ? `${minStays.join(" / ")} nights` : null;
            const releaseLabel = releases.length === 1 ? `${releases[0]} days (all rooms)` : releases.length > 1 ? `${releases.join(" / ")} days` : null;
            const cancRows = ss?.cancellation_policy?.individual || [];
            const hasCond = minStayLabel || releaseLabel || cancRows.length > 0;
            if (!hasCond) return null;

            const formatCancCharge = (row) => {
              if (!row) return "—";
              if (row.charge_nights) return `${row.charge_nights} night${row.charge_nights > 1 ? "s" : ""} charge`;
              if (row.charge_pct) return `${row.charge_pct}%`;
              return "—";
            };
            const formatCancLabel = (row) => {
              if (row.is_no_show) return "No show";
              if (row.days_to === null) return `Cancellation >${row.days_from} days`;
              if (row.days_from === row.days_to) return `Cancellation ${row.days_from} days`;
              return `Cancellation ${row.days_to}–${row.days_from} days`;
            };

            const condPairs = [];
            if (minStayLabel) condPairs.push({ label: "Min. stay", value: minStayLabel });
            if (releaseLabel) condPairs.push({ label: "Release period", value: releaseLabel });
            cancRows.forEach(row => condPairs.push({ label: formatCancLabel(row), value: formatCancCharge(row) }));
            if (condPairs.length % 2 !== 0) condPairs.push({ label: "", value: "" });

            const pairs = [];
            for (let i = 0; i < condPairs.length; i += 2) pairs.push([condPairs[i], condPairs[i + 1]]);

            return (
              <div>
                <SectionLabel>Contract conditions</SectionLabel>
                <InlineCard>
                  <div className="grid gap-y-3">
                    {pairs.map(([left, right], i) => (
                      <div key={i} className="grid grid-cols-2 gap-x-8">
                        <div>
                          <div className="text-xs text-gray-400 mb-0.5">{left.label}</div>
                          <div className="text-sm font-medium text-gray-800">{left.value}</div>
                        </div>
                        <div>
                          <div className="text-xs text-gray-400 mb-0.5">{right.label}</div>
                          <div className="text-sm font-medium text-gray-800">{right.value}</div>
                        </div>
                      </div>
                    ))}
                  </div>
                </InlineCard>
              </div>
            );
          })()}

          {/* Payment terms */}
          {ss?.payment_terms && (
            <div>
              <SectionLabel>Payment terms</SectionLabel>
              <InlineCard>
                <div className="grid grid-cols-3 gap-4 text-sm">
                  {ss.payment_terms.type && <div><span className="text-xs text-gray-400 block mb-0.5">Type</span><span className="font-medium capitalize">{ss.payment_terms.type}</span></div>}
                  {ss.payment_terms.deadline && <div><span className="text-xs text-gray-400 block mb-0.5">Deadline</span><span className="font-medium">{ss.payment_terms.deadline}</span></div>}
                  {ss.payment_terms.commission_pct !== undefined && <div><span className="text-xs text-gray-400 block mb-0.5">Commission</span><span className="font-medium">{ss.payment_terms.commission_pct}%</span></div>}
                </div>
              </InlineCard>
            </div>
          )}
        </div>
      )}

      {/* ROOMS */}
      {subTab === "rooms" && (
        <div>
          <div className="grid grid-cols-2 gap-3">
            {(ss?.room_types || []).map((room, i) => (
              <InlineCard key={i}>
                <div className="font-mono text-xs text-gray-400 mb-1">{room.code}</div>
                <div className="font-medium text-sm text-gray-900 mb-3 leading-tight">{room.name}</div>
                <div className="space-y-2">
                  {[
                    ["Size", room.size_m2 ? `${room.size_m2} m²` : null],
                    ["Default capacity", room.default_capacity ? `${room.default_capacity} guests` : null],
                    ["Max capacity", room.max_capacity ? `${room.max_capacity} guests` : null],
                    ["Pricing", room.price_type === "per_person_per_night" ? "Per person / night" : room.price_type === "per_unit_per_night" ? "Per unit / night" : room.price_type],
                    ["Default board", room.default_boarding],
                    ["Min. stay", room.min_stay ? `${room.min_stay} nights` : null],
                    ["Release", room.release ? `${room.release} days` : null],
                    ["Special rates", room.deviating_capacities?.length > 0 ? room.deviating_capacities.map(d => d.label).join(", ") : null],
                  ].filter(([, v]) => v !== null && v !== undefined).map(([label, value]) => (
                    <div key={label} className="flex justify-between text-xs">
                      <span className="text-gray-400">{label}</span>
                      <span className="font-medium text-gray-800">{value}</span>
                    </div>
                  ))}
                  <div className="flex justify-between text-xs">
                    <span className="text-gray-400">Allotment</span>
                    <span className={`font-medium ${room.allotment === "OR" ? "text-amber-600" : room.allotment === 1 ? "text-amber-600" : "text-gray-800"}`}>
                      {room.allotment === "OR" ? "On request" : room.allotment ? `${room.allotment} rooms` : "—"}
                    </span>
                  </div>
                </div>
                <OccupancyList occupancies={room.possible_occupancy_rules} />
              </InlineCard>
            ))}
          </div>
        </div>
      )}

      {/* PRICES */}
      {subTab === "prices" && (() => {
        const allRates = ss?.base_prices?.rates || {};
        const onReq = ss?.base_prices?.on_request || [];
        const seasons = ss?.base_prices?.seasons || [];
        const pricedCodes = Object.keys(allRates).filter(code => !onReq.includes(code));
        const orCodes = Object.keys(allRates).filter(code => onReq.includes(code));
        const allRoomCodes = [...pricedCodes, ...orCodes];
        const showRoomFilter = allRoomCodes.length > 4;
        const visibleCodes = priceRoomFilter === "all" ? pricedCodes : pricedCodes.filter(c => c === priceRoomFilter);

        return (
          <div>
            {/* Room filter */}
            {showRoomFilter && (
              <div className="flex items-center gap-2 mb-4 flex-wrap">
                <button onClick={() => setPriceRoomFilter("all")}
                  className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${priceRoomFilter === "all" ? "border-gray-300 bg-gray-100 text-gray-800 font-medium" : "border-gray-200 bg-white text-gray-400 hover:text-gray-600"}`}>
                  All rooms
                </button>
                {pricedCodes.map(code => (
                  <button key={code} onClick={() => setPriceRoomFilter(code)}
                    className={`text-xs px-3 py-1.5 rounded-full border transition-colors font-mono ${priceRoomFilter === code ? "border-gray-300 bg-gray-100 text-gray-800 font-medium" : "border-gray-200 bg-white text-gray-400 hover:text-gray-600"}`}>
                    {code}
                  </button>
                ))}
              </div>
            )}

            {/* Price table */}
            {(() => {
              const colCount = visibleCodes.reduce((n, code) => n + Object.keys(allRates[code] || {}).length, 0);
              return (
              <div className="overflow-x-auto rounded-xl border border-gray-100">
                <table className="w-full text-xs border-collapse bg-white" style={{ minWidth: Math.max(400, colCount * 120 + 180) }}>
                  <thead>
                    <tr className="border-b border-gray-100">
                      <th className="text-left px-4 py-3 font-semibold text-gray-400 whitespace-nowrap sticky left-0 bg-white z-10" style={{ minWidth: 160, boxShadow: "2px 0 4px -2px rgba(0,0,0,0.06)" }}>Season</th>
                      {visibleCodes.flatMap(code => {
                        const labels = allRates[code] || {};
                        return Object.keys(labels).map(label => (
                          <th key={`${code}-${label}`} className="text-right px-4 py-3 font-semibold text-gray-400 whitespace-nowrap">
                            <span className="font-mono">{code}</span>{label !== "default" ? <span className="text-gray-300 font-normal ml-1">{label}</span> : ""}
                          </th>
                        ));
                      })}
                    </tr>
                  </thead>
                  <tbody>
                    {seasons.map(([start, end], si) => (
                      <tr key={si} className="border-b border-gray-50 last:border-0 hover:bg-gray-50 transition-colors">
                        <td className="px-4 py-2.5 text-gray-500 whitespace-nowrap sticky left-0 bg-white z-10" style={{ boxShadow: "2px 0 4px -2px rgba(0,0,0,0.06)" }}>{start} – {end}</td>
                        {visibleCodes.flatMap(code => {
                          const labels = allRates[code] || {};
                          return Object.entries(labels).map(([label, prices]) => {
                            const raw = Array.isArray(prices) ? prices[si] : null;
                            const price = typeof raw === "string" ? parseFloat(raw) : raw;
                            return (
                              <td key={`${code}-${label}`} className="px-4 py-2.5 text-right">
                                {price == null || isNaN(price) ? <span className="text-gray-300">—</span> : <span className="font-medium font-mono text-gray-800">{price.toFixed(2)}</span>}
                              </td>
                            );
                          });
                        })}
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              );
            })()}

            {/* On-request rooms */}
            {orCodes.length > 0 && (
              <div className="mt-3 flex items-center gap-2 flex-wrap">
                <span className="text-xs text-gray-400">On request:</span>
                {orCodes.map(code => (
                  <span key={code} className="inline-flex items-center gap-1.5 text-xs font-mono bg-amber-50 text-amber-600 border border-amber-100 px-2.5 py-1 rounded-full">{code} <span className="text-amber-400 font-sans">OR</span></span>
                ))}
              </div>
            )}

            <p className="text-xs text-gray-400 mt-2">All prices in {ss?.hotel?.currency || ss?.base_prices?.currency || "—"} · taxes included where stated</p>
          </div>
        );
      })()}

      {/* FLAGS */}
      {subTab === "flags" && (
        <div className="space-y-3">
          {flags.length === 0 && (
            <div className="text-center py-16 text-gray-400"><div className="text-3xl mb-2">✓</div><p className="text-sm">No review items</p></div>
          )}
          {flags.filter(f => f.severity === "blocker").map(f => (
            <div key={f.id} className="border border-red-100 bg-red-50 rounded-xl p-4">
              <div className="flex items-center gap-2 mb-1.5"><Pill color="red">Blocker</Pill><span className="font-mono text-xs text-gray-400">{f.id}</span></div>
              <p className="text-sm text-gray-800">{f.message}</p>
              {f.path && <p className="font-mono text-xs text-gray-400 mt-1.5">{f.path}</p>}
            </div>
          ))}
          {flags.filter(f => f.severity === "warning").map(f => (
            <div key={f.id} className="border border-amber-100 bg-amber-50 rounded-xl p-4">
              <div className="flex items-center gap-2 mb-1.5"><Pill color="amber">Warning</Pill><span className="font-mono text-xs text-gray-400">{f.id}</span></div>
              <p className="text-sm text-gray-800">{f.message}</p>
              {f.path && <p className="font-mono text-xs text-gray-400 mt-1.5">{f.path}</p>}
              {f.current_value !== null && f.current_value !== undefined && (
                <p className="text-xs text-gray-500 mt-1">Current: <code className="bg-white border border-amber-100 rounded px-1">{JSON.stringify(f.current_value)}</code>
                {f.suggested_value !== undefined && f.suggested_value !== null && <> → Suggested: <code className="bg-white border border-amber-100 rounded px-1">{JSON.stringify(f.suggested_value)}</code></>}</p>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// UI — PRICING RULES PANEL
// ─────────────────────────────────────────────────────────────────────────────

const RULE_TYPE_CONFIG = {
  discount:     { label: "Discount",     color: "bg-green-50 text-green-700 border border-green-100" },
  early_booking:{ label: "Early booking", color: "bg-blue-50 text-blue-700 border border-blue-100" },
  offer:        { label: "Offer",         color: "bg-purple-50 text-purple-700 border border-purple-100" },
  supplement:   { label: "Supplement",   color: "bg-amber-50 text-amber-700 border border-amber-100" },
  compulsory:   { label: "Compulsory",   color: "bg-red-50 text-red-700 border border-red-100" },
};

function RuleCard({ rule, onConfirm, onReject, onUpdate, status, onUndo }) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState({});

  const startEdit = () => { setDraft({ label: rule.label, description: rule.description, applies_to_room_codes: typeof rule.applies_to_room_codes === "string" ? rule.applies_to_room_codes : rule.applies_to_room_codes?.join(", "), combinable: rule.combinable, note: rule.note }); setEditing(true); };
  const saveEdit = () => { onUpdate({ ...rule, ...draft, applies_to_room_codes: draft.applies_to_room_codes === "all" ? "all" : (draft.applies_to_room_codes || "").split(",").map(s => s.trim()).filter(Boolean), combinable: draft.combinable === "true" || draft.combinable === true }); onConfirm(); setEditing(false); };

  const typeConfig = RULE_TYPE_CONFIG[rule.type] || RULE_TYPE_CONFIG.discount;
  const roomsDisplay = Array.isArray(rule.applies_to_room_codes) ? rule.applies_to_room_codes.join(", ") : (rule.applies_to_room_codes || "all");
  const isNotCombinable = rule.combinable === false;

  const cardBorder = status === "confirmed" ? "border-green-100" : status === "rejected" ? "border-gray-100 opacity-50" : "border-gray-100";

  return (
    <div className={`bg-white border rounded-xl overflow-hidden mb-2 ${cardBorder}`}>
      <div className="flex items-start gap-3 p-4">
        <span className={`text-xs font-medium px-2 py-0.5 rounded-full shrink-0 mt-0.5 ${typeConfig.color}`}>{typeConfig.label}</span>
        <div className="flex-1 min-w-0">
          <div className="font-mono text-xs text-gray-300 mb-1">{rule.id}</div>
          <div className="font-medium text-sm text-gray-900 mb-2">{rule.label}</div>
          <div className="text-xs text-gray-500 leading-relaxed">{rule.description}</div>
          {rule.note && <div className="text-xs text-gray-400 bg-gray-50 rounded-lg px-3 py-2 mt-2 leading-relaxed">{rule.note}</div>}
        </div>
      </div>

      <div className="flex items-center gap-3 px-4 pb-3 flex-wrap">
        <span className="text-xs text-gray-400">Rooms <span className="text-gray-600 font-medium">{roomsDisplay}</span></span>
        <span className={`text-xs ${isNotCombinable ? "text-red-500" : "text-gray-400"}`}>
          Combinable <span className="font-medium">{isNotCombinable ? "No" : "Yes"}</span>
        </span>
      </div>

      <div className="flex items-center gap-2 px-4 py-2.5 border-t border-gray-50 bg-gray-50">
        {status === "confirmed" && <><span className="text-xs font-medium text-green-600">✓ Confirmed</span><button onClick={startEdit} className="text-xs px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-50 ml-1">Edit</button><button onClick={onUndo} className="text-xs px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-50">Undo</button></>}
        {status === "rejected" && <><span className="text-xs font-medium text-gray-400">✕ Rejected</span><button onClick={onUndo} className="text-xs px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-50 ml-1">Undo</button></>}
        {status === "pending" && <><button onClick={onConfirm} className="text-xs px-3 py-1.5 rounded-lg border border-green-200 bg-green-50 text-green-700 hover:bg-green-100 font-medium">✓ Confirm</button><button onClick={onReject} className="text-xs px-3 py-1.5 rounded-lg border border-red-200 bg-white text-red-500 hover:bg-red-50">✕ Reject</button><button onClick={startEdit} className="text-xs px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-50 ml-auto">Edit</button></>}
      </div>

      {editing && (
        <div className="border-t border-gray-100 p-4 bg-gray-50 space-y-3">
          <div>
            <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Label</div>
            <input className="w-full text-sm px-3 py-2 border border-gray-200 rounded-lg bg-white" value={draft.label || ""} onChange={e => setDraft(d => ({ ...d, label: e.target.value }))} />
          </div>
          <div>
            <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Description</div>
            <textarea className="w-full text-xs px-3 py-2 border border-gray-200 rounded-lg bg-white resize-y" style={{ minHeight: 72, fontFamily: "inherit" }} value={draft.description || ""} onChange={e => setDraft(d => ({ ...d, description: e.target.value }))} />
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div>
              <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Applies to rooms</div>
              <input className="w-full text-sm px-3 py-2 border border-gray-200 rounded-lg bg-white" value={draft.applies_to_room_codes || ""} onChange={e => setDraft(d => ({ ...d, applies_to_room_codes: e.target.value }))} />
            </div>
            <div>
              <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Combinable</div>
              <select className="w-full text-sm px-3 py-2 border border-gray-200 rounded-lg bg-white" value={String(draft.combinable)} onChange={e => setDraft(d => ({ ...d, combinable: e.target.value === "true" }))}>
                <option value="true">Yes</option>
                <option value="false">No</option>
              </select>
            </div>
          </div>
          <div>
            <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Note (optional)</div>
            <input className="w-full text-sm px-3 py-2 border border-gray-200 rounded-lg bg-white" value={draft.note || ""} onChange={e => setDraft(d => ({ ...d, note: e.target.value || null }))} />
          </div>
          <div className="flex gap-2 pt-1">
            <button onClick={saveEdit} className="text-xs px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium">Save &amp; confirm</button>
            <button onClick={() => setEditing(false)} className="text-xs px-3 py-2 border border-gray-200 rounded-lg text-gray-500 bg-white hover:bg-gray-50">Cancel</button>
          </div>
        </div>
      )}
    </div>
  );
}

function PricingRulesPanel({ pr, onUpdatePR }) {
  const [filter, setFilter] = useState("all");
  const [statuses, setStatuses] = useState({});
  const rules = pr?.price_rules || [];
  const total = rules.length;
  const confirmedCount = Object.values(statuses).filter(s => s === "confirmed").length;

  const TYPE_FILTERS = [
    { id: "all", label: `All (${total})` },
    { id: "discount", label: "Discounts" },
    { id: "early_booking", label: "Early booking" },
    { id: "offer", label: "Offers" },
    { id: "supplement", label: "Supplements" },
    { id: "compulsory", label: "Compulsory" },
  ].filter(f => f.id === "all" || rules.some(r => r.type === f.id));

  const filtered = filter === "all" ? rules : rules.filter(r => r.type === filter);

  const setStatus = (id, s) => setStatuses(prev => ({ ...prev, [id]: s }));
  const updateRule = (i, updated) => onUpdatePR(p => { const arr = [...(p.price_rules || [])]; arr[i] = updated; return { ...p, price_rules: arr }; });

  const grouped = TYPE_FILTERS.filter(f => f.id !== "all").map(f => ({
    ...f,
    rules: filtered.filter(r => r.type === f.id)
  })).filter(g => g.rules.length > 0);

  return (
    <div>
      {/* Toolbar */}
      <div className="flex items-center gap-2 mb-5 flex-wrap">
        {TYPE_FILTERS.map(f => (
          <button key={f.id} onClick={() => setFilter(f.id)}
            className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${filter === f.id ? "border-gray-300 bg-gray-100 text-gray-800 font-medium" : "border-gray-200 bg-white text-gray-400 hover:text-gray-600"}`}>
            {f.label}
          </button>
        ))}
        <div className="ml-auto flex items-center gap-3">
          <span className="text-xs text-gray-400">{confirmedCount} of {total} confirmed</span>
          <div className="w-24 h-1.5 bg-gray-100 rounded-full overflow-hidden">
            <div className="h-full bg-green-500 rounded-full transition-all" style={{ width: total > 0 ? `${Math.round((confirmedCount/total)*100)}%` : "0%" }} />
          </div>
        </div>
      </div>

      {/* Rules */}
      {filter === "all" ? (
        grouped.map(group => (
          <div key={group.id}>
            <div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3 mt-5 first:mt-0 pb-2 border-b border-gray-100">{group.label}</div>
            {group.rules.map((rule, i) => {
              const globalIdx = rules.findIndex(r => r.id === rule.id);
              return (
                <RuleCard key={rule.id || i} rule={rule}
                  status={statuses[rule.id] || "pending"}
                  onConfirm={() => setStatus(rule.id, "confirmed")}
                  onReject={() => setStatus(rule.id, "rejected")}
                  onUndo={() => setStatus(rule.id, "pending")}
                  onUpdate={updated => updateRule(globalIdx, updated)} />
              );
            })}
          </div>
        ))
      ) : (
        filtered.map((rule, i) => {
          const globalIdx = rules.findIndex(r => r.id === rule.id);
          return (
            <RuleCard key={rule.id || i} rule={rule}
              status={statuses[rule.id] || "pending"}
              onConfirm={() => setStatus(rule.id, "confirmed")}
              onReject={() => setStatus(rule.id, "rejected")}
              onUndo={() => setStatus(rule.id, "pending")}
              onUpdate={updated => updateRule(globalIdx, updated)} />
          );
        })
      )}

      {filtered.length === 0 && (
        <div className="text-center py-16 text-gray-400 text-sm">No rules of this type found</div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// APP
// ─────────────────────────────────────────────────────────────────────────────

function App() {
  const [stage, setStage] = useState("login");
  const [password, setPassword] = useState("");
  const [fileName, setFileName] = useState("");
  const [error, setError] = useState(null);
  const [ss, setSS] = useState(null);
  const [pr, setPR] = useState(null);
  const [mainTab, setMainTab] = useState("hotel");
  const [dragOver, setDragOver] = useState(false);
  const abortRef = useRef(null);
  const [streamText, setStreamText] = useState("");
  const [phase, setPhase] = useState("");
  const phaseIdxRef = useRef(0);

  const processFile = useCallback(async (file) => {
    const name = file?.name?.toLowerCase() || "";
    const isPDF = name.endsWith(".pdf");
    const isExcel = name.endsWith(".xlsx") || name.endsWith(".xls");
    if (!isPDF && !isExcel) { setError("Please upload a PDF or Excel (.xlsx) file."); return; }
    setFileName(file.name);
    setError(null);
    setStreamText("");
    setPhase("");
    phaseIdxRef.current = 0;
    abortRef.current = new AbortController();
    setStage("loading");
    try {
      let result;
      if (isExcel) {
        const text = await extractExcelText(file);
        result = await extractContractFromText(text, p => setStreamText(p), p => { setPhase(p); setStreamText(`// ${p}`); }, abortRef.current?.signal);
      } else {
        const b64 = await fileToBase64(file);
        result = await extractContract(b64, p => setStreamText(p), p => { setPhase(p); setStreamText(`// ${p}`); }, abortRef.current?.signal);
      }
      setSS(result.system_structure);
      setPR(result.price_rules);
      setMainTab("hotel");
      setStage("review");
    } catch (e) {
      if (e.name === "AbortError") { setStage("upload"); return; }
      setError(e.message || "Extraction failed — check console for details.");
      setStage("upload");
    }
  }, []);

  const onDrop = useCallback((e) => { e.preventDefault(); setDragOver(false); processFile(e.dataTransfer.files[0]); }, [processFile]);
  const onInputChange = useCallback((e) => processFile(e.target.files[0]), [processFile]);

  const exportData = () => {
    const blob = new Blob([JSON.stringify({ system_structure: ss, price_rules: pr }, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `${(ss?.hotel?.name || "hotel").toLowerCase().replace(/\s+/g, "_")}_extraction.json`;
    a.click();
    URL.revokeObjectURL(url);
  };

  // ── LOGIN ────────────────────────────────────────────────────────────
  if (stage === "login") return (
    <div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
      <div className="w-full max-w-sm">
        <div className="text-center mb-8">
          <div className="inline-block mb-4">
            <div className="text-2xl font-bold text-gray-900 tracking-wide uppercase">TRAVELPLUGIN</div>
            <div className="h-0.5 w-full bg-amber-400 rounded-full mt-1" />
          </div>
          <h1 className="text-lg font-semibold text-gray-900">Contract Extractor</h1>
        </div>
        <form onSubmit={function (e) {
          e.preventDefault();
          setError(null);
          fetch("/api/auth", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ password: password })
          })
            .then(function (r) { return r.json(); })
            .then(function (data) {
              if (data.ok) { authToken = data.token; setStage("upload"); setPassword(""); }
              else { setError("Incorrect password"); }
            })
            .catch(function () { setError("Connection error"); });
        }}>
          <input
            type="password"
            placeholder="Enter password"
            value={password}
            onChange={function (e) { setPassword(e.target.value); }}
            className="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
          />
          <button type="submit" className="w-full mt-3 bg-blue-600 text-white text-sm font-medium py-3 rounded-xl hover:bg-blue-700 transition-colors">
            Continue
          </button>
          {error && <div className="mt-3 text-sm text-red-600 text-center">{error}</div>}
        </form>
      </div>
    </div>
  );

  // ── UPLOAD ───────────────────────────────────────────────────────────
  if (stage === "upload") return (
    <div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
      <div className="w-full max-w-md">
        <div className="text-center mb-8">
          <div className="inline-block mb-4">
            <div className="text-2xl font-bold text-gray-900 tracking-wide uppercase">TRAVELPLUGIN</div>
            <div className="h-0.5 w-full bg-amber-400 rounded-full mt-1" />
          </div>
          <h1 className="text-lg font-semibold text-gray-900">Contract Extractor</h1>
          <p className="text-gray-400 text-sm mt-1">Upload a hotel contract PDF or Excel file</p>
        </div>
        <div
          className={`border-2 border-dashed rounded-2xl p-10 text-center cursor-pointer transition-all ${dragOver ? "border-blue-400 bg-blue-50" : "border-gray-200 bg-white hover:border-gray-300"}`}
          onDragOver={e => { e.preventDefault(); setDragOver(true); }}
          onDragLeave={() => setDragOver(false)}
          onDrop={onDrop}
          onClick={() => document.getElementById("pdf-input").click()}
        >
          <div className="text-3xl mb-3 text-gray-200">↑</div>
          <p className="text-gray-600 font-medium text-sm">Drop contract here</p>
          <p className="text-gray-400 text-xs mt-1">PDF or Excel · click to browse</p>
          <input id="pdf-input" type="file" accept=".pdf,.xlsx,.xls" className="hidden" onChange={onInputChange} />
        </div>
        {error && <div className="mt-4 p-3 bg-red-50 border border-red-100 rounded-xl text-sm text-red-600">{error}</div>}
      </div>
    </div>
  );

  // ── LOADING ──────────────────────────────────────────────────────────
  if (stage === "loading") {
    const lines = streamText ? streamText.split("\n").slice(-10) : [];
    const PHASES = [["Reading structure", "Reading hotel"], ["Extracting rules", "Extracting price"], ["Reading prices", "Extracting base"], ["Validating", "Validating"]];
    const rawPhaseIdx = PHASES.findIndex(([, m]) => (phase || "").startsWith(m));
    if (rawPhaseIdx >= 0) phaseIdxRef.current = rawPhaseIdx;
    const phaseIdx = rawPhaseIdx >= 0 ? rawPhaseIdx : phaseIdxRef.current;
    return (
      <div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-8">
        <div className="w-full max-w-xl">
          <div className="flex items-center gap-4 mb-6">
            <div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin shrink-0" />
            <div className="flex-1">
              <p className="text-sm font-medium text-gray-800">{phase || "Starting extraction…"}</p>
              <div className="flex gap-2 mt-2">
                {PHASES.map(([label], i) => (
                  <div key={i} className="flex items-center gap-1.5">
                    <div className={`h-1 w-8 rounded-full transition-colors ${i < phaseIdx ? "bg-blue-500" : i === phaseIdx ? "bg-blue-300" : "bg-gray-200"}`} />
                    <span className={`text-xs ${i <= phaseIdx ? "text-blue-500" : "text-gray-300"}`}>{label}</span>
                  </div>
                ))}
              </div>
            </div>
            <button onClick={() => abortRef.current?.abort()} className="text-xs text-red-500 px-3 py-1.5 rounded-lg border border-red-200 hover:bg-red-50">Cancel</button>
          </div>
          <div className="bg-white border border-gray-100 rounded-xl p-4 font-mono text-xs text-gray-400 overflow-hidden" style={{ minHeight: 160 }}>
            {lines.length === 0 && <span className="text-gray-300 animate-pulse">Waiting…</span>}
            {lines.map((line, i) => <div key={i} className={i === lines.length - 1 ? "text-blue-500" : ""}>{line || " "}</div>)}
            {streamText.length > 0 && <span className="inline-block w-1 h-3 bg-blue-500 animate-pulse ml-0.5" style={{ verticalAlign: "text-bottom" }} />}
          </div>
        </div>
      </div>
    );
  }

  // ── REVIEW ───────────────────────────────────────────────────────────
  const flags = Array.isArray(ss?._review_required) ? ss._review_required : [];
  const blockers = flags.filter(f => f.severity === "blocker").length;
  const warnings = flags.filter(f => f.severity === "warning").length;
  const ruleCount = pr?.price_rules?.length || 0;

  const contractStart = ss?.contract_validity?.start;
  const contractEnd = ss?.contract_validity?.end;

  return (
    <div className="min-h-screen bg-gray-50">
      {/* Top bar */}
      <div className="bg-white border-b border-gray-100 px-6 py-3 sticky top-0 z-20">
        <div className="max-w-5xl mx-auto flex items-center gap-4">
          <div className="shrink-0 mr-2">
            <div className="text-sm font-bold text-gray-900 tracking-wide uppercase leading-tight">TRAVELPLUGIN</div>
            <div className="h-0.5 w-full bg-amber-400 rounded-full" />
          </div>
          <div className="flex-1 min-w-0">
            <h1 className="text-sm font-semibold text-gray-900 truncate">{ss?.hotel?.name || "Hotel"}</h1>
            <div className="flex items-center gap-2 mt-0.5 flex-wrap">
              {ss?.hotel?.currency && <span className="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded-full">{ss.hotel.currency}</span>}
              {contractStart && contractEnd && <span className="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded-full">{contractStart} – {contractEnd}</span>}
              {blockers > 0 && <span className="text-xs text-red-600 font-medium">{blockers} blocker{blockers > 1 ? "s" : ""}</span>}
              {warnings > 0 && <span className="text-xs text-amber-500 font-medium">{warnings} warning{warnings > 1 ? "s" : ""}</span>}
              {blockers === 0 && warnings === 0 && <span className="text-xs text-green-600 font-medium">No issues</span>}
            </div>
          </div>
          <button onClick={() => { setSS(null); setPR(null); setStage("upload"); }} className="text-xs text-gray-400 hover:text-gray-600 px-3 py-1.5 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors">New contract</button>
          <button onClick={exportData} className="text-xs font-medium bg-blue-600 text-white px-4 py-1.5 rounded-lg hover:bg-blue-700 transition-colors">Export JSON</button>
        </div>
      </div>

      {/* Main tabs */}
      <div className="bg-white border-b border-gray-100 px-6">
        <div className="max-w-5xl mx-auto flex">
          {[
            { id: "hotel", label: "Hotel information" },
            { id: "rules", label: `Pricing rules (${ruleCount})` },
          ].map(t => (
            <button key={t.id} onClick={() => setMainTab(t.id)}
              className={`px-5 py-3.5 text-sm font-medium border-b-2 transition-colors ${mainTab === t.id ? "border-blue-600 text-blue-600" : "border-transparent text-gray-400 hover:text-gray-700"}`}>
              {t.label}
            </button>
          ))}
        </div>
      </div>

      {/* Content */}
      <div className="max-w-5xl mx-auto px-6 py-6">
        {mainTab === "hotel" && <HotelInfoPanel ss={ss} onUpdateSS={setSS} />}
        {mainTab === "rules" && <PricingRulesPanel pr={pr} onUpdatePR={setPR} />}
      </div>
    </div>
  );
}

// SheetJS must be loaded in index.html for Excel support:
// <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(App));
