// Source: index.html script block. Loaded via <script type="text/babel"> in index.html.
// Real-world clinical data → tau2 task converter.
// Six source formats: HL7 FHIR Bundle, MIMIC-IV discharge summary, MedDialog/MTS-Dialog,
// PrimeKG node, HL7 C-CDA (XML), OMOP CDM rows. Output is the in-app task shape
// produced by createBlankTask(); the panel pretty-prints taskToTau2(task) for the user.
//
// Every converter returns { task, warnings: string[] } or throws Error.
// Last two formats (C-CDA, OMOP) are best-effort and emit a "limited parser" warning.

// ---------- shared helpers ----------

function _ageFromBirthDate(birthDate) {
  // FHIR uses YYYY-MM-DD. Approximate age — month/day precision is not guaranteed.
  if (!birthDate) return null;
  const m = String(birthDate).match(/^(\d{4})/);
  if (!m) return null;
  const yr = parseInt(m[1], 10);
  const now = new Date().getFullYear();
  const age = now - yr;
  return (age >= 0 && age <= 120) ? age : null;
}

function _normGender(g) {
  if (!g) return "unspecified";
  const s = String(g).toLowerCase();
  if (s === "m" || s === "male") return "male";
  if (s === "f" || s === "female") return "female";
  return s;
}

