import { useEffect, useMemo, useRef, useState } from "react";
import {
  Award,
  CheckCircle2,
  Eye,
  GripVertical,
  Lightbulb,
  Loader2,
  RotateCcw,
  Save,
  XCircle,
} from "lucide-react";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { cn } from "@/lib/utils";
import { coursePlanService } from "@/services/coursePlan.service";
import type {
  CoursePlanMaterial,
  GapFillExercise,
  MatchPairsExercise,
  MultipleChoiceExercise,
  ReorderWordsExercise,
  ShortAnswerExercise,
  TransformationExercise,
  WorkbookAttempt,
  WorkbookAttemptScoreItem,
  WorkbookBody,
  WorkbookExercise,
} from "@/types";

/**
 * Workbook renderer for `material_type === "interactive_workbook"`.
 *
 * Modes:
 * - `student`  → answers persist server-side (graded, scored, saved).
 * - `preview`  → no persistence, no Submit; "Show answers" toggle reveals
 *   the key so the admin can verify the v2 generator's output.
 *
 * The renderer also accepts a `WorkbookBody` directly (for skill bodies
 * that embed `interactive_workbook: { exercises: [...] }`), so it can be
 * reused inside other material renderers without duplicating the logic.
 */
export type WorkbookMode = "student" | "preview";

interface InteractiveWorkbookProps {
  material: Pick<
    CoursePlanMaterial,
    "id" | "plan_id" | "body" | "title" | "summary" | "material_type"
  >;
  /** Optional override — when present we read exercises from here instead
   *  of `material.body`. Used by skill bodies that embed a workbook one
   *  level deep. */
  bodyOverride?: WorkbookBody;
  mode?: WorkbookMode;
}

type AnswerValue = string | string[] | number[][] | undefined;

function toExercises(body: unknown): WorkbookExercise[] {
  if (!body || typeof body !== "object") return [];
  const b = body as Record<string, unknown>;
  if (Array.isArray(b.exercises)) {
    return b.exercises as WorkbookExercise[];
  }
  // Skill bodies sometimes nest the workbook under `interactive_workbook`.
  const nested = b.interactive_workbook as { exercises?: WorkbookExercise[] } | undefined;
  if (nested && Array.isArray(nested.exercises)) {
    return nested.exercises;
  }
  return [];
}

function toLessonPlan(body: unknown): WorkbookBody["lesson_plan"] | undefined {
  if (!body || typeof body !== "object") return undefined;
  const b = body as Record<string, unknown>;
  if (b.lesson_plan && typeof b.lesson_plan === "object") {
    return b.lesson_plan as WorkbookBody["lesson_plan"];
  }
  const nested = b.interactive_workbook as { lesson_plan?: WorkbookBody["lesson_plan"] } | undefined;
  return nested?.lesson_plan;
}

// Local copies of the backend grading functions for instant per-exercise
// feedback while typing. The authoritative score still comes from the
// server when we POST — these are only used for the "Check" button.
function normText(s: unknown): string {
  if (s == null) return "";
  return String(s).replace(/\s+/g, " ").trim().toLowerCase();
}
function normPunct(s: unknown): string {
  return normText(s).replace(/[\s.?!,]+$/u, "");
}
function globMatch(pattern: string, value: string): boolean {
  const pat = normText(pattern);
  const val = normText(value);
  if (!pat) return false;
  if (!pat.includes("*")) return pat === val;
  const rx = new RegExp(
    "^" + pat.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*") + "$",
  );
  return rx.test(val);
}

