import { useCallback, useEffect, useMemo, useState } from "react";
import type { CSSProperties, Dispatch, SetStateAction } from "react";
import {
  closeEnvSocket,
  envWsSend,
  fetchCatalog,
  fetchModelStatus,
  fetchRewardBreakdown,
  orchestrateStep,
  resetEnv,
  stepCandidate,
} from "./lib/api";
import type {
  CandidateAction,
  EnvCatalog,
  EnvObservation,
  EnvStepPacket,
  ModelStatus,
  PolyGuardActionPayload,
  StepResponse,
  TaskPreset,
} from "./lib/types";
import AlternativeMedicineSearch from "./components/AlternativeMedicineSearch";
import MetaverseBackdrop from "./components/MetaverseBackdrop";

type WorkbenchMode = "agent" | "env";
type GuideTarget =
  | "topbar"
  | "mode"
  | "task"
  | "model"
  | "overview"
  | "candidates"
  | "console"
  | "rewards"
  | "medications"
  | "history"
  | "event-log";

type GuideStep = {
  target: GuideTarget;
  title: string;
  body: string;
};

const FALLBACK_CATALOG: EnvCatalog = {
  reward_range: [0.001, 0.999],
  reward_precision: 3,
  task_presets: [
    { id: "easy_screening", label: "Easy Screening", difficulty: "easy", sub_environment: "DDI" },
    { id: "budgeted_screening", label: "Budgeted Screening", difficulty: "medium", sub_environment: "REGIMEN_RISK" },
    { id: "complex_tradeoff", label: "Complex Tradeoff", difficulty: "hard", sub_environment: "REGIMEN_RISK" },
    { id: "bandit_mining", label: "Bandit Mining", difficulty: "hard", sub_environment: "BANDIT_MINING" },
  ],
  sub_environments: [
    "DDI",
    "BANDIT_MINING",
    "REGIMEN_RISK",
    "PRECISION_DOSING",
    "LONGITUDINAL_DEPRESCRIBING",
    "WEB_SEARCH_MISSING_DATA",
    "ALTERNATIVE_SUGGESTION",
    "NEW_DRUG_DECOMPOSITION",
  ],
};

const REWARD_KEYS = [
  "total_reward",
  "primary_safety_legality",
  "primary_clinical_improvement",
  "primary_dosing_quality",
  "primary_process_integrity",
  "legality_score",
  "safety_delta_score",
  "burden_improvement_score",
  "disease_stability_score",
  "dosing_quality_score",
  "process_fidelity_score",
  "explanation_grounding_score",
  "anti_cheat_score",
  "uncertainty_calibration_score",
];

const QTIPS_SEEN_KEY = "polyguard.qtips.v2.seen";