function _stripHtml(s) {
  return String(s || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}

function _firstSentence(s) {
  const m = String(s || "").trim().match(/^[^.!?\n]{1,160}([.!?]|$)/);
  return m ? m[0].trim() : String(s || "").slice(0, 160);
}

// ---------- 1. FHIR Bundle ----------

function convertFhir(text) {
  const warnings = [];
  let bundle;
  try { bundle = JSON.parse(text); }
  catch (e) { throw new Error("Invalid JSON: " + e.message); }
  if (!bundle || (bundle.resourceType !== "Bundle" && !Array.isArray(bundle.entry))) {
    throw new Error("Expected a FHIR Bundle (resourceType: 'Bundle' with entry[]).");
  }
  const entries = Array.isArray(bundle.entry) ? bundle.entry : [];
  const byType = {};
  for (const e of entries) {
    const r = e && e.resource;
    if (!r || !r.resourceType) continue;
    (byType[r.resourceType] = byType[r.resourceType] || []).push(r);
  }

  const patient = (byType.Patient || [])[0];
  const encounter = (byType.Encounter || [])[0];
  const conditions = byType.Condition || [];
  const medReqs = byType.MedicationRequest || [];
  const allergies = byType.AllergyIntolerance || [];
  const observations = byType.Observation || [];

  const task = window.createBlankTask({});
  if (patient) {
    const age = _ageFromBirthDate(patient.birthDate);
    if (age != null) task.age = age;
    else if (patient.birthDate) warnings.push("Patient.birthDate present but unparseable; age left at default.");
    task.gender = _normGender(patient.gender);
    const nm = patient.name && patient.name[0];
    if (nm) {
      const family = nm.family || "";
      const given = (nm.given || []).join(" ");
      const full = (given + " " + family).trim();
      if (full) task.notes = "Patient: " + full;
    }
  } else {
    warnings.push("No Patient resource found.");
  }

  const dx = conditions.find(c => c.code && (c.code.text || (c.code.coding && c.code.coding[0])));
  if (dx) {
    const dxText = (dx.code && (dx.code.text || (dx.code.coding && dx.code.coding[0] && dx.code.coding[0].display))) || "";
    task.diagnosis = dxText;
    if (dx.onsetDateTime) task.duration = "since " + String(dx.onsetDateTime).slice(0, 10);
  }

  if (encounter) {
    let reason = "";
    if (Array.isArray(encounter.reasonCode) && encounter.reasonCode[0]) {
      reason = encounter.reasonCode[0].text || (encounter.reasonCode[0].coding && encounter.reasonCode[0].coding[0] && encounter.reasonCode[0].coding[0].display) || "";
    }
    if (reason) task.chief = reason;
    if (encounter.priority && encounter.priority.text) {
      const p = encounter.priority.text.toLowerCase();
      if (p.indexOf("urg") >= 0 || p.indexOf("stat") >= 0) task.severity = "severe";
      else if (p.indexOf("rout") >= 0) task.severity = "mild";
    }
  }
  if (!task.chief && task.diagnosis) task.chief = task.diagnosis;

  task.meds = medReqs.map(m => {
    const cc = m.medicationCodeableConcept || {};
    const name = cc.text || (cc.coding && cc.coding[0] && cc.coding[0].display) || (m.medicationReference && m.medicationReference.display) || "(unnamed)";
    return { name, dose: "", freq: "" };
  });

  task.allergies = allergies.map(a => {
    const cc = a.code || {};
    return cc.text || (cc.coding && cc.coding[0] && cc.coding[0].display) || "(unnamed allergen)";
  });

  task.labsOnFile = observations
    .filter(o => o.category && o.category[0] && o.category[0].coding && o.category[0].coding[0] && /vital|laboratory/i.test(o.category[0].coding[0].code || ""))
    .slice(0, 8)
    .map(o => {
      const code = o.code && (o.code.text || (o.code.coding && o.code.coding[0] && o.code.coding[0].display)) || "observation";
      let value = "";
      if (o.valueQuantity) value = `${o.valueQuantity.value || ""} ${o.valueQuantity.unit || ""}`.trim();
      else if (o.valueString) value = o.valueString;
      return { name: code, value };
    });

  if (!task.diagnosis) warnings.push("No Condition with a parseable code found; required_diagnosis is empty.");
  if (!task.chief) warnings.push("No Encounter.reasonCode and no Condition; chief_complaint is empty.");
  return { task, warnings };
}

// ---------- 2. MIMIC-IV discharge summary ----------

function convertMimic(text) {
  const warnings = [];
  if (!text || typeof text !== "string" || text.trim().length < 30) {
    throw new Error("Pasted text is too short to look like a MIMIC discharge summary.");
  }
  const task = window.createBlankTask({});

  const ageMatch = text.match(/(\d{1,3})[- ]?year[- ]?old\s+(man|woman|male|female|m|f)?/i);
  if (ageMatch) {
    const a = parseInt(ageMatch[1], 10);
    if (a >= 0 && a <= 120) task.age = a;
    if (ageMatch[2]) task.gender = _normGender(ageMatch[2]);
  } else {
    warnings.push("No 'NN-year-old' pattern found; age left at default.");
  }

  const sexLine = text.match(/^\s*Sex:\s*([MF])\b/im);
  if (sexLine && task.gender === "unspecified") task.gender = _normGender(sexLine[1]);

  const grab = (label) => {
    // Capture up to the next blank line or a line that looks like another header.
    const re = new RegExp(label + "\\s*\\n([\\s\\S]*?)(?:\\n\\s*\\n|\\n\\s*[A-Z][A-Za-z ]{2,40}:\\s*\\n|$)", "i");
    const m = text.match(re);
    return m ? m[1].trim() : "";
  };

  const chief = grab("Chief Complaint:");
  if (chief) task.chief = _firstSentence(chief);

  const dxBlock = grab("Discharge Diagnosis:") || grab("Final Diagnosis:") || grab("Primary Diagnosis:");
  if (dxBlock) {
    const lines = dxBlock.split(/\n/).map(s => s.replace(/^[\s\d.\-)]+/, "").trim()).filter(Boolean);
    task.diagnosis = lines[0] || "";
    if (lines.length > 1) task.acceptableDiagnoses = lines.slice(1, 5);
  } else {
    warnings.push("No 'Discharge Diagnosis:' section found.");
  }

  const allergyBlock = grab("Allergies:");
  if (allergyBlock) {
    const al = allergyBlock.split(/[,\n;]/).map(s => s.trim()).filter(s => s && !/^no known/i.test(s));
    task.allergies = al.slice(0, 12);
  }

  const medsBlock = grab("Discharge Medications:") || grab("Medications on Admission:");
  if (medsBlock) {
    const lines = medsBlock.split(/\n/).map(s => s.replace(/^[\s\d.\-)]+/, "").trim()).filter(Boolean);
    task.meds = lines.slice(0, 20).map(line => {
      const dm = line.match(/^([A-Za-z][A-Za-z\- ]+?)\s+(\d+\s*(?:mg|mcg|g|ml|units?))\b/i);
      if (dm) return { name: dm[1].trim(), dose: dm[2].trim(), freq: "" };
      return { name: line, dose: "", freq: "" };
    });
  }

  const hpi = grab("History of Present Illness:") || grab("HPI:");
  if (hpi) {
    task.notes = hpi.slice(0, 600);
    if (!task.chief) task.chief = _firstSentence(hpi);
  }

  // De-identification placeholders ___ are preserved verbatim — that's how MIMIC ships.
  if (/___/.test(text)) warnings.push("De-identification placeholders ('___') preserved verbatim.");

  if (!task.chief) warnings.push("No 'Chief Complaint:' section found.");
  return { task, warnings };
}