function gradeOne(ex: WorkbookExercise, given: AnswerValue): boolean {
  if (given === undefined) return false;
  switch (ex.type) {
    case "gap_fill": {
      const accepted = [ex.answer, ...(ex.alt ?? [])];
      const n = normText(given as string);
      return accepted.some((a) => normText(a) === n);
    }
    case "multiple_choice": {
      const opts = ex.options ?? [];
      const ans = ex.answer;
      const g = normText(given as string);
      if (g === normText(ans)) return true;
      if (typeof ans === "string" && ans.length === 1 && /[a-z]/i.test(ans)) {
        const idx = ans.toUpperCase().charCodeAt(0) - 65;
        if (opts[idx] && normText(opts[idx]) === g) return true;
      }
      if (typeof given === "string" && given.length === 1 && /[a-z]/i.test(given)) {
        const idx = given.toUpperCase().charCodeAt(0) - 65;
        if (opts[idx] && normText(opts[idx]) === normText(ans)) return true;
      }
      return false;
    }
    case "match_pairs": {
      const pairs = (given as number[][]) ?? [];
      const expected = ex.answer ?? [];
      const setOf = (rows: number[][]) =>
        new Set(rows.filter((p) => p.length === 2).map((p) => `${p[0]},${p[1]}`));
      const a = setOf(expected);
      const b = setOf(pairs);
      if (a.size !== b.size) return false;
      for (const v of a) if (!b.has(v)) return false;
      return true;
    }
    case "reorder_words": {
      const value = Array.isArray(given) ? (given as string[]).join(" ") : String(given ?? "");
      return normText(value) === normText(ex.answer);
    }
    case "transformation": {
      const accepted = [ex.answer, ...(ex.alt ?? [])];
      const n = normPunct(given as string);
      return accepted.some((a) => normPunct(a) === n);
    }
    case "short_answer": {
      const accepted = [...(ex.accepted ?? []), ex.answer].filter(Boolean) as string[];
      return accepted.some((p) => globMatch(p, given as string));
    }
    default:
      return false;
  }
}

// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------