const GUIDE_STEPS: GuideStep[] = [
  {
    target: "topbar",
    title: "Start here",
    body: "PolyGuard is an interactive OpenEnv workbench. Use this top bar to choose the runtime, pick a clinical scenario, and reset into a real environment episode.",
  },
  {
    target: "mode",
    title: "Choose the runtime",
    body: "Agent Workbench uses the local REST API, candidate selector, reward breakdown, and Qwen-backed policy path. Env Explorer talks directly to the OpenEnv WebSocket service.",
  },
  {
    target: "task",
    title: "Pick a scenario",
    body: "Choose Easy Screening, Budgeted Screening, Complex Tradeoff, or Bandit Mining. Reset Episode then loads a real patient/regimen state from the backend.",
  },
  {
    target: "model",
    title: "Check the model truth",
    body: "This panel reports the live model-status endpoint. It only calls Qwen active when the API says Qwen/Qwen2.5-0.5B-Instruct artifacts are enabled and available.",
  },
  {
    target: "overview",
    title: "Read the episode state",
    body: "After reset, this shows the active task, patient, remaining step budget, latest reward, and risk delta. These values come from the current environment response.",
  },
  {
    target: "candidates",
    title: "Review legal actions",
    body: "Candidate Actions are the currently legal moves emitted by the environment. Select one to inspect its safety, uncertainty, target drug, and mode.",
  },
  {
    target: "console",
    title: "Submit or ask the agent",
    body: "Submit Candidate executes the selected legal action. Run Agent lets the policy stack choose a step, so check the model panel first if you require Qwen-backed output.",
  },
  {
    target: "rewards",
    title: "Inspect reward channels",
    body: "Reward Channels show real scorer output after each step. Empty values mean no step has produced that channel yet, not placeholder scoring.",
  },
  {
    target: "medications",
    title: "Track regimen changes",
    body: "Medication cards update from the environment observation. High-risk tags and dose/class details help explain why actions are legal or useful.",
  },
  {
    target: "history",
    title: "Audit actions and warnings",
    body: "Action History and Warnings give a running trace of what happened in the episode. Use this to verify that the workflow is not canned.",
  },
  {
    target: "event-log",
    title: "Follow the run",
    body: "The Event Log records resets, steps, rewards, and API errors. If Qwen or an env service is unavailable, this is where the UI tells you plainly.",
  },
];

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function toNumber(value: unknown): number | null {
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function formatReward(value: unknown): string {
  const num = toNumber(value);
  return num === null ? "-" : num.toFixed(3);
}

function humanize(value: string): string {
  return value
    .replace(/^primary_/, "")
    .replace(/_/g, " ")
    .replace(/\b\w/g, (char) => char.toUpperCase());
}

function shortValue(value: unknown): string {
  if (value === null || value === undefined || value === "") return "-";
  if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(value > 10 ? 0 : 3) : "-";
  if (typeof value === "boolean") return value ? "Yes" : "No";
  if (Array.isArray(value)) return value.length ? value.map(shortValue).join(", ") : "-";
  if (isRecord(value)) return JSON.stringify(value);
  return String(value);
}

function taskLabel(taskId: string, presets: TaskPreset[]): string {
  return presets.find((item) => item.id === taskId)?.label ?? humanize(taskId);
}

function taskResetOptions(taskId: string, difficulty: string, subEnvironment: string, presets: TaskPreset[]) {
  const preset = presets.find((item) => item.id === taskId);
  if (preset) {
    return {
      agent: { task_id: preset.id },
      env: { difficulty: preset.difficulty, sub_environment: preset.sub_environment },
    };
  }
  return {
    agent: { difficulty, sub_environment: subEnvironment },
    env: { difficulty, sub_environment: subEnvironment },
  };
}

function defaultCandidateForMode(candidates: CandidateAction[], mode: WorkbenchMode): CandidateAction | null {
  if (mode !== "env") return candidates[0] ?? null;

  return (
    candidates.find(
      (candidate) =>
        candidate.legality_precheck !== false &&
        candidate.action_type !== "KEEP_REGIMEN" &&
        !candidate.action_type.startsWith("REQUEST_"),
    ) ??
    candidates.find((candidate) => candidate.legality_precheck !== false && candidate.action_type !== "KEEP_REGIMEN") ??
    candidates[0] ??
    null
  );
}

function modelSignal(status: ModelStatus | null): {
  label: string;
  detail: string;
  isQwen: boolean;
  isLive: boolean;
} {
  if (!status) {
    return {
      label: "Model status unavailable",
      detail: "The API did not return /policy/model_status. Results can still run, but Qwen cannot be verified here.",
      isQwen: false,
      isLive: false,
    };
  }

  if (status.ollama?.enabled && status.ollama.available) {
    return {
      label: "Ollama Qwen active",
      detail: `${status.ollama.model || "Ollama model"} is enabled locally; provider order=${(status.provider_preference ?? []).join(" > ") || "ollama > transformers"}.`,
      isQwen: /qwen/i.test(status.ollama.model || ""),
      isLive: true,
    };
  }

  const modelName = status.model_id || status.base_model || status.runtime_model_name || "";
  const isQwen = /Qwen\/Qwen2\.5-0\.5B-Instruct/i.test(modelName);
  const available = Object.values(status.availability ?? {}).some(Boolean);
  const isLive = Boolean(status.enabled && status.active && available && isQwen);
  const artifact = status.loaded_source || status.preferred_artifact || "artifact";
  const loadError = status.load_error ? ` Load error: ${status.load_error}` : "";

  return {
    label: isLive ? "Qwen 0.5B active" : "Qwen not verified",
    detail: isLive
      ? `${modelName} is enabled with ${artifact}; run ${status.run_id || "active manifest"}.${loadError}`
      : `${modelName || "No model"}; enabled=${String(status.enabled)} active=${String(status.active)} available=${String(available)}.${loadError}`,
    isQwen,
    isLive,
  };
}

function normalizeStepPacket(packet: EnvStepPacket | StepResponse | Record<string, unknown>): {
  observation: EnvObservation | null;
  reward: number | null;
  done: boolean;
  info: Record<string, unknown>;
} {
  const observation = isRecord(packet.observation) ? (packet.observation as EnvObservation) : null;
  const info = isRecord(packet.info) ? packet.info : {};
  return {
    observation,
    reward: toNumber(packet.reward),
    done: Boolean(packet.done),
    info,
  };
}

function buildActionPayload(
  candidate: CandidateAction,
  confidence: number,
  rationale: string,
): PolyGuardActionPayload {
  return {
    mode: candidate.mode || "REVIEW",
    action_type: candidate.action_type,
    target_drug: candidate.target_drug ?? null,
    replacement_drug: candidate.replacement_drug ?? null,
    dose_bucket: candidate.dose_bucket ?? "NA",
    taper_days: candidate.taper_days ?? null,
    monitoring_plan: candidate.monitoring_plan ?? null,
    evidence_query: candidate.evidence_query ?? null,
    new_drug_name: candidate.new_drug_name ?? null,
    candidate_components: candidate.candidate_components ?? [],
    candidate_id: candidate.candidate_id,
    confidence,
    rationale_brief: rationale,
  };
}

function appendEvent(setter: Dispatch<SetStateAction<string[]>>, message: string) {
  setter((prev) => [`${new Date().toLocaleTimeString()}  ${message}`, ...prev].slice(0, 24));
}

function QTips({
  open,
  step,
  steps,
  onNext,
  onPrev,
  onClose,
}: {
  open: boolean;
  step: number;
  steps: GuideStep[];
  onNext: () => void;
  onPrev: () => void;
  onClose: () => void;
}) {
  const [rect, setRect] = useState<DOMRect | null>(null);
  const current = steps[step];

  const updateRect = useCallback(() => {
    if (!open || !current) return;
    const target = document.querySelector(`[data-guide="${current.target}"]`);
    if (!target) {
      setRect(null);
      return;
    }
    target.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
    setRect(target.getBoundingClientRect());
  }, [current, open]);

  useEffect(() => {
    updateRect();
    window.addEventListener("resize", updateRect);
    window.addEventListener("scroll", updateRect, true);
    return () => {
      window.removeEventListener("resize", updateRect);
      window.removeEventListener("scroll", updateRect, true);
    };
  }, [updateRect]);

  if (!open || !current) return null;

  const tooltipStyle = rect
    ? ({
        "--tip-top": `${Math.max(14, Math.min(window.innerHeight - 260, rect.bottom + 12))}px`,
        "--tip-left": `${Math.max(14, Math.min(window.innerWidth - 390, rect.left))}px`,
      } as CSSProperties)
    : undefined;

  return (
    <div className="qtip-overlay" role="dialog" aria-modal="true" aria-label="Q Tips walkthrough">
      <div className="qtip-dim" onClick={onClose} />
      {rect && (
        <div
          className="qtip-ring"
          style={{
            top: rect.top - 6,
            left: rect.left - 6,
            width: rect.width + 12,
            height: rect.height + 12,
          }}
        />
      )}
      <section className="qtip-card panel-surface" style={tooltipStyle}>
        <div className="qtip-header">
          <span>Q Tips</span>
          <strong>
            {step + 1} / {steps.length}
          </strong>
        </div>
        <h2>{current.title}</h2>
        <p>{current.body}</p>
        <div className="qtip-actions">
          <button className="secondary" onClick={onPrev} disabled={step === 0}>
            Back
          </button>
          <button className="secondary" onClick={onClose}>
            Skip
          </button>
          <button onClick={step === steps.length - 1 ? onClose : onNext}>
            {step === steps.length - 1 ? "Done" : "Next"}
          </button>
        </div>
      </section>
    </div>
  );
}

function TopBar({
  mode,
  setMode,
  taskId,
  onTaskChange,
  catalog,
  statusText,
  modelStatus,
  loading,
  onReset,
  onOpenTips,
}: {
  mode: WorkbenchMode;
  setMode: (mode: WorkbenchMode) => void;
  taskId: string;
  onTaskChange: (taskId: string) => void;
  catalog: EnvCatalog;
  statusText: string;
  modelStatus: ModelStatus | null;
  loading: boolean;
  onReset: () => void;
  onOpenTips: () => void;
}) {
  const signal = modelSignal(modelStatus);

  return (
    <header className="topbar panel-surface" data-guide="topbar">
      <div className="title-wrap">
        <h1>PolyGuard</h1>
        <p>OpenEnv medication safety workbench</p>
      </div>

      <div className="mode-toggle" aria-label="Runtime mode" data-guide="mode">
        <button className={mode === "agent" ? "active" : ""} onClick={() => setMode("agent")}>
          Agent Workbench
        </button>
        <button className={mode === "env" ? "active" : ""} onClick={() => setMode("env")}>
          Env Explorer
        </button>
      </div>

      <div className="topbar-status">
        <span className={`status-chip ${statusText === "Live" ? "live" : "idle"}`}>{statusText}</span>
        <span className={`status-chip ${signal.isLive ? "live" : "idle"}`}>
          {mode === "agent" ? signal.label : "ws env"}
        </span>
        <button className="qtip-trigger secondary" onClick={onOpenTips}>
          Q Tips
        </button>
      </div>

      <div className="topbar-actions" data-guide="task">
        <select aria-label="Task" value={taskId} onChange={(event) => onTaskChange(event.target.value)}>
          {catalog.task_presets.map((item) => (
            <option key={item.id} value={item.id}>
              {item.label}
            </option>
          ))}
          <option value="advanced">Advanced</option>
        </select>
        <button onClick={onReset} disabled={loading}>
          Reset Episode
        </button>
      </div>
    </header>
  );
}

function EpisodeOverview({
  mode,
  observation,
  reward,
  done,
  taskId,
  catalog,
}: {
  mode: WorkbenchMode;
  observation: EnvObservation | null;
  reward: number | null;
  done: boolean;
  taskId: string;
  catalog: EnvCatalog;
}) {
  const contract = observation?.deterministic_contract ?? {};
  const summary = observation?.patient_summary ?? {};
  const burden = observation?.burden_score_summary ?? {};

  const kpis: Array<[string, unknown]> = [
    ["Mode", mode === "agent" ? "Agent Workbench" : "Env Explorer"],
    ["Task", taskLabel(taskId, catalog.task_presets)],
    ["Difficulty", contract.difficulty ?? "-"],
    ["Environment", contract.sub_environment ?? observation?.sub_environment ?? "-"],
    ["Step Budget", observation?.step_budget_remaining ?? "-"],
    ["Last Reward", formatReward(reward)],
    ["Patient", summary.patient_id ?? summary.id ?? "-"],
    ["Status", done ? "Complete" : observation ? "Live" : "Ready"],
  ];

  return (
    <section className="panel-surface panel-wide" data-guide="overview">
      <div className="panel-heading">
        <h2>Episode Overview</h2>
        <span>{observation ? "Live" : "Ready"}</span>
      </div>
      <div className="kpi-grid">
        {kpis.map(([label, value]) => (
          <div key={String(label)}>
            <span>{label}</span>
            <strong>{shortValue(value)}</strong>
          </div>
        ))}
      </div>
      <div className="overview-lower">
        <div>
          <h3>Patient Summary</h3>
          <dl className="compact-defs">
            {Object.entries(summary).slice(0, 8).map(([key, value]) => (
              <div key={key}>
                <dt>{humanize(key)}</dt>
                <dd>{shortValue(value)}</dd>
              </div>
            ))}
            {Object.keys(summary).length === 0 && <p className="muted">No patient loaded.</p>}
          </dl>
        </div>
        <div>
          <h3>Risk Delta</h3>
          <dl className="compact-defs">
            {Object.entries(burden).slice(0, 8).map(([key, value]) => (
              <div key={key}>
                <dt>{humanize(key)}</dt>
                <dd>{shortValue(value)}</dd>
              </div>
            ))}
            {Object.keys(burden).length === 0 && <p className="muted">No risk data.</p>}
          </dl>
        </div>
      </div>
    </section>
  );
}

function CandidatePanel({
  candidates,
  selected,
  onSelect,
}: {
  candidates: CandidateAction[];
  selected: CandidateAction | null;
  onSelect: (candidateId: string) => void;
}) {
  return (
    <section className="panel-surface panel-scroll" data-guide="candidates">
      <div className="panel-heading">
        <h2>Candidate Actions</h2>
        <span>{candidates.length}</span>
      </div>
      <div className="candidate-list">
        {candidates.map((candidate) => {
          const active = candidate.candidate_id === selected?.candidate_id;
          const legal = candidate.legality_precheck !== false;
          return (
            <button
              key={candidate.candidate_id}
              className={`candidate-row ${active ? "selected" : ""} ${legal ? "" : "illegal"}`}
              onClick={() => {
                if (legal) onSelect(candidate.candidate_id);
              }}
              disabled={!legal}
            >
              <span>
                <strong>{candidate.candidate_id}</strong>
                {humanize(candidate.action_type)}
              </span>
              <span>{shortValue(candidate.target_drug ?? candidate.replacement_drug ?? candidate.mode)}</span>
              <span>{legal ? formatReward(candidate.estimated_safety_delta) : "Blocked"}</span>
            </button>
          );
        })}
        {candidates.length === 0 && <p className="muted">Reset an episode to load legal candidates.</p>}
      </div>
    </section>
  );
}

function ActionConsole({
  mode,
  selected,
  confidence,
  rationale,
  loading,
  canSubmit,
  canRunAgent,
  done,
  terminationReason,
  onConfidence,
  onRationale,
  onSubmit,
  onAgent,
  onReset,
}: {
  mode: WorkbenchMode;
  selected: CandidateAction | null;
  confidence: number;
  rationale: string;
  loading: boolean;
  canSubmit: boolean;
  canRunAgent: boolean;
  done: boolean;
  terminationReason: string | null;
  onConfidence: (value: number) => void;
  onRationale: (value: string) => void;
  onSubmit: () => void;
  onAgent: () => void;
  onReset: () => void;
}) {
  const details = [
    ["Type", selected?.action_type],
    ["Mode", selected?.mode],
    ["Target", selected?.target_drug],
    ["Replacement", selected?.replacement_drug],
    ["Dose", selected?.dose_bucket],
    ["Uncertainty", selected?.uncertainty_score],
  ];

  return (
    <section className="panel-surface action-console" data-guide="console">
      <div className="panel-heading">
        <h2>Action Console</h2>
        <span>{selected?.candidate_id ?? "-"}</span>
      </div>
      <div className="action-detail-grid">
        {details.map(([label, value]) => (
          <div key={String(label)}>
            <span>{label}</span>
            <strong>{shortValue(value)}</strong>
          </div>
        ))}
      </div>
      <label className="field">
        <span>Confidence</span>
        <input
          type="number"
          min="0.001"
          max="0.999"
          step="0.001"
          value={confidence.toFixed(3)}
          onChange={(event) => onConfidence(Number(event.target.value))}
        />
      </label>
      <label className="field">
        <span>Rationale</span>
        <input value={rationale} onChange={(event) => onRationale(event.target.value)} />
      </label>
      {done && (
        <div className="console-notice">
          {mode === "env" ? "Env Explorer" : "Agent Workbench"} returned <strong>done</strong>
          {terminationReason ? ` (${humanize(terminationReason)})` : ""}. Reset the episode before submitting another
          step.
        </div>
      )}
      <div className="button-row">
        <button onClick={done ? onReset : onSubmit} disabled={loading || (!canSubmit && !done)}>
          {done ? "Reset Episode" : mode === "env" ? "Submit Env Step" : "Submit Candidate"}
        </button>
        <button className="secondary" onClick={onAgent} disabled={mode !== "agent" || loading || done || !canRunAgent}>
          Run Agent
        </button>
      </div>
    </section>
  );
}

function MedicationCards({ meds }: { meds: Array<Record<string, unknown>> }) {
  return (
    <section className="panel-surface panel-wide" data-guide="medications">
      <div className="panel-heading">
        <h2>Current Medications</h2>
        <span>{meds.length}</span>
      </div>
      <div className="med-grid">
        {meds.map((med, index) => {
          const flags = [med.beers_flag, med.flag, med.warning].filter(Boolean);
          const highRisk = Boolean(med.high_risk ?? med.is_high_risk_elderly ?? flags.length);
          return (
            <article className={`med-card ${highRisk ? "high-risk" : ""}`} key={`${shortValue(med.drug)}-${index}`}>
              <div className="med-card-header">
                <strong>{shortValue(med.drug ?? med.drug_id ?? med.name)}</strong>
                {highRisk && <span>High Risk</span>}
              </div>
              <p>{shortValue(med.indication ?? med.class_name ?? med.atc_class)}</p>
              <div className="med-meta">
                <span>{shortValue(med.dose_bucket ?? med.dose_mg ?? med.dose)}</span>
                <span>{shortValue(med.requires_taper ? "taper" : med.monitoring ?? med.route)}</span>
              </div>
            </article>
          );
        })}
        {meds.length === 0 && <p className="muted">No medications loaded.</p>}
      </div>
    </section>
  );
}

function RewardBars({ rewardBreakdown, reward }: { rewardBreakdown: Record<string, unknown> | null; reward: number | null }) {
  const source = rewardBreakdown ?? { total_reward: reward };
  return (
    <section className="panel-surface panel-scroll" data-guide="rewards">
      <div className="panel-heading">
        <h2>Reward Channels</h2>
        <span>{formatReward(source.total_reward ?? reward)}</span>
      </div>
      <div className="reward-bars">
        {REWARD_KEYS.map((key) => {
          const value = toNumber(source[key]);
          const width = Math.max(0.5, Math.min(value ?? 0, 0.999) * 100);
          return (
            <div className="reward-row" key={key}>
              <span>{humanize(key)}</span>
              <div className="reward-track">
                <div className="reward-fill" style={{ width: `${width}%` }} />
              </div>
              <strong>{formatReward(value)}</strong>
            </div>
          );
        })}
      </div>
    </section>
  );
}

function ModelTruthPanel({ status }: { status: ModelStatus | null }) {
  const signal = modelSignal(status);
  const availability = status?.availability ?? {};
  const availabilityRows = Object.entries(availability);
  return (
    <section className={`model-truth panel-surface ${signal.isLive ? "verified" : "unverified"}`} data-guide="model">
      <div className="panel-heading">
        <h2>Model Truth</h2>
        <span>{signal.label}</span>
      </div>
      <p>{signal.detail}</p>
      <div className="model-truth-grid">
        <div>
          <span>Model</span>
          <strong>{shortValue(status?.model_id ?? status?.base_model ?? "unavailable")}</strong>
        </div>
        <div>
          <span>Run</span>
          <strong>{shortValue(status?.run_id)}</strong>
        </div>
        <div>
          <span>Artifact</span>
          <strong>{shortValue(status?.loaded_source || status?.preferred_artifact)}</strong>
        </div>
        <div>
          <span>Availability</span>
          <strong>
            {availabilityRows.length
              ? availabilityRows.map(([key, value]) => `${humanize(key)}:${value ? "yes" : "no"}`).join(" | ")
              : "-"}
          </strong>
        </div>
      </div>
    </section>
  );
}

function HistoryPanel({ observation }: { observation: EnvObservation | null }) {
  const history = observation?.action_history ?? [];
  const warnings = observation?.warning_summary ?? [];
  return (
    <section className="panel-surface panel-wide" data-guide="history">
      <div className="history-grid">
        <div>
          <div className="panel-heading inline-heading">
            <h2>Action History</h2>
            <span>{history.length}</span>
          </div>
          <div className="history-list">
            {history.map((item, index) => {
              const action = isRecord(item.action) ? item.action : item;
              return (
                <div className="history-item" key={`${index}-${shortValue(item.step ?? index)}`}>
                  <strong>
                    Step {shortValue(item.step ?? index)} - {humanize(shortValue(action.action_type ?? "action"))}
                  </strong>
                  <span>{shortValue(action.candidate_id ?? action.target_drug ?? item.reward)}</span>
                </div>
              );
            })}
            {history.length === 0 && <p className="muted">No actions yet.</p>}
          </div>
        </div>
        <div>
          <div className="panel-heading inline-heading">
            <h2>Warnings</h2>
            <span>{warnings.length}</span>
          </div>
          <div className="history-list">
            {warnings.map((warning, index) => (
              <div className="history-item warning" key={`${warning}-${index}`}>
                {warning}
              </div>
            ))}
            {warnings.length === 0 && <p className="muted">No active warnings.</p>}
          </div>
        </div>
      </div>
    </section>
  );
}

function DetailPanel({
  title,
  data,
}: {
  title: string;
  data: Record<string, unknown> | unknown[] | null | undefined;
}) {
  const hasData = Array.isArray(data) ? data.length > 0 : isRecord(data) && Object.keys(data).length > 0;
  return (
    <section className="panel-surface detail-panel">
      <div className="panel-heading">
        <h2>{title}</h2>
      </div>
      {hasData ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p className="muted">No data.</p>}
    </section>
  );
}

function EventLog({ events, error }: { events: string[]; error: string | null }) {
  return (
    <section className="panel-surface panel-wide event-panel" data-guide="event-log">
      <div className="panel-heading">
        <h2>Event Log</h2>
        <span>{events.length}</span>
      </div>
      {error && <div className="error-banner">{error}</div>}
      <div className="event-log">
        {events.map((line, index) => (
          <div key={`${line}-${index}`}>{line}</div>
        ))}
        {events.length === 0 && <p className="muted">Events will appear here.</p>}
      </div>
    </section>
  );
}

export default function App() {
  const [mode, setMode] = useState<WorkbenchMode>("agent");
  const [catalog, setCatalog] = useState<EnvCatalog>(FALLBACK_CATALOG);
  const [taskId, setTaskId] = useState("budgeted_screening");
  const [difficulty, setDifficulty] = useState("medium");
  const [subEnvironment, setSubEnvironment] = useState("REGIMEN_RISK");
  const [agentObservation, setAgentObservation] = useState<EnvObservation | null>(null);
  const [envObservation, setEnvObservation] = useState<EnvObservation | null>(null);
  const [agentReward, setAgentReward] = useState<number | null>(null);
  const [envReward, setEnvReward] = useState<number | null>(null);
  const [agentDone, setAgentDone] = useState(false);
  const [envDone, setEnvDone] = useState(false);
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [confidence, setConfidence] = useState(0.75);
  const [rationale, setRationale] = useState("Selected from the interactive workbench.");
  const [rewardBreakdown, setRewardBreakdown] = useState<Record<string, unknown> | null>(null);
  const [agentInfo, setAgentInfo] = useState<Record<string, unknown> | null>(null);
  const [envInfo, setEnvInfo] = useState<Record<string, unknown> | null>(null);
  const [modelStatus, setModelStatus] = useState<ModelStatus | null>(null);
  const [decision, setDecision] = useState<Record<string, unknown> | null>(null);
  const [explanation, setExplanation] = useState<Record<string, unknown> | null>(null);
  const [evidence, setEvidence] = useState<unknown>(null);
  const [events, setEvents] = useState<string[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [tipsOpen, setTipsOpen] = useState(() => {
    try {
      return window.localStorage.getItem(QTIPS_SEEN_KEY) !== "true";
    } catch {
      return true;
    }
  });
  const [tipStep, setTipStep] = useState(0);

  const refreshModelStatus = useCallback(async () => {
    try {
      const status = await fetchModelStatus();
      setModelStatus(status);
      return status;
    } catch {
      setModelStatus(null);
      return null;
    }
  }, []);

  useEffect(() => {
    fetchCatalog().then(setCatalog).catch(() => setCatalog(FALLBACK_CATALOG));
    refreshModelStatus().then((status) => {
      if (!status) appendEvent(setEvents, "Model status endpoint unavailable; Qwen cannot be verified yet.");
    });
    return () => closeEnvSocket();
  }, [refreshModelStatus]);

  const activeObservation = mode === "agent" ? agentObservation : envObservation;
  const activeReward = mode === "agent" ? agentReward : envReward;
  const activeDone = mode === "agent" ? agentDone : envDone;
  const candidates = activeObservation?.candidate_action_set ?? [];
  const selected = useMemo(
    () => candidates.find((candidate) => candidate.candidate_id === selectedId) ?? defaultCandidateForMode(candidates, mode),
    [candidates, mode, selectedId],
  );
  const statusText = activeDone ? "Complete" : activeObservation ? "Live" : "Ready";
  const activeInfo = mode === "agent" ? agentInfo : envInfo;
  const activeTerminationReason = shortValue(activeInfo?.termination_reason);
  const terminationReason = activeTerminationReason !== "-" ? activeTerminationReason : null;
  const regimenForAltTool = useMemo(() => {
    const meds = activeObservation?.medication_table ?? [];
    const names: string[] = [];
    for (const row of meds) {
      const v = row.drug ?? row.drug_id ?? row.name;
      if (typeof v === "string" && v.trim()) {
        names.push(v.trim());
      }
    }
    return names;
  }, [activeObservation]);

  const heroStats: Array<[string, string]> = [
    ["Runtime", mode === "agent" ? "Agent Workbench" : "Env Explorer"],
    ["Scenario", taskLabel(taskId, catalog.task_presets)],
    ["Candidates", String(candidates.length)],
    ["Reward", formatReward(activeReward)],
  ];
  const closeTips = () => {
    setTipsOpen(false);
    try {
      window.localStorage.setItem(QTIPS_SEEN_KEY, "true");
    } catch {
      // Ignore localStorage failures in private browser contexts.
    }
  };

  const handleTaskChange = (nextTaskId: string) => {
    setTaskId(nextTaskId);
    const preset = catalog.task_presets.find((item) => item.id === nextTaskId);
    if (preset) {
      setDifficulty(preset.difficulty);
      setSubEnvironment(preset.sub_environment);
    }
  };

  const handleModeChange = (nextMode: WorkbenchMode) => {
    if (nextMode === mode) return;
    setMode(nextMode);
    setEvents([]);
    setError(null);
    setSelectedId(null);
    if (nextMode === "agent") {
      setAgentObservation(null);
      setAgentReward(null);
      setAgentDone(false);
      setAgentInfo(null);
      setRewardBreakdown(null);
      setDecision(null);
      setExplanation(null);
      setEvidence(null);
    } else {
      setEnvObservation(null);
      setEnvReward(null);
      setEnvDone(false);
      setEnvInfo(null);
      setRewardBreakdown(null);
    }
  };

  const updateAgentResult = useCallback(async (packet: StepResponse | Record<string, unknown>, source: string) => {
    const normalized = normalizeStepPacket(packet);
    setAgentObservation(normalized.observation);
    setAgentReward(normalized.reward);
    setAgentDone(normalized.done);
    setAgentInfo(normalized.info);
    setDecision((packet.final_action as Record<string, unknown> | undefined) ?? null);
    setExplanation((packet.explanation as Record<string, unknown> | undefined) ?? null);
    setEvidence(packet.evidence);
    const finalAction = isRecord(packet.final_action) ? packet.final_action : null;
    const finalCandidateId = typeof finalAction?.candidate_id === "string" ? finalAction.candidate_id : null;
    const candidatesAfterStep = normalized.observation?.candidate_action_set ?? [];
    setSelectedId(
      finalCandidateId && candidatesAfterStep.some((candidate) => candidate.candidate_id === finalCandidateId)
        ? finalCandidateId
        : defaultCandidateForMode(candidatesAfterStep, "agent")?.candidate_id ?? null,
    );
    const breakdown =
      (normalized.info.reward_breakdown as Record<string, unknown> | undefined) ??
      ((await fetchRewardBreakdown().catch(() => null)) as Record<string, unknown> | null);
    setRewardBreakdown(breakdown ?? null);
    const reason = shortValue(normalized.info.termination_reason);
    appendEvent(
      setEvents,
      `${source} reward ${formatReward(normalized.reward)}${normalized.done && reason !== "-" ? ` - complete: ${reason}` : ""}`,
    );
  }, []);

  const updateEnvResult = useCallback((packet: EnvStepPacket, source: string, submittedCandidateId?: string) => {
    const normalized = normalizeStepPacket(packet);
    const candidatesAfterStep = normalized.observation?.candidate_action_set ?? [];
    setEnvObservation(normalized.observation);
    setEnvReward(normalized.reward);
    setEnvDone(normalized.done);
    setEnvInfo(normalized.info);
    setSelectedId(
      submittedCandidateId && candidatesAfterStep.some((candidate) => candidate.candidate_id === submittedCandidateId)
        ? submittedCandidateId
        : defaultCandidateForMode(candidatesAfterStep, "env")?.candidate_id ?? null,
    );
    const rawBreakdown = normalized.info.reward_breakdown;
    if (isRecord(rawBreakdown) && Object.keys(rawBreakdown).length > 0) {
      setRewardBreakdown(rawBreakdown);
    } else {
      setRewardBreakdown(null);
    }
    const reason = shortValue(normalized.info.termination_reason);
    appendEvent(
      setEvents,
      `${source} reward ${formatReward(normalized.reward)}${normalized.done && reason !== "-" ? ` - complete: ${reason}` : ""}`,
    );
  }, []);

  const handleReset = async () => {
    setLoading(true);
    setError(null);
    setEvents([]);
    try {
      const options = taskResetOptions(taskId, difficulty, subEnvironment, catalog.task_presets);
      if (mode === "agent") {
        await refreshModelStatus();
        const obs = await resetEnv(options.agent);
        setAgentObservation(obs);
        setAgentReward(null);
        setAgentDone(false);
        setAgentInfo(null);
        setRewardBreakdown(null);
        setDecision(null);
        setExplanation(null);
        setEvidence(null);
        setSelectedId(defaultCandidateForMode(obs.candidate_action_set, "agent")?.candidate_id ?? null);
      } else {
        const packet = await envWsSend<EnvStepPacket>("reset", options.env);
        updateEnvResult(packet, "Env reset");
      }
      appendEvent(setEvents, `Reset ${taskLabel(taskId, catalog.task_presets)} in ${mode}`);
    } catch (err) {
      const message = err instanceof Error ? err.message : "Reset failed";
      setError(message);
      appendEvent(setEvents, message);
    } finally {
      setLoading(false);
    }
  };

  const submitSelected = async () => {
    if (!selected) return;
    setLoading(true);
    setError(null);
    try {
      if (mode === "agent") {
        const result = await stepCandidate({
          candidate_id: selected.candidate_id,
          confidence,
          rationale_brief: rationale,
        });
        await updateAgentResult(result, humanize(selected.action_type));
        await refreshModelStatus();
      } else {
        const payload = buildActionPayload(selected, confidence, rationale);
        const packet = await envWsSend<EnvStepPacket>("step", payload);
        updateEnvResult(packet, humanize(selected.action_type), selected.candidate_id);
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : "Step failed";
      setError(message);
      appendEvent(setEvents, message);
    } finally {
      setLoading(false);
    }
  };

  const runAgent = async () => {
    setLoading(true);
    setError(null);
    try {
      const result = await orchestrateStep();
      await updateAgentResult(result, "Agent");
      await refreshModelStatus();
    } catch (err) {
      const message = err instanceof Error ? err.message : "Agent run failed";
      setError(message);
      appendEvent(setEvents, message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="workbench-shell">
      <MetaverseBackdrop />
      <div className="workbench-container">
        <section className="metaverse-hero panel-surface">
          <div className="hero-copy">
            <div className="welcome-box">
              <span className="spark-glyph">*</span>
              <span className="welcome-text">PolyGuard neural safety cockpit</span>
            </div>
            <h2>
              Clinical medication safety, guided by
              <span> constrained RL decisions.</span>
            </h2>
            <p>
              PolyGuard coordinates live OpenEnv episodes, candidate actions, reward channels, and evidence-grounded
              policy traces for safer polypharmacy review.
            </p>
          </div>
          <div className="hero-stat-grid" aria-label="Current workbench state">
            {heroStats.map(([label, value]) => (
              <div key={label}>
                <span>{label}</span>
                <strong>{value}</strong>
              </div>
            ))}
          </div>
        </section>
        <TopBar
          mode={mode}
          setMode={handleModeChange}
          taskId={taskId}
          onTaskChange={handleTaskChange}
          catalog={catalog}
          statusText={statusText}
          modelStatus={modelStatus}
          loading={loading}
          onReset={handleReset}
          onOpenTips={() => {
            setTipStep(0);
            setTipsOpen(true);
          }}
        />
        <ModelTruthPanel status={modelStatus} />

        {taskId === "advanced" && (
          <section className="advanced-strip panel-surface">
            <label className="field">
              <span>Difficulty</span>
              <select value={difficulty} onChange={(event) => setDifficulty(event.target.value)}>
                <option value="easy">easy</option>
                <option value="medium">medium</option>
                <option value="hard">hard</option>
              </select>
            </label>
            <label className="field">
              <span>Environment</span>
              <select value={subEnvironment} onChange={(event) => setSubEnvironment(event.target.value)}>
                {catalog.sub_environments.map((item) => (
                  <option key={item} value={item}>
                    {item}
                  </option>
                ))}
              </select>
            </label>
          </section>
        )}

        <main className="workbench-layout">
          <EpisodeOverview
            mode={mode}
            observation={activeObservation}
            reward={activeReward}
            done={activeDone}
            taskId={taskId}
            catalog={catalog}
          />
          <CandidatePanel candidates={candidates} selected={selected} onSelect={setSelectedId} />
          <ActionConsole
            mode={mode}
            selected={selected}
            confidence={confidence}
            rationale={rationale}
            loading={loading}
          canSubmit={Boolean(selected && selected.legality_precheck !== false && activeObservation && !activeDone)}
          canRunAgent={Boolean(mode === "agent" && activeObservation && !activeDone)}
          done={activeDone}
          terminationReason={terminationReason}
            onConfidence={setConfidence}
            onRationale={setRationale}
            onSubmit={submitSelected}
            onAgent={runAgent}
          onReset={handleReset}
          />
          <RewardBars rewardBreakdown={rewardBreakdown} reward={activeReward} />
          <MedicationCards meds={activeObservation?.medication_table ?? []} />
          <HistoryPanel observation={activeObservation} />
          <DetailPanel title="Decision" data={mode === "agent" ? decision : null} />
          <DetailPanel title="Explanation" data={mode === "agent" ? explanation : null} />
          <DetailPanel title="Evidence" data={mode === "agent" ? (isRecord(evidence) || Array.isArray(evidence) ? evidence : null) : null} />
          <EventLog events={events} error={error} />
          <AlternativeMedicineSearch regimenDrugNames={regimenForAltTool} />
        </main>
        <QTips
          open={tipsOpen}
          step={tipStep}
          steps={GUIDE_STEPS}
          onNext={() => setTipStep((step) => Math.min(step + 1, GUIDE_STEPS.length - 1))}
          onPrev={() => setTipStep((step) => Math.max(step - 1, 0))}
          onClose={closeTips}
        />
      </div>
    </div>
  );
}