// ---------- 3. MedDialog / MTS-Dialog ----------

function convertMedDialog(text) {
  const warnings = [];
  let payload;
  try { payload = JSON.parse(text); }
  catch (e) { throw new Error("Invalid JSON: " + e.message); }

  let turns;
  let topLevelDx = "";
  let topLevelChief = "";
  if (Array.isArray(payload)) {
    turns = payload;
  } else if (payload && Array.isArray(payload.dialogue)) {
    turns = payload.dialogue;
    topLevelDx = payload.diagnosis || payload.disease || "";
    topLevelChief = payload.chief || payload.chief_complaint || "";
  } else if (payload && Array.isArray(payload.utterances)) {
    turns = payload.utterances;
    topLevelDx = payload.diagnosis || payload.disease || "";
  } else {
    throw new Error("Expected an array of {speaker, utterance} or {role, text} turns.");
  }

  const refDialogue = [];
  let firstPatient = "";
  let lastDoctor = "";
  turns.forEach((t, i) => {
    const speaker = (t.speaker || t.role || t.from || "").toString().toLowerCase();
    const text = t.utterance || t.text || t.content || t.message || "";
    if (!text) return;
    const isDoctor = /doctor|physician|md|assistant|provider/.test(speaker);
    const isPatient = /patient|user|caller/.test(speaker);
    const role = isDoctor ? "assistant" : isPatient ? "user" : speaker || "user";
    refDialogue.push({ idx: refDialogue.length, role, text, t: "" });
    if (isPatient && !firstPatient) firstPatient = text;
    if (isDoctor) lastDoctor = text;
  });

  if (refDialogue.length === 0) throw new Error("No turns with text found in payload.");

  const task = window.createBlankTask({});
  task.refDialogue = refDialogue;
  task.chief = topLevelChief || _firstSentence(firstPatient);
  task.diagnosis = topLevelDx || "";
  task.minTurns = Math.max(3, Math.min(20, Math.floor(refDialogue.length / 2)));
  task.maxTurns = Math.max(task.minTurns, Math.min(20, refDialogue.length));

  if (!task.diagnosis) {
    const dxHint = lastDoctor.match(/diagnosis(?: of| is|:)\s*([^.\n;]{3,80})/i);
    if (dxHint) task.diagnosis = dxHint[1].trim();
    else warnings.push("No top-level diagnosis/disease field; required_diagnosis is empty.");
  }
  if (!task.chief) warnings.push("First patient utterance missing; chief_complaint is empty.");
  return { task, warnings };
}

// ---------- 4. PrimeKG node ----------