export default function InteractiveWorkbook({
  material,
  bodyOverride,
  mode = "student",
}: InteractiveWorkbookProps) {
  const exercises = useMemo<WorkbookExercise[]>(
    () => toExercises(bodyOverride ?? material.body),
    [bodyOverride, material.body],
  );
  const lessonPlan = useMemo(
    () => toLessonPlan(bodyOverride ?? material.body),
    [bodyOverride, material.body],
  );

  const { toast } = useToast();
  const [answers, setAnswers] = useState<Record<string, AnswerValue>>({});
  const [revealedIds, setRevealedIds] = useState<Set<string>>(new Set());
  const [showKey, setShowKey] = useState(false);
  const [serverScore, setServerScore] = useState<{
    correct: number;
    total: number;
    percent: number;
    items?: WorkbookAttemptScoreItem[];
    is_final?: boolean;
  } | null>(null);
  const [saving, setSaving] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [loading, setLoading] = useState(mode === "student");

  // Load existing attempt (student mode only).
  useEffect(() => {
    if (mode !== "student") return;
    let cancelled = false;
    (async () => {
      try {
        const res = await coursePlanService.myWorkbookAttempt(
          material.plan_id,
          material.id,
        );
        if (cancelled) return;
        const attempt = res.data;
        if (attempt) {
          setAnswers((attempt.answers ?? {}) as Record<string, AnswerValue>);
          if (attempt.score && "items" in attempt.score) {
            setServerScore({
              correct: attempt.correct_count,
              total: attempt.total_count,
              percent: attempt.percent,
              items: attempt.score.items,
              is_final: attempt.is_final,
            });
          }
        }
      } catch {
        // Non-fatal — empty workbook on first open.
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [material.id, material.plan_id, mode]);

  const isFinal = !!serverScore?.is_final;
  const liveCorrect = useMemo(
    () => exercises.filter((ex) => gradeOne(ex, answers[ex.id])).length,
    [answers, exercises],
  );
  const livePercent = exercises.length
    ? Math.round((liveCorrect / exercises.length) * 100)
    : 0;

  const onChange = (id: string, value: AnswerValue) => {
    setAnswers((prev) => ({ ...prev, [id]: value }));
  };

  const onCheck = (id: string) => {
    setRevealedIds((prev) => new Set(prev).add(id));
    if (mode === "student") void persist(false);
  };

  const persist = async (finalize: boolean) => {
    if (mode !== "student") return;
    if (finalize) setSubmitting(true);
    else setSaving(true);
    try {
      const res = await coursePlanService.saveWorkbookAttempt(
        material.plan_id,
        material.id,
        { answers, finalize },
      );
      const attempt: WorkbookAttempt = res.data;
      setServerScore({
        correct: attempt.correct_count,
        total: attempt.total_count,
        percent: attempt.percent,
        items: attempt.score && "items" in attempt.score ? attempt.score.items : [],
        is_final: attempt.is_final,
      });
      if (finalize) {
        toast({
          title: "Submitted",
          description: `${attempt.correct_count} / ${attempt.total_count} correct (${attempt.percent.toFixed(0)}%)`,
        });
      }
    } catch (err) {
      toast({
        title: finalize ? "Submit failed" : "Save failed",
        description: err instanceof Error ? err.message : String(err),
        variant: "destructive",
      });
    } finally {
      setSaving(false);
      setSubmitting(false);
    }
  };

  // Debounce per-keystroke saves: every 1.5s after the last change.
  const saveTimer = useRef<number | null>(null);
  useEffect(() => {
    if (mode !== "student" || isFinal || loading) return;
    if (saveTimer.current) window.clearTimeout(saveTimer.current);
    saveTimer.current = window.setTimeout(() => {
      void persist(false);
    }, 1500);
    return () => {
      if (saveTimer.current) window.clearTimeout(saveTimer.current);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [answers]);

  const reset = () => {
    setAnswers({});
    setRevealedIds(new Set());
  };

  if (!exercises.length) {
    return (
      <div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
        This workbook has no exercises yet — generate or extract first.
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {mode === "preview" && (
        <div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-2 text-sm text-amber-900 flex items-center justify-between">
          <span>
            <strong>Preview</strong> — answers won't be saved. Toggle the
            answer key to verify the generated content.
          </span>
          <Button
            type="button"
            size="sm"
            variant={showKey ? "default" : "outline"}
            onClick={() => setShowKey((v) => !v)}
          >
            <Eye className="mr-1 h-4 w-4" />
            {showKey ? "Hide answers" : "Show answers"}
          </Button>
        </div>
      )}

      {lessonPlan && Object.values(lessonPlan).some(Boolean) && (
        <details className="rounded-lg border bg-background/70 p-3">
          <summary className="cursor-pointer text-sm font-medium">
            Teacher's lesson plan
          </summary>
          <div className="mt-2 grid gap-2 sm:grid-cols-2 text-sm">
            {(["warmup", "presentation", "controlled_practice", "freer_practice", "exit_ticket"] as const).map(
              (k) =>
                lessonPlan[k] ? (
                  <div key={k}>
                    <div className="text-xs uppercase tracking-wide text-muted-foreground">
                      {k.replace(/_/g, " ")}
                    </div>
                    <div className="leading-6">{lessonPlan[k]}</div>
                  </div>
                ) : null,
            )}
          </div>
        </details>
      )}

      <div className="rounded-lg border bg-background/70 p-4">
        <div className="flex items-center justify-between mb-3">
          <div className="flex items-center gap-2 text-sm">
            <Award className="h-4 w-4 text-primary" />
            <strong>{liveCorrect}</strong>
            <span className="text-muted-foreground">/ {exercises.length} correct (live)</span>
            {serverScore && (
              <Badge variant="secondary" className="ml-2">
                Server: {serverScore.correct}/{serverScore.total} ({serverScore.percent.toFixed(0)}%)
              </Badge>
            )}
          </div>
          {mode === "student" && (
            <div className="text-xs text-muted-foreground flex items-center gap-2">
              {saving ? (
                <>
                  <Loader2 className="h-3 w-3 animate-spin" /> Saving…
                </>
              ) : (
                <>
                  <Save className="h-3 w-3" />{" "}
                  {serverScore ? "Auto-saved" : "Auto-save on"}
                </>
              )}
            </div>
          )}
        </div>
        <Progress value={livePercent} className="h-2" />
      </div>

      <ol className="space-y-3 list-none p-0">
        {exercises.map((ex, idx) => {
          const value = answers[ex.id];
          const checked = revealedIds.has(ex.id) || isFinal || showKey;
          const correct = checked ? gradeOne(ex, value) : null;
          return (
            <li
              key={ex.id || idx}
              className={cn(
                "rounded-lg border p-4 bg-background/70 space-y-3",
                checked && correct === true && "border-emerald-400 bg-emerald-50/40",
                checked && correct === false && "border-rose-400 bg-rose-50/40",
              )}
            >
              <div className="flex items-start justify-between gap-3">
                <div className="text-xs uppercase tracking-wide text-muted-foreground">
                  Exercise {idx + 1} · {ex.type.replace(/_/g, " ")}
                </div>
                {checked && (
                  <Badge variant={correct ? "default" : "destructive"} className="capitalize">
                    {correct ? (
                      <>
                        <CheckCircle2 className="mr-1 h-3 w-3" /> Correct
                      </>
                    ) : (
                      <>
                        <XCircle className="mr-1 h-3 w-3" /> Try again
                      </>
                    )}
                  </Badge>
                )}
              </div>

              {renderExercise(ex, value, (v) => onChange(ex.id, v), {
                disabled: mode === "preview" || isFinal,
                showKey: showKey,
              })}

              {ex.hint && !checked && (
                <div className="text-xs text-muted-foreground flex items-center gap-1">
                  <Lightbulb className="h-3 w-3" /> {ex.hint}
                </div>
              )}

              {checked && (
                <div className="text-xs text-muted-foreground space-y-1">
                  {!correct && (
                    <div>
                      Expected: <span className="font-medium">{formatAnswer(ex)}</span>
                    </div>
                  )}
                  {ex.rationale && <div>{ex.rationale}</div>}
                </div>
              )}

              {!isFinal && mode !== "preview" && (
                <div className="flex justify-end">
                  <Button
                    type="button"
                    size="sm"
                    variant="ghost"
                    onClick={() => onCheck(ex.id)}
                  >
                    <CheckCircle2 className="mr-1 h-4 w-4" /> Check
                  </Button>
                </div>
              )}
            </li>
          );
        })}
      </ol>

      {mode === "student" && (
        <div className="flex items-center justify-between rounded-lg border bg-background/70 p-3">
          <Button type="button" variant="ghost" onClick={reset} disabled={isFinal}>
            <RotateCcw className="mr-1 h-4 w-4" />
            Reset
          </Button>
          <Button
            type="button"
            disabled={submitting || isFinal}
            onClick={() => persist(true)}
          >
            {submitting ? (
              <Loader2 className="mr-1 h-4 w-4 animate-spin" />
            ) : (
              <Save className="mr-1 h-4 w-4" />
            )}
            {isFinal ? "Submitted" : "Submit final"}
          </Button>
        </div>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// Per-type renderers
// ---------------------------------------------------------------------------

function renderExercise(
  ex: WorkbookExercise,
  value: AnswerValue,
  onChange: (v: AnswerValue) => void,
  opts: { disabled?: boolean; showKey?: boolean },
): React.ReactNode {
  switch (ex.type) {
    case "gap_fill":
      return <GapFill ex={ex} value={value as string} onChange={onChange} disabled={opts.disabled} />;
    case "multiple_choice":
      return (
        <MultipleChoice
          ex={ex}
          value={value as string}
          onChange={onChange}
          disabled={opts.disabled}
        />
      );
    case "match_pairs":
      return (
        <MatchPairs
          ex={ex}
          value={value as number[][]}
          onChange={onChange}
          disabled={opts.disabled}
        />
      );
    case "reorder_words":
      return (
        <ReorderWords
          ex={ex}
          value={value as string[]}
          onChange={onChange}
          disabled={opts.disabled}
        />
      );
    case "transformation":
      return (
        <Transformation
          ex={ex}
          value={value as string}
          onChange={onChange}
          disabled={opts.disabled}
        />
      );
    case "short_answer":
      return (
        <ShortAnswer
          ex={ex}
          value={value as string}
          onChange={onChange}
          disabled={opts.disabled}
        />
      );
    default:
      return <p className="text-sm text-muted-foreground">Unsupported exercise type.</p>;
  }
}

function formatAnswer(ex: WorkbookExercise): string {
  switch (ex.type) {
    case "match_pairs":
      return (ex.answer ?? [])
        .map((p) => `${ex.left[p[0]] ?? "?"} ↔ ${ex.right[p[1]] ?? "?"}`)
        .join(", ");
    case "reorder_words":
      return ex.answer;
    default:
      return String((ex as { answer?: unknown }).answer ?? "");
  }
}

// ---------------------------------------------------------------------------
// Type-specific input subcomponents
// ---------------------------------------------------------------------------

function GapFill({
  ex,
  value,
  onChange,
  disabled,
}: {
  ex: GapFillExercise;
  value: string | undefined;
  onChange: (v: string) => void;
  disabled?: boolean;
}) {
  // Replace ___ in the stem with an inline input. Falls back to a stem
  // label + standalone input when there's no underscore marker.
  const parts = ex.stem.split(/_{2,}/);
  if (parts.length === 1) {
    return (
      <div className="space-y-2">
        <p className="leading-7">{ex.stem}</p>
        <Input
          value={value ?? ""}
          onChange={(e) => onChange(e.target.value)}
          disabled={disabled}
          placeholder="Type your answer"
        />
      </div>
    );
  }
  return (
    <p className="leading-8 text-base flex flex-wrap items-center gap-1">
      {parts.map((part, i) => (
        <span key={i} className="inline-flex items-center gap-1">
          <span>{part}</span>
          {i < parts.length - 1 && (
            <Input
              type="text"
              className="inline-block w-32 align-baseline"
              value={value ?? ""}
              onChange={(e) => onChange(e.target.value)}
              disabled={disabled}
              placeholder="…"
            />
          )}
        </span>
      ))}
    </p>
  );
}

function MultipleChoice({
  ex,
  value,
  onChange,
  disabled,
}: {
  ex: MultipleChoiceExercise;
  value: string | undefined;
  onChange: (v: string) => void;
  disabled?: boolean;
}) {
  return (
    <div className="space-y-2">
      <p className="leading-7">{ex.stem}</p>
      <RadioGroup
        value={value ?? ""}
        onValueChange={onChange}
        disabled={disabled}
        className="grid gap-2 sm:grid-cols-2"
      >
        {ex.options.map((opt, i) => {
          const id = `${ex.id}_opt_${i}`;
          return (
            <Label
              key={id}
              htmlFor={id}
              className="flex items-start gap-2 rounded-md border p-2 cursor-pointer hover:bg-muted"
            >
              <RadioGroupItem id={id} value={opt} className="mt-0.5" />
              <span className="text-sm">{opt}</span>
            </Label>
          );
        })}
      </RadioGroup>
    </div>
  );
}

function MatchPairs({
  ex,
  value,
  onChange,
  disabled,
}: {
  ex: MatchPairsExercise;
  value: number[][] | undefined;
  onChange: (v: number[][]) => void;
  disabled?: boolean;
}) {
  const [selectedLeft, setSelectedLeft] = useState<number | null>(null);
  const pairs = value ?? [];
  const pairedLeft = new Set(pairs.map((p) => p[0]));
  const pairedRight = new Set(pairs.map((p) => p[1]));

  const tapLeft = (idx: number) => {
    if (disabled) return;
    setSelectedLeft(idx);
  };
  const tapRight = (idx: number) => {
    if (disabled || selectedLeft == null) return;
    const next = pairs.filter((p) => p[0] !== selectedLeft && p[1] !== idx);
    next.push([selectedLeft, idx]);
    onChange(next);
    setSelectedLeft(null);
  };
  const clear = (li: number) => {
    if (disabled) return;
    onChange(pairs.filter((p) => p[0] !== li));
  };

  return (
    <div className="grid grid-cols-2 gap-3 text-sm">
      <div className="space-y-1">
        <div className="text-xs uppercase tracking-wide text-muted-foreground">Match these…</div>
        {ex.left.map((item, i) => {
          const paired = pairs.find((p) => p[0] === i);
          return (
            <button
              type="button"
              key={`L${i}`}
              onClick={() => (paired ? clear(i) : tapLeft(i))}
              disabled={disabled}
              className={cn(
                "block w-full rounded border px-2 py-1.5 text-left",
                selectedLeft === i && "border-primary ring-1 ring-primary",
                paired && "bg-muted",
              )}
            >
              <span className="text-muted-foreground mr-1">{i + 1}.</span>
              {item}
              {paired ? (
                <span className="ml-2 text-xs text-muted-foreground">
                  → {ex.right[paired[1]]}
                </span>
              ) : null}
            </button>
          );
        })}
      </div>
      <div className="space-y-1">
        <div className="text-xs uppercase tracking-wide text-muted-foreground">…with these</div>
        {ex.right.map((item, j) => (
          <button
            type="button"
            key={`R${j}`}
            onClick={() => tapRight(j)}
            disabled={disabled || selectedLeft == null}
            className={cn(
              "block w-full rounded border px-2 py-1.5 text-left",
              pairedRight.has(j) && "bg-muted",
            )}
          >
            <span className="text-muted-foreground mr-1">{String.fromCharCode(65 + j)}.</span>
            {item}
          </button>
        ))}
      </div>
      <div className="col-span-2 text-xs text-muted-foreground">
        Tap a left item, then tap its match on the right. Tap a paired left
        item to clear it.
      </div>
      {!pairedLeft.size && (
        <div className="col-span-2 text-xs text-muted-foreground">
          {ex.left.length} pair(s) to match.
        </div>
      )}
    </div>
  );
}

function ReorderWords({
  ex,
  value,
  onChange,
  disabled,
}: {
  ex: ReorderWordsExercise;
  value: string[] | undefined;
  onChange: (v: string[]) => void;
  disabled?: boolean;
}) {
  // Stateful tap-to-insert: tap a token in the bank to append; tap a
  // token in the answer strip to remove. Keeps it touch-friendly.
  const order = value ?? [];
  const remaining: { tok: string; bankIdx: number }[] = [];
  const used: number[] = [];
  // Build maps so duplicate tokens stay independently selectable.
  const usedCounts = new Map<string, number>();
  order.forEach((t) => usedCounts.set(t, (usedCounts.get(t) ?? 0) + 1));
  const seenWhileBuilding = new Map<string, number>();
  ex.tokens.forEach((tok, idx) => {
    const seen = seenWhileBuilding.get(tok) ?? 0;
    const usesLeft = (usedCounts.get(tok) ?? 0) - seen;
    if (usesLeft > 0) {
      used.push(idx);
      seenWhileBuilding.set(tok, seen + 1);
    } else {
      remaining.push({ tok, bankIdx: idx });
    }
  });

  const append = (tok: string) => {
    if (disabled) return;
    onChange([...order, tok]);
  };
  const removeAt = (i: number) => {
    if (disabled) return;
    onChange(order.filter((_, idx) => idx !== i));
  };

  return (
    <div className="space-y-2 text-sm">
      <div className="rounded border bg-muted/30 p-2 min-h-10 flex flex-wrap gap-1">
        {order.length === 0 ? (
          <span className="text-xs text-muted-foreground">Tap tokens below to build the sentence…</span>
        ) : (
          order.map((tok, i) => (
            <button
              type="button"
              key={`a${i}`}
              onClick={() => removeAt(i)}
              disabled={disabled}
              className="rounded bg-background border px-2 py-1"
            >
              <GripVertical className="mr-1 inline h-3 w-3 text-muted-foreground" />
              {tok}
            </button>
          ))
        )}
      </div>
      <div className="flex flex-wrap gap-1">
        {remaining.map(({ tok, bankIdx }) => (
          <button
            type="button"
            key={`b${bankIdx}`}
            onClick={() => append(tok)}
            disabled={disabled}
            className="rounded border px-2 py-1 hover:bg-muted"
          >
            {tok}
          </button>
        ))}
      </div>
    </div>
  );
}

function Transformation({
  ex,
  value,
  onChange,
  disabled,
}: {
  ex: TransformationExercise;
  value: string | undefined;
  onChange: (v: string) => void;
  disabled?: boolean;
}) {
  return (
    <div className="space-y-2">
      <div className="rounded border bg-muted/30 px-3 py-2 text-sm">
        <Badge variant="outline" className="mr-2">
          {ex.instruction}
        </Badge>
        <span className="font-medium">{ex.from}</span>
      </div>
      <Textarea
        value={value ?? ""}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
        rows={2}
        placeholder="Transformed sentence"
      />
    </div>
  );
}

function ShortAnswer({
  ex,
  value,
  onChange,
  disabled,
}: {
  ex: ShortAnswerExercise;
  value: string | undefined;
  onChange: (v: string) => void;
  disabled?: boolean;
}) {
  return (
    <div className="space-y-2">
      <p className="leading-7">{ex.stem}</p>
      <Textarea
        value={value ?? ""}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
        rows={3}
        placeholder="Type your answer"
      />
    </div>
  );
}