function convertPrimeKG(text) {
  const warnings = [];
  let node;
  try { node = JSON.parse(text); }
  catch (e) { throw new Error("Invalid JSON: " + e.message); }
  if (!node || typeof node !== "object" || !node.disease) {
    throw new Error("Expected an object with at least a 'disease' field.");
  }

  const task = window.createBlankTask({ diagnosis: node.disease });
  const symptoms = Array.isArray(node.symptoms) ? node.symptoms : [];
  const drugs = Array.isArray(node.drugs) ? node.drugs : [];
  const diffs = Array.isArray(node.differentials) ? node.differentials : [];

  if (symptoms.length > 0) {
    task.chief = symptoms[0];
    task.mustAskAbout = symptoms.slice(0, 8);
  } else {
    task.chief = node.disease;
    warnings.push("No symptoms[] provided; chief_complaint synthesized from disease name.");
  }

  if (drugs.length > 0) task.meds = drugs.slice(0, 10).map(d => ({ name: d, dose: "", freq: "" }));
  if (diffs.length > 0) task.acceptableDiagnoses = diffs.slice(0, 8);

  task.notes = "Synthesized from PrimeKG node: " + node.disease;
  return { task, warnings };
}

// ---------- 5. HL7 C-CDA (XML) ----------

function convertCCDA(text) {
  const warnings = ["C-CDA parser is best-effort; verify before submitting."];
  if (!/^\s*<\?xml|^\s*<ClinicalDocument/i.test(text)) {
    throw new Error("Input does not look like XML. Expected a C-CDA document.");
  }
  const parser = new DOMParser();
  const doc = parser.parseFromString(text, "text/xml");
  const perr = doc.getElementsByTagName("parsererror")[0];
  if (perr) throw new Error("XML parse error: " + (perr.textContent || "").slice(0, 120));

  const task = window.createBlankTask({});
  const patient = doc.querySelector("recordTarget patientRole patient");
  if (patient) {
    const gEl = patient.querySelector("administrativeGenderCode");
    if (gEl) task.gender = _normGender(gEl.getAttribute("code"));
    const bEl = patient.querySelector("birthTime");
    if (bEl) {
      const v = bEl.getAttribute("value") || "";
      const m = v.match(/^(\d{4})/);
      if (m) {
        const age = new Date().getFullYear() - parseInt(m[1], 10);
        if (age >= 0 && age <= 120) task.age = age;
      }
    }
  } else {
    warnings.push("No recordTarget/patientRole/patient element found.");
  }

  // LOINC-coded sections
  const sectionByLoinc = {};
  const sections = doc.querySelectorAll("component section");
  sections.forEach(sec => {
    const codeEl = sec.querySelector(":scope > code");
    const code = codeEl && codeEl.getAttribute("code");
    if (code) sectionByLoinc[code] = sec;
  });

  const textOf = (sec) => sec ? _stripHtml(sec.textContent).slice(0, 600) : "";

  // Problems (11450-4) → diagnosis
  const problems = sectionByLoinc["11450-4"];
  if (problems) {
    const t = textOf(problems);
    task.diagnosis = _firstSentence(t).replace(/^Problem(s)?\s*[:\-]?\s*/i, "");
  }
  // Medications (10160-0)
  const meds = sectionByLoinc["10160-0"];
  if (meds) {
    const t = textOf(meds).replace(/^Medications?\s*[:\-]?\s*/i, "");
    task.meds = t.split(/[,;\n]/).map(s => s.trim()).filter(Boolean).slice(0, 10).map(name => ({ name, dose: "", freq: "" }));
  }
  // Allergies (48765-2)
  const allergy = sectionByLoinc["48765-2"];
  if (allergy) {
    const t = textOf(allergy).replace(/^Allerg(y|ies)\s*[:\-]?\s*/i, "");
    task.allergies = t.split(/[,;\n]/).map(s => s.trim()).filter(s => s && !/no known/i.test(s)).slice(0, 10);
  }
  // History of Present Illness (10164-2)
  const hpi = sectionByLoinc["10164-2"];
  if (hpi) {
    const t = textOf(hpi);
    task.notes = t;
    if (!task.chief) task.chief = _firstSentence(t).replace(/^History.*?:\s*/i, "");
  }

  if (!task.diagnosis) warnings.push("No Problems section (LOINC 11450-4); required_diagnosis is empty.");
  if (!task.chief) {
    task.chief = task.diagnosis || "";
    if (!task.chief) warnings.push("No HPI section (LOINC 10164-2); chief_complaint is empty.");
  }
  return { task, warnings };
}

// ---------- 6. OMOP CDM rows ----------

function convertOMOP(text) {
  const warnings = ["OMOP parser is best-effort; concept_id resolution is not performed."];
  let rows;
  try { rows = JSON.parse(text); }
  catch (e) { throw new Error("Invalid JSON: " + e.message); }
  if (!rows || typeof rows !== "object") throw new Error("Expected an OMOP rows object.");

  const task = window.createBlankTask({});
  const person = rows.person || (Array.isArray(rows.persons) && rows.persons[0]) || null;
  if (person) {
    if (person.year_of_birth) {
      const age = new Date().getFullYear() - parseInt(person.year_of_birth, 10);
      if (age >= 0 && age <= 120) task.age = age;
    }
    if (person.gender_concept_name || person.gender) task.gender = _normGender(person.gender_concept_name || person.gender);
    else if (person.gender_concept_id === 8507) task.gender = "male";
    else if (person.gender_concept_id === 8532) task.gender = "female";
  } else {
    warnings.push("No 'person' record found.");
  }

  const conditions = Array.isArray(rows.condition_occurrence) ? rows.condition_occurrence : [];
  if (conditions.length > 0) {
    const c = conditions[0];
    task.diagnosis = c.concept_name || c.condition_concept_name || c.condition_source_value || ("concept_id:" + (c.condition_concept_id || ""));
    if (conditions.length > 1) {
      task.acceptableDiagnoses = conditions.slice(1, 5)
        .map(x => x.concept_name || x.condition_concept_name || x.condition_source_value)
        .filter(Boolean);
    }
  } else {
    warnings.push("No condition_occurrence rows; required_diagnosis is empty.");
  }

  const drugs = Array.isArray(rows.drug_exposure) ? rows.drug_exposure : [];
  if (drugs.length > 0) {
    task.meds = drugs.slice(0, 12).map(d => ({
      name: d.concept_name || d.drug_concept_name || d.drug_source_value || ("concept_id:" + (d.drug_concept_id || "")),
      dose: d.dose || "",
      freq: "",
    }));
  }

  const visits = Array.isArray(rows.visit_occurrence) ? rows.visit_occurrence : [];
  if (visits[0] && visits[0].visit_concept_name) {
    task.domain = visits[0].visit_concept_name;
  }

  if (task.diagnosis) task.chief = task.diagnosis;
  if (!task.chief) warnings.push("No condition to seed chief_complaint; left empty.");
  return { task, warnings };
}

// ---------- samples ----------

const SAMPLES = {
  fhir: JSON.stringify({
    resourceType: "Bundle",
    type: "collection",
    entry: [
      { resource: { resourceType: "Patient", gender: "male", birthDate: "1985-04-12", name: [{ family: "Doe", given: ["John"] }] } },
      { resource: { resourceType: "Encounter", reasonCode: [{ text: "Chest pain" }], priority: { text: "urgent" } } },
      { resource: { resourceType: "Condition", code: { text: "Acute coronary syndrome", coding: [{ display: "Acute coronary syndrome" }] }, onsetDateTime: "2026-04-18" } },
      { resource: { resourceType: "MedicationRequest", medicationCodeableConcept: { text: "lisinopril 10mg" } } },
      { resource: { resourceType: "AllergyIntolerance", code: { text: "penicillin" } } },
      { resource: { resourceType: "Observation", category: [{ coding: [{ code: "vital-signs" }] }], code: { text: "Heart rate" }, valueQuantity: { value: 92, unit: "bpm" } } }
    ]
  }, null, 2),

  mimic: `Sex: M
Service: MEDICINE

Allergies:
penicillin, sulfa drugs

Chief Complaint:
chest pain

History of Present Illness:
Mr. ___ is a 58-year-old man with a history of hypertension and tobacco use
who presents with 3 days of substernal chest pressure radiating to the left
arm, worse with exertion. No prior cardiac history.

Discharge Diagnosis:
1. Non-ST elevation myocardial infarction
2. Hypertension
3. Tobacco use disorder

Discharge Medications:
1. Aspirin 81 mg PO daily
2. Atorvastatin 80 mg PO QHS
3. Metoprolol succinate 25 mg PO daily
`,

  meddialog: JSON.stringify({
    diagnosis: "Allergic rhinitis",
    chief: "sneezing and runny nose",
    dialogue: [
      { speaker: "patient", text: "I've been sneezing nonstop for two weeks and my nose is constantly running." },
      { speaker: "doctor", text: "Any itchy eyes or throat? Does it get worse outdoors?" },
      { speaker: "patient", text: "Yes, much worse when I'm outside, and my eyes itch a lot." },
      { speaker: "doctor", text: "This sounds like seasonal allergic rhinitis. Try a daily antihistamine like loratadine." }
    ]
  }, null, 2),

  primekg: JSON.stringify({
    disease: "Type 2 diabetes mellitus",
    symptoms: ["increased thirst", "frequent urination", "fatigue", "blurred vision"],
    drugs: ["metformin", "glipizide"],
    differentials: ["Type 1 diabetes mellitus", "MODY", "Gestational diabetes"]
  }, null, 2),

  ccda: `<?xml version="1.0"?>
<ClinicalDocument xmlns="urn:hl7-org:v3">
  <recordTarget>
    <patientRole>
      <patient>
        <administrativeGenderCode code="F"/>
        <birthTime value="19720314"/>
      </patient>
    </patientRole>
  </recordTarget>
  <component>
    <structuredBody>
      <component>
        <section>
          <code code="11450-4"/>
          <title>Problems</title>
          <text>Migraine without aura</text>
        </section>
      </component>
      <component>
        <section>
          <code code="10160-0"/>
          <title>Medications</title>
          <text>sumatriptan 50mg, propranolol 40mg</text>
        </section>
      </component>
      <component>
        <section>
          <code code="48765-2"/>
          <title>Allergies</title>
          <text>No known drug allergies</text>
        </section>
      </component>
      <component>
        <section>
          <code code="10164-2"/>
          <title>History of Present Illness</title>
          <text>Patient reports recurrent unilateral throbbing headaches for 6 months, lasting 4-12 hours, with photophobia.</text>
        </section>
      </component>
    </structuredBody>
  </component>
</ClinicalDocument>`,

  omop: JSON.stringify({
    person: { person_id: 1001, year_of_birth: 1962, gender_concept_id: 8507, gender_concept_name: "MALE" },
    visit_occurrence: [{ visit_occurrence_id: 5001, visit_concept_name: "Emergency Room Visit" }],
    condition_occurrence: [
      { condition_concept_id: 4329847, concept_name: "Pneumonia" },
      { condition_concept_id: 4193704, concept_name: "Cough" }
    ],
    drug_exposure: [
      { drug_concept_id: 1734104, concept_name: "amoxicillin 500mg" },
      { drug_concept_id: 19078187, concept_name: "albuterol inhaler" }
    ]
  }, null, 2),
};

const CAA_CONVERTERS = {
  fhir: convertFhir,
  mimic: convertMimic,
  meddialog: convertMedDialog,
  primekg: convertPrimeKG,
  ccda: convertCCDA,
  omop: convertOMOP,
};

const FORMAT_KEYS = ["fhir", "mimic", "meddialog", "primekg", "ccda", "omop"];
const LIMITED_FORMATS = { ccda: true, omop: true };
const FMT_LABEL = { fhir: "FHIR", mimic: "MIMIC-IV", meddialog: "MedDialog", primekg: "PrimeKG", ccda: "C-CDA", omop: "OMOP" };

function _currentUserLabel() {
  try {
    const u = window.__CAA_USER;
    if (u && (u.email || u.name)) return u.email || u.name;
  } catch (e) { /* ignore */ }
  try {
    const raw = localStorage.getItem("caa_user");
    if (raw) {
      const u = JSON.parse(raw);
      if (u && (u.email || u.name)) return u.email || u.name;
    }
  } catch (e) { /* ignore */ }
  return "converter";
}

function _rand4() {
  return Math.random().toString(36).slice(2, 6);
}

// ---------- panel ----------

function ConverterPanel() {
  const { t: tr } = useLang();
  const [fmt, setFmt] = React.useState("fhir");
  const [src, setSrc] = React.useState("");
  const [result, setResult] = React.useState(null); // { task, tau2, warnings }
  const [error, setError] = React.useState("");
  const [toast, setToast] = React.useState("");
  // Richer feedback for Add-to-queue:
  //   null | { kind: "pending"|"ok"|"err", message, taskId, cohortId?, cohortLabel?, error? }
  const [queueFeedback, setQueueFeedback] = React.useState(null);
  const pendingTaskIdRef = React.useRef(null);

  React.useEffect(() => {
    if (!toast) return undefined;
    const id = setTimeout(() => setToast(""), 1600);
    return () => clearTimeout(id);
  }, [toast]);

  // Listen for the app's reply after dispatching caa-add-converted-task. Match
  // on taskId so stale replies from an earlier click don't overwrite a newer
  // success toast.
  React.useEffect(() => {
    const onResult = (ev) => {
      const detail = ev && ev.detail;
      if (!detail || !detail.taskId) return;
      if (detail.taskId !== pendingTaskIdRef.current) return;
      if (detail.ok) {
        setQueueFeedback(prev => prev && prev.taskId === detail.taskId ? { ...prev, kind: "ok", message: "Added" } : prev);
      } else {
        setQueueFeedback(prev => prev && prev.taskId === detail.taskId ? { ...prev, kind: "err", error: detail.error || "persistence failed" } : prev);
      }
    };
    window.addEventListener("caa-converted-task-result", onResult);
    return () => window.removeEventListener("caa-converted-task-result", onResult);
  }, []);

  const run = () => {
    setError("");
    setResult(null);
    const fn = CAA_CONVERTERS[fmt];
    if (!fn) { setError("Unknown format."); return; }
    if (!src.trim()) { setError(tr("conv_error_prefix") + " input is empty."); return; }
    try {
      const out = fn(src);
      const tau2 = window.taskToTau2(out.task);
      setResult({ task: out.task, tau2, warnings: out.warnings || [] });
    } catch (e) {
      setError(tr("conv_error_prefix") + " " + (e.message || String(e)));
    }
  };

  const loadSample = () => {
    setSrc(SAMPLES[fmt] || "");
    setError("");
    setResult(null);
  };

  const copyJson = async () => {
    if (!result) return;
    const text = JSON.stringify(result.tau2, null, 2);
    try {
      await navigator.clipboard.writeText(text);
      setToast("Copied");
    } catch (e) {
      setToast("Copy failed");
    }
  };

  const download = () => {
    if (!result) return;
    const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
    window.downloadBlob("converted_" + stamp + ".json", JSON.stringify(result.tau2, null, 2));
  };

  const addToQueue = () => {
    if (!result) return;
    const cohortId = "coh_converted_" + fmt;
    const cohortLabel = "Converted · " + (FMT_LABEL[fmt] || fmt.toUpperCase());
    const stamp = Date.now().toString(36);
    const taskId = (result.task && result.task.id) || (fmt + "_" + stamp + "_" + _rand4());
    const task = {
      ...result.task,
      id: taskId,
      status: "needs-review",
      cohortId,
      lastEditor: _currentUserLabel(),
      lastEdited: "just now",
    };
    window.__caaPendingConvertedTask = task;
    pendingTaskIdRef.current = taskId;
    setQueueFeedback({ kind: "pending", message: "Adding…", taskId, cohortId, cohortLabel });
    try {
      window.dispatchEvent(new CustomEvent("caa-add-converted-task", {
        detail: { task, cohortId, cohortLabel, format: fmt },
      }));
    } catch (e) {
      setQueueFeedback({ kind: "err", message: "", taskId, cohortId, cohortLabel, error: e.message || String(e) });
    }
  };

  const viewInQueue = () => {
    if (!queueFeedback || queueFeedback.kind !== "ok") return;
    try {
      window.dispatchEvent(new CustomEvent("caa-set-route", {
        detail: { route: "queue", activeId: queueFeedback.taskId, prefilter: { cohort: queueFeedback.cohortId } },
      }));
    } catch (e) { /* ignore */ }
  };

  const dismissFeedback = () => setQueueFeedback(null);

  return (
    <div className="converter-panel">
      <div className="converter-head">
        <h3>{tr("conv_title")}</h3>
        <div className="converter-sub">{tr("conv_subtitle")}</div>
      </div>

      <div className="converter-row">
        <label className="converter-label">{tr("conv_src_label")}</label>
        <select className="inp converter-select" value={fmt} onChange={e => { setFmt(e.target.value); setResult(null); setError(""); }}>
          {FORMAT_KEYS.map(k => (
            <option key={k} value={k}>{tr("conv_fmt_" + k)}</option>
          ))}
        </select>
        <button className="btn ghost" onClick={loadSample} type="button">{tr("conv_load_sample")}</button>
      </div>

      {LIMITED_FORMATS[fmt] && (
        <div className="converter-warn-banner">{tr("conv_limited_warn")}</div>
      )}

      <div className="converter-row converter-row-block">
        <label className="converter-label">{tr("conv_input_label")}</label>
        <textarea
          className="ta mono"
          rows={10}
          value={src}
          onChange={e => setSrc(e.target.value)}
          placeholder={tr("conv_input_ph")}
          spellCheck={false}
        />
      </div>

      <div className="converter-actions">
        <button className="btn primary" onClick={run} type="button">{tr("conv_convert_btn")}</button>
      </div>

      {error && (
        <div className="converter-error">
          <b>{tr("conv_error_prefix")}</b> {error.replace(new RegExp("^" + tr("conv_error_prefix") + "\\s*"), "")}
          <div className="converter-error-hint">check format / see example above</div>
        </div>
      )}

      {result && result.warnings.length > 0 && (
        <div className="converter-warnings">
          <div className="converter-warnings-h">{tr("conv_warnings_label")}</div>
          <ul>
            {result.warnings.map((w, i) => <li key={i}>{w}</li>)}
          </ul>
        </div>
      )}

      {result && (
        <>
          <div className="converter-row converter-row-block">
            <label className="converter-label">{tr("conv_output_label")}</label>
            <pre className="doc-code">{JSON.stringify(result.tau2, null, 2)}</pre>
          </div>
          <div className="converter-actions">
            <button className="btn" onClick={copyJson} type="button">{tr("conv_copy")}</button>
            <button className="btn" onClick={download} type="button">{tr("conv_download")}</button>
            <button className="btn" onClick={addToQueue} type="button" disabled={queueFeedback && queueFeedback.kind === "pending"}>{tr("conv_add_queue")}</button>
            {toast && <span className="converter-toast">{toast}</span>}
          </div>
          {queueFeedback && queueFeedback.kind === "pending" && (
            <div className="converter-toast-row converter-toast-pending">
              <span>{queueFeedback.taskId} · adding to {queueFeedback.cohortLabel}…</span>
            </div>
          )}
          {queueFeedback && queueFeedback.kind === "ok" && (
            <div className="converter-toast-row converter-toast-ok">
              <span><b>{queueFeedback.taskId}</b> · Added to {queueFeedback.cohortLabel}</span>
              <button className="btn ghost" onClick={viewInQueue} type="button">View in queue</button>
              <button className="btn ghost converter-toast-x" onClick={dismissFeedback} type="button" aria-label="Dismiss">×</button>
            </div>
          )}
          {queueFeedback && queueFeedback.kind === "err" && (
            <div className="converter-toast-row converter-toast-err">
              <span><b>Add failed.</b> {queueFeedback.error}</span>
              <button className="btn ghost converter-toast-x" onClick={dismissFeedback} type="button" aria-label="Dismiss">×</button>
            </div>
          )}
        </>
      )}
    </div>
  );
}

Object.assign(window, {
  ConverterPanel,
  CAA_CONVERTERS,
  convertFhir,
  convertMimic,
  convertMedDialog,
  convertPrimeKG,
  convertCCDA,
  convertOMOP,
});
