import { useEffect, useMemo, useRef, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
  ArrowLeft,
  BookOpen,
  Calendar,
  CheckCircle2,
  ClipboardList,
  Database,
  Download,
  Eye,
  EyeOff,
  ExternalLink,
  FileText,
  Headphones,
  Image as ImageIcon,
  Library,
  Link2,
  Loader2,
  MessageSquare,
  Mic,
  Music,
  PenSquare,
  Play,
  Plus,
  RefreshCw,
  Sparkles,
  Trash2,
  Type,
  Upload,
  UserPlus,
  Users,
  Video,
  Wand2,
  type LucideIcon,
} from "lucide-react";

import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Progress } from "@/components/ui/progress";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet";
import { coursePlanService } from "@/services/coursePlan.service";
import { entitiesService } from "@/services/entities.service";
import { lmsService } from "@/services/lms.service";
import { LibraryPickerDialog } from "@/components/coursePlan/LibraryPickerDialog";
import MaterialBookView, {
  SkillBadge,
} from "@/components/coursePlan/MaterialBookView";
import PlanReader, {
  GroundingBadge,
} from "@/components/coursePlan/PlanReader";
import { describeApiError, withAuthQuery } from "@/lib/api-client";
import type {
  CoursePlan,
  CoursePlanAssignment,
  CoursePlanDeliverables,
  CoursePlanMaterial,
  CoursePlanMedia,
  CoursePlanSkill,
  CoursePlanSource,
  CoursePlanWeek,
  Entity,
} from "@/types";

/**
 * Detail page for a single AI-generated course plan.
 *
 * Sections (top → bottom):
 *   1. Header with course metadata + CEFR badge.
 *   2. **Generation progress strip** (Phase B) — planned / generated /
 *      ready counters with a per-week breakdown.
 *   3. **Reference sources card** (Phase A) — upload PDFs, URLs, text;
 *      reindex/delete; status badges.
 *   4. **Assignments card** (Phase D) — assign to a batch or specific
 *      students with a due date / message.
 *   5. Course description, objectives, outcomes, grammar.
 *   6. Weekly delivery plan (accordion) with per-material media drawers
 *      (Phase C: audio / image / video) and a per-week bulk-generate
 *      button.
 */

const SKILL_ICONS: Record<string, LucideIcon> = {
  reading: BookOpen,
  writing: PenSquare,
  listening: Headphones,
  speaking: Mic,
  grammar: Type,
  vocabulary: Library,
  integrated: MessageSquare,
};

export default function AdminCoursePlanDetail() {
  const { t } = useTranslation();
  const params = useParams<{ planId: string }>();
  const planId = Number(params.planId);
  const qc = useQueryClient();

  const planQ = useQuery({
    queryKey: ["course-plan", planId],
    queryFn: () => coursePlanService.get(planId),
    enabled: Number.isFinite(planId) && planId > 0,
  });

  const sourcesQ = useQuery({
    queryKey: ["course-plan", planId, "sources"],
    queryFn: () => coursePlanService.listSources(planId),
    enabled: Number.isFinite(planId) && planId > 0,
    refetchInterval: (q) => {
      // Poll while any source is still indexing.
      const data = q.state.data as { items?: CoursePlanSource[] } | undefined;
      const hasPending = data?.items?.some(
        (s) => s.status === "pending" || s.status === "indexing",
      );
      return hasPending ? 4000 : false;
    },
  });

  const deliverablesQ = useQuery({
    queryKey: ["course-plan", planId, "deliverables"],
    queryFn: () => coursePlanService.deliverables(planId),
    enabled: Number.isFinite(planId) && planId > 0,
  });

  const assignmentsQ = useQuery({
    queryKey: ["course-plan", planId, "assignments"],
    queryFn: () => coursePlanService.listAssignments(planId),
    enabled: Number.isFinite(planId) && planId > 0,
  });

  const plan = planQ.data?.data as CoursePlan | undefined;
  const [studentPreview, setStudentPreview] = useState(false);
  const [gateWeek, setGateWeek] = useState<number | null>(null);

  // True as soon as at least one uploaded source is fully indexed in
  // pgvector. Drives the v2 (RAG-grounded) path on Generate and gates
  // the soft "upload your book first" modal.
  const indexedSources = useMemo(
    () => (sourcesQ.data?.items ?? []).filter((s) => s.status === "indexed"),
    [sourcesQ.data?.items],
  );
  const hasIndexedSources = indexedSources.length > 0;

  const generateV2Mut = useMutation({
    mutationFn: (weekNumber: number) =>
      coursePlanService.generateWeekMaterialsV2(planId, weekNumber),
    onSuccess: (res) => {
      qc.invalidateQueries({ queryKey: ["course-plan", planId] });
      qc.invalidateQueries({ queryKey: ["course-plan", planId, "deliverables"] });
      toast.success(
        t("coursePlan.weekMaterialsGenerated") +
          (res.rag?.sources_used?.length
            ? ` (${t("coursePlan.rag.groundedOn", {
                count: res.rag.sources_used.length,
                defaultValue: "grounded on {{count}} source(s)",
              })})`
            : ""),
      );
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.weekMaterialsFailed"))),
  });

  const generateMut = useMutation({
    mutationFn: (weekNumber: number) =>
      coursePlanService.generateWeekMaterials(planId, weekNumber),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["course-plan", planId] });
      qc.invalidateQueries({ queryKey: ["course-plan", planId, "deliverables"] });
      toast.success(t("coursePlan.weekMaterialsGenerated"));
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.weekMaterialsFailed"))),
  });

  /**
   * Single entry point for the per-week Generate button:
   * - if any source is indexed → call the RAG-grounded v2 endpoint
   * - otherwise → open the upload gate; the admin can still choose
   *   "Generate without RAG" to fall back to the legacy generator
   */
  const requestGenerate = (weekNumber: number) => {
    if (hasIndexedSources) {
      generateV2Mut.mutate(weekNumber);
    } else {
      setGateWeek(weekNumber);
    }
  };

  const generateBusy = (weekNumber: number) =>
    (generateV2Mut.isPending && generateV2Mut.variables === weekNumber) ||
    (generateMut.isPending && generateMut.variables === weekNumber);

  const bulkMediaMut = useMutation({
    mutationFn: (vars: {
      week: number;
      kinds: Array<"audio" | "image" | "video">;
    }) =>
      coursePlanService.generateWeekMedia(planId, vars.week, {
        kinds: vars.kinds,
      }),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["course-plan", planId] });
      qc.invalidateQueries({ queryKey: ["course-plan", planId, "deliverables"] });
      toast.success(t("coursePlan.media.bulkSuccess"));
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.media.bulkFailed"))),
  });

  const materialsByWeek = useMemo(() => {
    const map = new Map<number, CoursePlanMaterial[]>();
    if (!plan?.materials) return map;
    for (const m of plan.materials) {
      const list = map.get(m.week_number) ?? [];
      list.push(m);
      map.set(m.week_number, list);
    }
    return map;
  }, [plan?.materials]);

  return (
    <div className="space-y-6">
      <Button variant="ghost" size="sm" asChild className="-ml-2 text-muted-foreground">
        <Link to="/admin/course-plans" className="flex items-center gap-1">
          <ArrowLeft className="h-4 w-4" />
          {t("coursePlan.backToList")}
        </Link>
      </Button>

      {planQ.isLoading && (
        <div className="space-y-4">
          <Skeleton className="h-20 rounded-lg" />
          <Skeleton className="h-40 rounded-lg" />
          <Skeleton className="h-60 rounded-lg" />
        </div>
      )}

      {planQ.isError && (
        <div className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
          {describeApiError(planQ.error, t("coursePlan.loadFailed"))}
        </div>
      )}

      {plan && (
        <>
          <Card>
            <CardHeader>
              <div className="flex items-start justify-between gap-3 flex-wrap">
                <div className="space-y-1 flex-1 min-w-0">
                  <CardTitle className="text-2xl">{plan.name}</CardTitle>
                  {plan.description && (
                    <CardDescription className="max-w-3xl">
                      {plan.description}
                    </CardDescription>
                  )}
                </div>
                <div className="flex gap-2 flex-wrap">
                  <Button
                    variant={studentPreview ? "default" : "outline"}
                    size="sm"
                    onClick={() => setStudentPreview((v) => !v)}
                  >
                    {studentPreview ? (
                      <EyeOff className="mr-1 h-4 w-4" />
                    ) : (
                      <Eye className="mr-1 h-4 w-4" />
                    )}
                    {studentPreview
                      ? t("coursePlan.preview.exitStudentView", "Exit student view")
                      : t("coursePlan.preview.studentView", "Student view")}
                  </Button>
                  <Badge variant="secondary" className="uppercase">
                    {plan.cefr_level || "—"}
                  </Badge>
                  <Badge variant="outline">
                    {t("coursePlan.weeksCount", { count: plan.total_weeks })}
                  </Badge>
                  <Badge variant="outline">
                    {t("coursePlan.hoursPerWeek", {
                      count: plan.contact_hours_per_week,
                    })}
                  </Badge>
                </div>
              </div>
              {plan.skills_division && !studentPreview && (
                <p className="text-sm text-muted-foreground">
                  {plan.skills_division}
                </p>
              )}
            </CardHeader>
          </Card>

          {studentPreview ? (
            <>
              <div className="rounded-md border border-amber-300/60 bg-amber-50 dark:bg-amber-900/20 px-3 py-2 text-sm text-amber-900 dark:text-amber-100 flex items-center gap-2">
                <Eye className="h-4 w-4" />
                {t(
                  "coursePlan.preview.banner",
                  "Preview — answers won't be saved. Toggle Student view to exit.",
                )}
              </div>
              <PlanReader plan={plan} mode="preview" />
            </>
          ) : (
            <>
          <DeliverablesStrip data={deliverablesQ.data} />

          <SourcesCard
            planId={planId}
            sources={sourcesQ.data?.items ?? []}
            isLoading={sourcesQ.isLoading}
          />

          <AssignmentsCard
            plan={plan}
            assignments={assignmentsQ.data?.items ?? []}
            studentPreview={studentPreview}
          />

          {plan.objectives.length > 0 && (
            <Card>
              <CardHeader>
                <CardTitle className="text-base flex items-center gap-2">
                  <ClipboardList className="h-4 w-4 text-primary" />
                  {t("coursePlan.sections.objectives")}
                </CardTitle>
              </CardHeader>
              <CardContent>
                <ol className="list-decimal list-inside space-y-1 text-sm">
                  {plan.objectives.map((obj, i) => (
                    <li key={i}>{obj}</li>
                  ))}
                </ol>
              </CardContent>
            </Card>
          )}

          {Object.keys(plan.outcomes).length > 0 && (
            <Card>
              <CardHeader>
                <CardTitle className="text-base flex items-center gap-2">
                  <Sparkles className="h-4 w-4 text-primary" />
                  {t("coursePlan.sections.outcomes")}
                </CardTitle>
              </CardHeader>
              <CardContent className="space-y-4">
                {(Object.keys(plan.outcomes) as CoursePlanSkill[]).map(
                  (skill) => {
                    const list = plan.outcomes[skill];
                    if (!list?.length) return null;
                    const Icon = SKILL_ICONS[skill] ?? ClipboardList;
                    return (
                      <div key={skill}>
                        <div className="flex items-center gap-2 mb-2">
                          <Icon className="h-4 w-4" />
                          <div className="font-medium capitalize">
                            {t(`coursePlan.skill.${skill}`, skill)}
                          </div>
                        </div>
                        <ul className="space-y-1 text-sm">
                          {list.map((o) => (
                            <li key={o.code} className="flex gap-2">
                              <Badge
                                variant="outline"
                                className="font-mono text-[10px] shrink-0"
                              >
                                {o.code}
                              </Badge>
                              <span className="flex-1">{o.description}</span>
                            </li>
                          ))}
                        </ul>
                      </div>
                    );
                  },
                )}
              </CardContent>
            </Card>
          )}

          {plan.grammar.length > 0 && (
            <Card>
              <CardHeader>
                <CardTitle className="text-base flex items-center gap-2">
                  <Type className="h-4 w-4 text-primary" />
                  {t("coursePlan.sections.grammar")}
                </CardTitle>
              </CardHeader>
              <CardContent>
                <ul className="space-y-2 text-sm">
                  {plan.grammar.map((g) => (
                    <li key={g.code}>
                      <div className="font-medium">
                        <Badge
                          variant="outline"
                          className="font-mono text-[10px] mr-2"
                        >
                          {g.code}
                        </Badge>
                        {g.label}
                      </div>
                      {g.sub_items?.length ? (
                        <ul className="list-disc list-inside ml-6 mt-1 text-muted-foreground text-xs">
                          {g.sub_items.map((s, i) => (
                            <li key={i}>{s}</li>
                          ))}
                        </ul>
                      ) : null}
                    </li>
                  ))}
                </ul>
              </CardContent>
            </Card>
          )}

          {plan.weeks && plan.weeks.length > 0 && (
            <Card>
              <CardHeader>
                <CardTitle className="text-base flex items-center gap-2">
                  <Calendar className="h-4 w-4 text-primary" />
                  {t("coursePlan.sections.delivery")}
                </CardTitle>
                <CardDescription>{t("coursePlan.deliveryHint")}</CardDescription>
              </CardHeader>
              <CardContent>
                <Accordion type="multiple" className="w-full">
                  {plan.weeks.map((week) => (
                    <WeekAccordionItem
                      key={week.id}
                      week={week}
                      materials={materialsByWeek.get(week.week_number) ?? []}
                      generating={generateBusy(week.week_number)}
                      onGenerate={() => requestGenerate(week.week_number)}
                      onBulkMedia={(kinds) =>
                        bulkMediaMut.mutate({ week: week.week_number, kinds })
                      }
                      bulkBusy={
                        bulkMediaMut.isPending &&
                        bulkMediaMut.variables?.week === week.week_number
                      }
                      studentPreview={studentPreview}
                      hasIndexedSources={hasIndexedSources}
                    />
                  ))}
                </Accordion>
              </CardContent>
            </Card>
          )}
            </>
          )}
        </>
      )}

      <PreGenerationGate
        open={gateWeek !== null}
        onOpenChange={(v) => {
          if (!v) setGateWeek(null);
        }}
        weekNumber={gateWeek}
        onUpload={() => {
          setGateWeek(null);
          // Scroll to the SourcesCard upload area; the admin uploads
          // the PDF, watches it index, then clicks Generate again.
          document
            .querySelector<HTMLElement>(`[data-section="sources-card"]`)
            ?.scrollIntoView({ behavior: "smooth", block: "start" });
        }}
        onContinueWithoutRag={() => {
          if (gateWeek !== null) {
            generateMut.mutate(gateWeek);
            setGateWeek(null);
          }
        }}
      />
    </div>
  );
}

// ---------------------------------------------------------------------------
// Pre-generation upload gate
// ---------------------------------------------------------------------------

/**
 * Soft gate shown before Generate when the plan has no indexed sources.
 * Encourages the admin to upload reference material first so the v2
 * generator can ground exercises on real text. Dismissable: clicking
 * "Generate without RAG" falls back to the legacy v1 endpoint so we
 * never block the workflow on a missing book.
 */
function PreGenerationGate({
  open,
  onOpenChange,
  weekNumber,
  onUpload,
  onContinueWithoutRag,
}: {
  open: boolean;
  onOpenChange: (v: boolean) => void;
  weekNumber: number | null;
  onUpload: () => void;
  onContinueWithoutRag: () => void;
}) {
  const { t } = useTranslation();
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>
            {t("coursePlan.gate.title", "Upload your reference book first?")}
          </DialogTitle>
          <DialogDescription>
            {t(
              "coursePlan.gate.subtitle",
              "Generating week {{n}} works best when grounded on your uploaded PDF / DOCX / link. Upload a reference, wait for indexing, then click Generate again. You can dismiss this and continue without RAG.",
              { n: weekNumber ?? 0 },
            )}
          </DialogDescription>
        </DialogHeader>
        <div className="text-xs text-muted-foreground">
          {t(
            "coursePlan.gate.hint",
            "Tip: a typical course book takes 10-30 seconds to index after upload.",
          )}
        </div>
        <DialogFooter className="gap-2 flex-wrap">
          <Button variant="outline" onClick={() => onOpenChange(false)}>
            {t("coursePlan.gate.cancel", "Cancel")}
          </Button>
          <Button variant="ghost" onClick={onContinueWithoutRag}>
            {t("coursePlan.gate.continueWithoutRag", "Generate without RAG")}
          </Button>
          <Button onClick={onUpload}>
            <Upload className="mr-1 h-4 w-4" />
            {t("coursePlan.gate.upload", "Upload now")}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

// ---------------------------------------------------------------------------
// Phase B — Deliverables / progress strip
// ---------------------------------------------------------------------------

function DeliverablesStrip({ data }: { data: CoursePlanDeliverables | undefined }) {
  const { t } = useTranslation();
  if (!data) return null;
  const s = data.summary;
  return (
    <Card>
      <CardHeader className="pb-2">
        <CardTitle className="text-base flex items-center gap-2">
          <CheckCircle2 className="h-4 w-4 text-primary" />
          {t("coursePlan.deliverables.title")}
        </CardTitle>
        <CardDescription>{t("coursePlan.deliverables.subtitle")}</CardDescription>
      </CardHeader>
      <CardContent className="space-y-3">
        <div className="grid gap-3 sm:grid-cols-3">
          <Stat
            label={t("coursePlan.deliverables.summary.planned", { count: s.planned })}
            value={s.planned}
          />
          <Stat
            label={t("coursePlan.deliverables.summary.generated", { count: s.generated })}
            value={s.generated}
          />
          <Stat
            label={t("coursePlan.deliverables.summary.ready", { count: s.ready })}
            value={s.ready}
          />
        </div>
        <Progress value={s.percent_ready} className="h-2" />
        <p className="text-xs text-muted-foreground">
          {t("coursePlan.deliverables.mediaCounts", {
            audio: s.media.audio,
            image: s.media.image,
            video: s.media.video,
          })}
        </p>
      </CardContent>
    </Card>
  );
}

function Stat({ label, value }: { label: string; value: number }) {
  return (
    <div className="rounded-md border bg-muted/20 px-3 py-2">
      <div className="text-2xl font-semibold">{value}</div>
      <div className="text-xs text-muted-foreground">{label}</div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Phase A — Sources card
// ---------------------------------------------------------------------------

function SourcesCard({
  planId,
  sources,
  isLoading,
}: {
  planId: number;
  sources: CoursePlanSource[];
  isLoading: boolean;
}) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const fileRef = useRef<HTMLInputElement>(null);
  const [urlDraft, setUrlDraft] = useState("");
  const [textDraft, setTextDraft] = useState("");
  const [libraryOpen, setLibraryOpen] = useState(false);
  // The set of resource ids that are already attached to this plan.
  // Passed into the picker so already-linked rows render disabled and
  // can't produce duplicate sources on re-attach.
  const linkedResourceIds = useMemo(
    () =>
      new Set(
        sources
          .filter((s) => s.kind === "resource" && s.resource_id)
          .map((s) => s.resource_id as number),
      ),
    [sources],
  );

  const invalidate = () => {
    qc.invalidateQueries({ queryKey: ["course-plan", planId, "sources"] });
    qc.invalidateQueries({ queryKey: ["course-plan", planId, "deliverables"] });
    qc.invalidateQueries({ queryKey: ["course-plan", planId] });
  };

  const uploadFiles = async (files: FileList | null) => {
    if (!files || !files.length) return;
    for (const f of Array.from(files)) {
      try {
        await coursePlanService.uploadSource(planId, f, { name: f.name });
      } catch (err) {
        toast.error(
          describeApiError(err, t("coursePlan.sources.uploadFailed", { name: f.name })),
        );
      }
    }
    invalidate();
  };

  const addUrl = async () => {
    const url = urlDraft.trim();
    if (!url) return;
    try {
      await coursePlanService.createSource(planId, { kind: "url", url, name: url });
      setUrlDraft("");
      invalidate();
    } catch (err) {
      toast.error(describeApiError(err, t("coursePlan.sources.uploadFailed", { name: url })));
    }
  };

  const addText = async () => {
    const text = textDraft.trim();
    if (!text) return;
    try {
      await coursePlanService.createSource(planId, {
        kind: "text",
        inline_text: text,
        name: text.slice(0, 60).replace(/\s+/g, " "),
      });
      setTextDraft("");
      invalidate();
    } catch (err) {
      toast.error(
        describeApiError(err, t("coursePlan.sources.uploadFailed", { name: "text" })),
      );
    }
  };

  const reindexMut = useMutation({
    mutationFn: (sourceId: number) =>
      coursePlanService.reindexSource(planId, sourceId),
    onSuccess: () => {
      toast.success(t("coursePlan.sources.reindexed"));
      invalidate();
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.sources.reindexFailed"))),
  });

  const deleteMut = useMutation({
    mutationFn: (sourceId: number) =>
      coursePlanService.deleteSource(planId, sourceId),
    onSuccess: () => {
      toast.success(t("coursePlan.sources.deleted"));
      invalidate();
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.sources.deleteFailed"))),
  });

  // Attach a batch of library resources to this plan; the picker reports
  // the selected `Resource[]` and we just feed their ids to the
  // `from-resources` endpoint, then invalidate so the new rows appear.
  const attachLibraryMut = useMutation({
    mutationFn: (resourceIds: number[]) =>
      coursePlanService.attachResources(planId, resourceIds),
    onSuccess: (res) => {
      if (res.count > 0) {
        toast.success(
          t("coursePlan.sources.libraryAttached", { count: res.count }),
        );
      }
      if (res.skipped_existing.length > 0) {
        toast.message(
          t("coursePlan.sources.librarySkipped", {
            count: res.skipped_existing.length,
          }),
        );
      }
      invalidate();
      setLibraryOpen(false);
    },
    onError: (err) =>
      toast.error(
        describeApiError(err, t("coursePlan.sources.libraryAttachFailed")),
      ),
  });

  const extractMut = useMutation({
    mutationFn: () => coursePlanService.extractWorkbooks(planId),
    onSuccess: (res) => {
      if (res.data.materials_created > 0) {
        toast.success(
          t("coursePlan.sources.extractDone", {
            materials: res.data.materials_created,
            exercises: res.data.exercises_total,
            defaultValue:
              "Extracted {{exercises}} exercises into {{materials}} workbooks",
          }),
        );
      } else {
        toast.message(
          res.data.reason
            ? `${t("coursePlan.sources.extractEmpty", { defaultValue: "No new exercises extracted" })} (${res.data.reason})`
            : t("coursePlan.sources.extractEmpty", {
                defaultValue: "No new exercises extracted",
              }),
        );
      }
      qc.invalidateQueries({ queryKey: ["course-plan", planId] });
      qc.invalidateQueries({ queryKey: ["course-plan", planId, "deliverables"] });
    },
    onError: (err) =>
      toast.error(
        describeApiError(
          err,
          t("coursePlan.sources.extractFailed", {
            defaultValue: "Failed to extract workbooks",
          }),
        ),
      ),
  });

  // True once at least one source is fully indexed — surfaces the
  // "Extract workbooks" action only when running it can actually
  // produce results.
  const hasIndexed = sources.some((s) => s.status === "indexed");

  return (
    <Card data-section="sources-card">
      <CardHeader>
        <div className="flex items-start justify-between gap-2 flex-wrap">
          <div>
            <CardTitle className="text-base flex items-center gap-2">
              <Database className="h-4 w-4 text-primary" />
              {t("coursePlan.sources.sectionTitle")}
            </CardTitle>
            <CardDescription>
              {t("coursePlan.sources.sectionDesc")}
            </CardDescription>
          </div>
          <Button
            size="sm"
            variant="outline"
            onClick={() => extractMut.mutate()}
            disabled={extractMut.isPending || !hasIndexed}
            title={
              hasIndexed
                ? undefined
                : t("coursePlan.sources.extractGate", {
                    defaultValue: "Index a source first",
                  })
            }
          >
            {extractMut.isPending ? (
              <Loader2 className="mr-1 h-4 w-4 animate-spin" />
            ) : (
              <ClipboardList className="mr-1 h-4 w-4" />
            )}
            {t("coursePlan.sources.extractWorkbooks", {
              defaultValue: "Extract workbooks from sources",
            })}
          </Button>
        </div>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
          <div className="rounded-md border-dashed border-2 px-4 py-5 text-center bg-muted/20">
            <FileText className="mx-auto h-5 w-5 text-muted-foreground" />
            <p className="mt-1 text-xs text-muted-foreground">
              {t("coursePlan.sources.dropHint")}
            </p>
            <input
              ref={fileRef}
              type="file"
              multiple
              className="hidden"
              accept=".pdf,.doc,.docx,.txt,.md,.html,.htm,.rtf"
              onChange={(e) => {
                uploadFiles(e.currentTarget.files);
                if (fileRef.current) fileRef.current.value = "";
              }}
            />
            <Button
              variant="outline"
              size="sm"
              className="mt-2"
              onClick={() => fileRef.current?.click()}
            >
              <Upload className="mr-1 h-4 w-4" />
              {t("coursePlan.sources.uploadFiles")}
            </Button>
          </div>
          {/* Pick from /admin/resources — surfaces the central learning
              library inside the course-plan workflow so admins don't
              have to re-upload PDFs/links they already curated. */}
          <div className="rounded-md border-dashed border-2 px-4 py-5 text-center bg-muted/20">
            <Library className="mx-auto h-5 w-5 text-muted-foreground" />
            <p className="mt-1 text-xs text-muted-foreground">
              {t("coursePlan.sources.libraryHint")}
            </p>
            <Button
              variant="outline"
              size="sm"
              className="mt-2"
              onClick={() => setLibraryOpen(true)}
            >
              <Library className="mr-1 h-4 w-4" />
              {t("coursePlan.sources.pickFromLibrary")}
            </Button>
          </div>
          <div>
            <Label className="text-xs">{t("coursePlan.sources.urlLabel")}</Label>
            <div className="mt-1 flex gap-2">
              <Input
                value={urlDraft}
                onChange={(e) => setUrlDraft(e.target.value)}
                placeholder="https://example.com/article"
                onKeyDown={(e) => {
                  if (e.key === "Enter") {
                    e.preventDefault();
                    void addUrl();
                  }
                }}
              />
              <Button
                variant="outline"
                size="sm"
                disabled={!urlDraft.trim()}
                onClick={() => void addUrl()}
              >
                <Link2 className="h-4 w-4" />
              </Button>
            </div>
          </div>
          <div>
            <Label className="text-xs">{t("coursePlan.sources.textLabel")}</Label>
            <div className="mt-1 flex gap-2">
              <Textarea
                rows={3}
                value={textDraft}
                onChange={(e) => setTextDraft(e.target.value)}
                placeholder={t("coursePlan.sources.textPlaceholder")}
              />
              <Button
                variant="outline"
                size="sm"
                disabled={!textDraft.trim()}
                onClick={() => void addText()}
                className="self-start"
              >
                <Plus className="h-4 w-4" />
              </Button>
            </div>
          </div>
        </div>

        {isLoading && <Skeleton className="h-12 w-full" />}

        {!isLoading && sources.length === 0 && (
          <div className="text-sm text-muted-foreground italic">
            {t("coursePlan.sources.empty")}
          </div>
        )}

        {sources.length > 0 && (
          <div className="rounded-md border">
            <ul className="divide-y">
              {sources.map((s) => (
                <li
                  key={s.id}
                  className="flex flex-wrap items-center gap-2 px-3 py-2 text-sm"
                >
                  <Badge variant="outline" className="capitalize text-[10px]">
                    {t(`coursePlan.sources.kindLabel.${s.kind}`, s.kind)}
                  </Badge>
                  <span className="flex-1 min-w-0 truncate font-medium">
                    {s.kind === "resource"
                      ? s.resource_name || s.name || `Resource #${s.resource_id}`
                      : s.name || s.url || s.file_name || `Source #${s.id}`}
                  </span>
                  {s.kind === "resource" && s.resource_id && (
                    <Badge
                      variant="secondary"
                      className="text-[10px] inline-flex items-center gap-1"
                      title={t("coursePlan.sources.linkedToLibrary")}
                    >
                      <Library className="h-3 w-3" />
                      {t("coursePlan.sources.fromLibrary")}
                    </Badge>
                  )}
                  <SourceStatusBadge status={s.status} />
                  {s.chunks_count > 0 && (
                    <span className="text-xs text-muted-foreground">
                      {t("coursePlan.sources.chunks", { count: s.chunks_count })}
                    </span>
                  )}
                  <Button
                    size="sm"
                    variant="ghost"
                    onClick={() => reindexMut.mutate(s.id)}
                    disabled={reindexMut.isPending}
                  >
                    <RefreshCw className="h-3.5 w-3.5" />
                  </Button>
                  <Button
                    size="sm"
                    variant="ghost"
                    onClick={() => {
                      if (!window.confirm(t("common.confirm", "Are you sure?")))
                        return;
                      deleteMut.mutate(s.id);
                    }}
                  >
                    <Trash2 className="h-3.5 w-3.5" />
                  </Button>
                </li>
              ))}
            </ul>
          </div>
        )}
      </CardContent>
      <LibraryPickerDialog
        open={libraryOpen}
        onOpenChange={setLibraryOpen}
        alreadyLinkedIds={linkedResourceIds}
        isPending={attachLibraryMut.isPending}
        onConfirm={(picked) =>
          attachLibraryMut.mutate(picked.map((r) => r.id))
        }
      />
    </Card>
  );
}

function SourceStatusBadge({ status }: { status: CoursePlanSource["status"] }) {
  const { t } = useTranslation();
  const map: Record<CoursePlanSource["status"], { variant: "default" | "secondary" | "destructive" | "outline"; label: string }> = {
    pending: { variant: "outline", label: t("coursePlan.sources.status.pending") },
    indexing: { variant: "secondary", label: t("coursePlan.sources.status.indexing") },
    indexed: { variant: "default", label: t("coursePlan.sources.status.indexed") },
    failed: { variant: "destructive", label: t("coursePlan.sources.status.failed") },
  };
  const cfg = map[status] ?? map.pending;
  return <Badge variant={cfg.variant}>{cfg.label}</Badge>;
}

// ---------------------------------------------------------------------------
// Phase D — Assignments card + dialog
// ---------------------------------------------------------------------------

function AssignmentsCard({
  plan,
  assignments,
  studentPreview,
}: {
  plan: CoursePlan;
  assignments: CoursePlanAssignment[];
  studentPreview: boolean;
}) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const [open, setOpen] = useState(false);

  const removeMut = useMutation({
    mutationFn: (aid: number) =>
      coursePlanService.deleteAssignment(plan.id, aid),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["course-plan", plan.id, "assignments"] });
      toast.success(t("coursePlan.assignments.removed"));
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.assignments.removeFailed"))),
  });

  return (
    <Card>
      <CardHeader>
        <div className="flex items-center justify-between flex-wrap gap-2">
          <div>
            <CardTitle className="text-base flex items-center gap-2">
              <Users className="h-4 w-4 text-primary" />
              {t("coursePlan.assignments.title")}
            </CardTitle>
            <CardDescription>{plan.name}</CardDescription>
          </div>
          {!studentPreview && (
            <Button size="sm" onClick={() => setOpen(true)}>
              <UserPlus className="mr-1 h-4 w-4" />
              {t("coursePlan.assignments.assign")}
            </Button>
          )}
        </div>
      </CardHeader>
      <CardContent>
        {assignments.length === 0 ? (
          <p className="text-sm italic text-muted-foreground">
            {t("coursePlan.assignments.empty")}
          </p>
        ) : (
          <ul className="space-y-2">
            {assignments.map((a) => (
              <li
                key={a.id}
                className="flex items-center gap-3 rounded-md border px-3 py-2 text-sm flex-wrap"
              >
                <Badge variant="outline" className="capitalize">
                  {a.mode === "batch"
                    ? t("coursePlan.assignments.modeBatch")
                    : a.mode === "entities"
                      ? t("coursePlan.assignments.modeEntities", "Entities")
                    : t("coursePlan.assignments.modeStudents")}
                </Badge>
                <div className="flex-1 min-w-0">
                  <div className="font-medium truncate">
                    {a.mode === "batch"
                      ? a.batch_name || `Batch #${a.batch_id}`
                      : a.mode === "entities"
                        ? (a.entity_names?.join(", ") || t("coursePlan.assignments.modeEntities", "Entities"))
                      : t("coursePlan.assignments.students", {
                          count: a.student_count,
                        })}
                  </div>
                  <div className="text-xs text-muted-foreground truncate">
                    {a.assigned_by_name &&
                      t("coursePlan.assignments.assignedBy", {
                        name: a.assigned_by_name,
                      })}
                    {a.due_date ? ` · ${a.due_date}` : ""}
                  </div>
                </div>
                {!studentPreview && (
                  <Button
                    size="sm"
                    variant="ghost"
                    onClick={() => {
                      if (!window.confirm(t("coursePlan.assignments.confirmRemove")))
                        return;
                      removeMut.mutate(a.id);
                    }}
                  >
                    <Trash2 className="h-4 w-4" />
                  </Button>
                )}
              </li>
            ))}
          </ul>
        )}
      </CardContent>

      <AssignDialog open={open} onOpenChange={setOpen} planId={plan.id} />
    </Card>
  );
}

function AssignDialog({
  open,
  onOpenChange,
  planId,
}: {
  open: boolean;
  onOpenChange: (v: boolean) => void;
  planId: number;
}) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const [mode, setMode] = useState<"batch" | "students" | "entities">("batch");
  const [batchId, setBatchId] = useState<string>("");
  const [studentIds, setStudentIds] = useState<number[]>([]);
  const [entityIds, setEntityIds] = useState<number[]>([]);
  const [dueDate, setDueDate] = useState<string>("");
  const [message, setMessage] = useState<string>("");

  const batchesQ = useQuery({
    queryKey: ["lms-batches"],
    queryFn: () => lmsService.listBatches({ size: 100 }),
    enabled: open,
  });

  const studentsQ = useQuery({
    queryKey: ["lms-students-for-assign"],
    queryFn: () => lmsService.listStudents({ size: 200 }),
    enabled: open && mode === "students",
  });

  const entitiesQ = useQuery({
    queryKey: ["entities-for-course-plan-assign"],
    queryFn: () => entitiesService.list({ size: 200 }),
    enabled: open && mode === "entities",
  });

  useEffect(() => {
    if (!open) {
      setMode("batch");
      setBatchId("");
      setStudentIds([]);
      setEntityIds([]);
      setDueDate("");
      setMessage("");
    }
  }, [open]);

  const createMut = useMutation({
    mutationFn: () => {
      if (mode === "batch") {
        return coursePlanService.createAssignment(planId, {
          mode: "batch",
          batch_id: Number(batchId),
          due_date: dueDate || null,
          message: message.trim() || undefined,
        });
      }
      if (mode === "entities") {
        return coursePlanService.createAssignment(planId, {
          mode: "entities",
          entity_ids: entityIds,
          due_date: dueDate || null,
          message: message.trim() || undefined,
        });
      }
      return coursePlanService.createAssignment(planId, {
        mode: "students",
        student_user_ids: studentIds,
        due_date: dueDate || null,
        message: message.trim() || undefined,
      });
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["course-plan", planId, "assignments"] });
      toast.success(t("coursePlan.assignments.created"));
      onOpenChange(false);
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.assignments.createFailed"))),
  });

  const canSave =
    (mode === "batch" && batchId) ||
    (mode === "entities" && entityIds.length > 0) ||
    (mode === "students" && studentIds.length > 0);

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-lg">
        <DialogHeader>
          <DialogTitle>{t("coursePlan.assignments.assignTitle")}</DialogTitle>
          <DialogDescription>
            {t("coursePlan.assignments.assignSubtitle")}
          </DialogDescription>
        </DialogHeader>

        <div className="space-y-4">
          <div className="grid grid-cols-3 gap-2">
            <Button
              type="button"
              variant={mode === "batch" ? "default" : "outline"}
              size="sm"
              onClick={() => setMode("batch")}
            >
              <Users className="mr-1 h-4 w-4" />
              {t("coursePlan.assignments.modeBatch")}
            </Button>
            <Button
              type="button"
              variant={mode === "entities" ? "default" : "outline"}
              size="sm"
              onClick={() => setMode("entities")}
            >
              <Users className="mr-1 h-4 w-4" />
              {t("coursePlan.assignments.modeEntities", "Entities")}
            </Button>
            <Button
              type="button"
              variant={mode === "students" ? "default" : "outline"}
              size="sm"
              onClick={() => setMode("students")}
            >
              <UserPlus className="mr-1 h-4 w-4" />
              {t("coursePlan.assignments.modeStudents")}
            </Button>
          </div>

          {mode === "batch" && (
            <div>
              <Label className="text-xs">
                {t("coursePlan.assignments.pickBatch")}
              </Label>
              <Select value={batchId} onValueChange={setBatchId}>
                <SelectTrigger>
                  <SelectValue
                    placeholder={t("coursePlan.assignments.pickBatch")}
                  />
                </SelectTrigger>
                <SelectContent>
                  {batchesQ.data?.items.length ? (
                    batchesQ.data.items.map((b) => (
                      <SelectItem key={b.id} value={String(b.id)}>
                        {b.name}{" "}
                        <span className="text-xs text-muted-foreground">
                          ({b.course_name})
                        </span>
                      </SelectItem>
                    ))
                  ) : (
                    <div className="px-3 py-2 text-xs text-muted-foreground">
                      {t("coursePlan.assignments.noBatches")}
                    </div>
                  )}
                </SelectContent>
              </Select>
            </div>
          )}

          {mode === "students" && (
            <div>
              <Label className="text-xs">
                {t("coursePlan.assignments.pickStudents")}
              </Label>
              <div className="mt-1 max-h-56 overflow-auto rounded-md border">
                {studentsQ.isLoading ? (
                  <div className="p-3">
                    <Skeleton className="h-4 w-3/4" />
                  </div>
                ) : studentsQ.data?.items.length ? (
                  <ul className="divide-y">
                    {studentsQ.data.items
                      .filter((s) => s.user_id != null)
                      .map((s) => {
                        const uid = s.user_id as number;
                        const checked = studentIds.includes(uid);
                        return (
                          <li
                            key={s.id}
                            className="flex items-center gap-2 px-3 py-1.5"
                          >
                            <Checkbox
                              checked={checked}
                              onCheckedChange={(v) => {
                                setStudentIds((prev) =>
                                  v
                                    ? Array.from(new Set([...prev, uid]))
                                    : prev.filter((x) => x !== uid),
                                );
                              }}
                              id={`stu-${s.id}`}
                            />
                            <label
                              htmlFor={`stu-${s.id}`}
                              className="text-sm flex-1 truncate cursor-pointer"
                            >
                              {s.name}{" "}
                              <span className="text-xs text-muted-foreground">
                                {s.email}
                              </span>
                            </label>
                          </li>
                        );
                      })}
                  </ul>
                ) : (
                  <div className="px-3 py-2 text-xs text-muted-foreground">
                    {t("coursePlan.assignments.noStudents")}
                  </div>
                )}
              </div>
            </div>
          )}

          {mode === "entities" && (
            <div>
              <Label className="text-xs">
                {t("coursePlan.assignments.pickEntities", "Pick entities")}
              </Label>
              <div className="mt-1 max-h-56 overflow-auto rounded-md border">
                {entitiesQ.isLoading ? (
                  <div className="p-3">
                    <Skeleton className="h-4 w-3/4" />
                  </div>
                ) : entitiesQ.data?.items.length ? (
                  <ul className="divide-y">
                    {entitiesQ.data.items.map((e: Entity) => {
                      const checked = entityIds.includes(e.id);
                      return (
                        <li
                          key={e.id}
                          className="flex items-center gap-2 px-3 py-1.5"
                        >
                          <Checkbox
                            checked={checked}
                            onCheckedChange={(v) => {
                              setEntityIds((prev) =>
                                v
                                  ? Array.from(new Set([...prev, e.id]))
                                  : prev.filter((x) => x !== e.id),
                              );
                            }}
                            id={`entity-${e.id}`}
                          />
                          <label
                            htmlFor={`entity-${e.id}`}
                            className="text-sm flex-1 truncate cursor-pointer"
                          >
                            {e.name}
                          </label>
                        </li>
                      );
                    })}
                  </ul>
                ) : (
                  <div className="px-3 py-2 text-xs text-muted-foreground">
                    {t("coursePlan.assignments.noEntities", "No entities found")}
                  </div>
                )}
              </div>
            </div>
          )}

          <div className="grid gap-3 sm:grid-cols-2">
            <div>
              <Label className="text-xs">
                {t("coursePlan.assignments.dueDate")}
              </Label>
              <Input
                type="date"
                value={dueDate}
                onChange={(e) => setDueDate(e.target.value)}
              />
            </div>
            <div>
              <Label className="text-xs">
                {t("coursePlan.assignments.message")}
              </Label>
              <Textarea
                rows={2}
                value={message}
                onChange={(e) => setMessage(e.target.value)}
                placeholder={t("coursePlan.assignments.messagePlaceholder")}
              />
            </div>
          </div>
        </div>

        <DialogFooter>
          <Button
            variant="outline"
            onClick={() => onOpenChange(false)}
            disabled={createMut.isPending}
          >
            {t("coursePlan.assignments.cancel")}
          </Button>
          <Button
            onClick={() => createMut.mutate()}
            disabled={!canSave || createMut.isPending}
          >
            {createMut.isPending ? (
              <Loader2 className="mr-1 h-4 w-4 animate-spin" />
            ) : (
              <UserPlus className="mr-1 h-4 w-4" />
            )}
            {t("coursePlan.assignments.save")}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

// ---------------------------------------------------------------------------
// Weeks + materials with Phase C media drawer
// ---------------------------------------------------------------------------

function WeekAccordionItem({
  week,
  materials,
  generating,
  onGenerate,
  onBulkMedia,
  bulkBusy,
  studentPreview,
  hasIndexedSources,
}: {
  week: CoursePlanWeek;
  materials: CoursePlanMaterial[];
  generating: boolean;
  onGenerate: () => void;
  onBulkMedia: (kinds: Array<"audio" | "image" | "video">) => void;
  bulkBusy: boolean;
  studentPreview: boolean;
  hasIndexedSources: boolean;
}) {
  const { t } = useTranslation();
  const [skillFilter, setSkillFilter] = useState<string>("all");
  const materialSkills = useMemo(
    () => Array.from(new Set(materials.map((m) => m.skill))).sort(),
    [materials],
  );
  const filteredMaterials = useMemo(
    () => materials.filter((m) => skillFilter === "all" || m.skill === skillFilter),
    [materials, skillFilter],
  );
  return (
    <AccordionItem value={String(week.week_number)}>
      <AccordionTrigger className="text-left">
        <div className="flex-1 flex items-center gap-3 min-w-0">
          <Badge variant="outline" className="shrink-0">
            {t("coursePlan.weekN", { n: week.week_number })}
          </Badge>
          <div className="flex-1 min-w-0">
            <div className="font-medium truncate">
              {week.focus || week.unit || "—"}
            </div>
            {week.date_label && (
              <div className="text-xs text-muted-foreground truncate">
                {week.date_label}
              </div>
            )}
          </div>
          {materials.length > 0 && (
            <Badge variant="secondary" className="shrink-0">
              {t("coursePlan.materialsCount", { count: materials.length })}
            </Badge>
          )}
        </div>
      </AccordionTrigger>
      <AccordionContent className="space-y-4">
        {week.items.length > 0 && (
          <div className="rounded-md border overflow-hidden">
            <table className="w-full text-sm">
              <thead className="bg-muted/50 text-xs uppercase text-muted-foreground">
                <tr>
                  <th className="px-3 py-2 text-left font-medium">
                    {t("coursePlan.table.skill")}
                  </th>
                  <th className="px-3 py-2 text-left font-medium">
                    {t("coursePlan.table.outcomes")}
                  </th>
                  <th className="px-3 py-2 text-left font-medium">
                    {t("coursePlan.table.remarks")}
                  </th>
                </tr>
              </thead>
              <tbody>
                {week.items.map((item, i) => {
                  const Icon = SKILL_ICONS[item.skill] ?? ClipboardList;
                  return (
                    <tr key={i} className="border-t">
                      <td className="px-3 py-2">
                        <span className="flex items-center gap-1.5 capitalize">
                          <Icon className="h-3.5 w-3.5 text-muted-foreground" />
                          <SkillBadge skill={item.skill} />
                        </span>
                      </td>
                      <td className="px-3 py-2 flex flex-wrap gap-1">
                        {item.outcome_codes.map((c) => (
                          <Badge
                            key={c}
                            variant="outline"
                            className="font-mono text-[10px]"
                          >
                            {c}
                          </Badge>
                        ))}
                      </td>
                      <td className="px-3 py-2 text-muted-foreground">
                        {item.remarks || "—"}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}

        <div className="flex flex-wrap items-center gap-2">
          {!studentPreview && (
            <>
              <Button size="sm" onClick={onGenerate} disabled={generating}>
                <Wand2 className="mr-1 h-4 w-4" />
                {generating
                  ? t("coursePlan.generating")
                  : materials.length > 0
                    ? t("coursePlan.regenerateMaterials")
                    : t("coursePlan.generateMaterials")}
                {hasIndexedSources && (
                  <Badge variant="secondary" className="ml-2">
                    RAG
                  </Badge>
                )}
              </Button>
              {materials.length > 0 && (
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => onBulkMedia(["audio", "image"])}
                  disabled={bulkBusy}
                >
                  {bulkBusy ? (
                    <Loader2 className="mr-1 h-4 w-4 animate-spin" />
                  ) : (
                    <Sparkles className="mr-1 h-4 w-4" />
                  )}
                  {t("coursePlan.media.bulk")}
                </Button>
              )}
              <span className="text-xs text-muted-foreground">
                {t("coursePlan.generateHint")}
              </span>
            </>
          )}
          {materialSkills.length > 0 && (
            <div className="ms-auto flex items-center gap-1 flex-wrap">
              <Button
                size="sm"
                variant={skillFilter === "all" ? "default" : "outline"}
                onClick={() => setSkillFilter("all")}
              >
                {t("common.all", "All")}
              </Button>
              {materialSkills.map((skill) => (
                <Button
                  key={skill}
                  size="sm"
                  variant={skillFilter === skill ? "default" : "outline"}
                  onClick={() => setSkillFilter(skill)}
                  className="capitalize"
                >
                  {skill}
                </Button>
              ))}
            </div>
          )}
        </div>

        {materials.length > 0 && (
          <div className="space-y-3">
            {filteredMaterials.map((m) => (
              <MaterialCard key={m.id} material={m} studentPreview={studentPreview} />
            ))}
          </div>
        )}
      </AccordionContent>
    </AccordionItem>
  );
}

function MaterialCard({
  material,
  studentPreview,
}: {
  material: CoursePlanMaterial;
  studentPreview: boolean;
}) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [editing, setEditing] = useState(false);
  const [title, setTitle] = useState(material.title);
  const [summary, setSummary] = useState(material.summary || "");
  const [bodyText, setBodyText] = useState(material.body_text || "");
  const [shareDate, setShareDate] = useState(material.share_date ?? "");
  const [isStatic, setIsStatic] = useState(Boolean(material.is_static));
  const Icon = SKILL_ICONS[material.skill] ?? ClipboardList;
  const mediaCount = material.media?.length ?? 0;

  useEffect(() => {
    setTitle(material.title);
    setSummary(material.summary || "");
    setBodyText(material.body_text || "");
    setShareDate(material.share_date ?? "");
    setIsStatic(Boolean(material.is_static));
  }, [material]);

  const saveMut = useMutation({
    mutationFn: () =>
      coursePlanService.updateMaterial(material.id, {
        title,
        summary,
        body_text: bodyText,
        share_date: shareDate || null,
        is_static: isStatic,
      }),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["course-plan", material.plan_id] });
      qc.invalidateQueries({
        queryKey: ["course-plan", material.plan_id, "deliverables"],
      });
      toast.success(t("common.saved", "Saved"));
      setEditing(false);
    },
    onError: (err) => toast.error(describeApiError(err, t("common.error", "Error"))),
  });

  return (
    <Card>
      <CardHeader className="pb-2">
        <div className="flex items-center gap-2 flex-wrap">
          <Icon className="h-4 w-4 text-primary" />
          <CardTitle className="text-base flex-1 min-w-0">
            {material.title}
          </CardTitle>
          <Badge variant="outline" className="text-[10px]">
            {t(
              `coursePlan.materialType.${material.material_type}`,
              material.material_type,
            )}
          </Badge>
          <SkillBadge skill={material.skill} />
          <GroundingBadge material={material} />
          <Button
            variant="outline"
            size="sm"
            onClick={() => setDrawerOpen(true)}
          >
            <Music className="mr-1 h-4 w-4" />
            {t("coursePlan.media.mediaForMaterial")}
            {mediaCount > 0 && (
              <Badge variant="secondary" className="ml-1">
                {mediaCount}
              </Badge>
            )}
          </Button>
          {!studentPreview && (
            <Button
              size="sm"
              variant={editing ? "default" : "outline"}
              onClick={() => setEditing((v) => !v)}
            >
              {editing ? t("common.cancel", "Cancel") : t("common.edit", "Edit")}
            </Button>
          )}
        </div>
        {material.summary && <CardDescription>{material.summary}</CardDescription>}
      </CardHeader>
      <CardContent>
        <div className="mb-3 flex items-center gap-2 text-xs text-muted-foreground flex-wrap">
          {material.share_date && (
            <span>
              {t("coursePlan.shareDate", "Share date")}: {material.share_date}
            </span>
          )}
          {material.is_static && (
            <Badge variant="secondary">
              {t("coursePlan.staticMaterial", "Static material (keep on regenerate)")}
            </Badge>
          )}
        </div>
        {editing && !studentPreview ? (
          <div className="space-y-3">
            <div className="grid gap-3 md:grid-cols-2">
              <div>
                <Label className="text-xs mb-1 block">{t("common.title", "Title")}</Label>
                <Input value={title} onChange={(e) => setTitle(e.target.value)} />
              </div>
              <div>
                <Label className="text-xs mb-1 block">{t("coursePlan.shareDate", "Share date")}</Label>
                <Input
                  type="date"
                  value={shareDate || ""}
                  onChange={(e) => setShareDate(e.target.value)}
                />
              </div>
            </div>
            <div className="flex items-center gap-2">
              <Checkbox
                id={`static-${material.id}`}
                checked={isStatic}
                onCheckedChange={(v) => setIsStatic(Boolean(v))}
              />
              <label htmlFor={`static-${material.id}`} className="text-sm">
                {t("coursePlan.staticMaterial", "Static material (keep on regenerate)")}
              </label>
            </div>
            <div>
              <Label className="text-xs mb-1 block">{t("common.summary", "Summary")}</Label>
              <Textarea rows={3} value={summary} onChange={(e) => setSummary(e.target.value)} />
            </div>
            <div>
              <Label className="text-xs mb-1 block">{t("coursePlan.content", "Content")}</Label>
              <Textarea
                rows={10}
                value={bodyText}
                onChange={(e) => setBodyText(e.target.value)}
                placeholder={t("coursePlan.contentPlaceholder", "Write material content here...")}
              />
            </div>
            <div className="flex justify-end">
              <Button onClick={() => saveMut.mutate()} disabled={saveMut.isPending}>
                {saveMut.isPending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
                {t("common.save", "Save")}
              </Button>
            </div>
          </div>
        ) : (
          <MaterialBookView material={material} />
        )}
      </CardContent>

      <MediaDrawer
        open={drawerOpen}
        onOpenChange={setDrawerOpen}
        material={material}
        studentPreview={studentPreview}
      />
    </Card>
  );
}

function MediaDrawer({
  open,
  onOpenChange,
  material,
  studentPreview,
}: {
  open: boolean;
  onOpenChange: (v: boolean) => void;
  material: CoursePlanMaterial;
  studentPreview: boolean;
}) {
  const { t } = useTranslation();
  const qc = useQueryClient();

  const mediaQ = useQuery({
    queryKey: ["material-media", material.id],
    queryFn: () => coursePlanService.listMaterialMedia(material.id),
    enabled: open,
    initialData: material.media
      ? { items: material.media, count: material.media.length }
      : undefined,
  });

  const refresh = () => {
    qc.invalidateQueries({ queryKey: ["material-media", material.id] });
    qc.invalidateQueries({ queryKey: ["course-plan", material.plan_id] });
    qc.invalidateQueries({
      queryKey: ["course-plan", material.plan_id, "deliverables"],
    });
  };

  const audioMut = useMutation({
    mutationFn: () => coursePlanService.generateAudio(material.id),
    onSuccess: () => {
      toast.success(t("coursePlan.media.audioReady"));
      refresh();
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.media.audioFailed"))),
  });
  const imageMut = useMutation({
    mutationFn: () => coursePlanService.generateImage(material.id),
    onSuccess: () => {
      toast.success(t("coursePlan.media.imageReady"));
      refresh();
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.media.imageFailed"))),
  });
  const videoMut = useMutation({
    mutationFn: () => coursePlanService.composeVideo(material.id),
    onSuccess: () => {
      toast.success(t("coursePlan.media.videoReady"));
      refresh();
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.media.videoFailed"))),
  });
  const deleteMut = useMutation({
    mutationFn: (mid: number) => coursePlanService.deleteMedia(mid),
    onSuccess: () => {
      toast.success(t("coursePlan.media.deleted"));
      refresh();
    },
    onError: (err) =>
      toast.error(describeApiError(err, t("coursePlan.media.deleteFailed"))),
  });

  // Admin upload — one mutation handles all three kinds, the file picker
  // hidden inputs below decide which kind the file is for. The mutation
  // input is the kind+file pair so React Query can serialize multiple
  // uploads (e.g. drag two images in a row) without colliding state.
  const uploadMut = useMutation({
    mutationFn: ({ kind, file }: { kind: "audio" | "image" | "video"; file: File }) =>
      coursePlanService.uploadMedia(material.id, kind, file),
    onSuccess: (_data, vars) => {
      toast.success(
        t(`coursePlan.media.uploaded_${vars.kind}`, {
          defaultValue: `${vars.kind.charAt(0).toUpperCase() + vars.kind.slice(1)} uploaded`,
        }),
      );
      refresh();
    },
    onError: (err) =>
      toast.error(
        describeApiError(
          err,
          t("coursePlan.media.uploadFailed", { defaultValue: "Upload failed" }),
        ),
      ),
  });

  const audioInputRef = useRef<HTMLInputElement>(null);
  const imageInputRef = useRef<HTMLInputElement>(null);
  const videoInputRef = useRef<HTMLInputElement>(null);

  function handleFilePicked(
    kind: "audio" | "image" | "video",
    e: React.ChangeEvent<HTMLInputElement>,
  ) {
    const file = e.target.files?.[0];
    // Always reset the input value so the same file can be re-selected
    // after a failed upload — otherwise onChange won't fire again.
    e.target.value = "";
    if (!file) return;
    uploadMut.mutate({ kind, file });
  }

  const items = mediaQ.data?.items ?? [];

  return (
    <Sheet open={open} onOpenChange={onOpenChange}>
      <SheetContent className="sm:max-w-xl overflow-auto">
        <SheetHeader>
          <SheetTitle>
            {t("coursePlan.media.drawerTitle", { title: material.title })}
          </SheetTitle>
          <SheetDescription>
            {t("coursePlan.media.drawerSubtitle")}
          </SheetDescription>
        </SheetHeader>

        {!studentPreview && (
          <>
            <div className="mt-4 grid gap-2 sm:grid-cols-3">
              <Button
                variant="outline"
                size="sm"
                onClick={() => audioMut.mutate()}
                disabled={audioMut.isPending}
              >
                {audioMut.isPending ? (
                  <Loader2 className="mr-1 h-4 w-4 animate-spin" />
                ) : (
                  <Music className="mr-1 h-4 w-4" />
                )}
                {t("coursePlan.media.generateAudio")}
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => imageMut.mutate()}
                disabled={imageMut.isPending}
              >
                {imageMut.isPending ? (
                  <Loader2 className="mr-1 h-4 w-4 animate-spin" />
                ) : (
                  <ImageIcon className="mr-1 h-4 w-4" />
                )}
                {t("coursePlan.media.generateImage")}
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => videoMut.mutate()}
                disabled={videoMut.isPending}
              >
                {videoMut.isPending ? (
                  <Loader2 className="mr-1 h-4 w-4 animate-spin" />
                ) : (
                  <Video className="mr-1 h-4 w-4" />
                )}
                {t("coursePlan.media.generateVideo")}
              </Button>
            </div>

            {/*
              Manual upload row — admins can attach a real human voiceover,
              a custom illustration, or a recorded scene to override the
              AI-generated asset. The hidden inputs are kept off-DOM-flow
              and triggered by the visible buttons via refs so the styling
              stays consistent with the Generate row above.
            */}
            <div className="mt-2 grid gap-2 sm:grid-cols-3">
              <input
                ref={audioInputRef}
                type="file"
                accept="audio/*"
                className="hidden"
                onChange={(e) => handleFilePicked("audio", e)}
              />
              <input
                ref={imageInputRef}
                type="file"
                accept="image/*"
                className="hidden"
                onChange={(e) => handleFilePicked("image", e)}
              />
              <input
                ref={videoInputRef}
                type="file"
                accept="video/*"
                className="hidden"
                onChange={(e) => handleFilePicked("video", e)}
              />
              <Button
                variant="ghost"
                size="sm"
                onClick={() => audioInputRef.current?.click()}
                disabled={uploadMut.isPending}
              >
                <Upload className="mr-1 h-4 w-4" />
                {t("coursePlan.media.uploadAudio", {
                  defaultValue: "Upload audio",
                })}
              </Button>
              <Button
                variant="ghost"
                size="sm"
                onClick={() => imageInputRef.current?.click()}
                disabled={uploadMut.isPending}
              >
                <Upload className="mr-1 h-4 w-4" />
                {t("coursePlan.media.uploadImage", {
                  defaultValue: "Upload image",
                })}
              </Button>
              <Button
                variant="ghost"
                size="sm"
                onClick={() => videoInputRef.current?.click()}
                disabled={uploadMut.isPending}
              >
                <Upload className="mr-1 h-4 w-4" />
                {t("coursePlan.media.uploadVideo", {
                  defaultValue: "Upload video",
                })}
              </Button>
            </div>
            {uploadMut.isPending && (
              <p className="text-muted-foreground mt-2 inline-flex items-center gap-1 text-xs">
                <Loader2 className="h-3 w-3 animate-spin" />
                {t("coursePlan.media.uploading", {
                  defaultValue: "Uploading…",
                })}
              </p>
            )}
          </>
        )}

        <div className="mt-6 space-y-3">
          {items.length === 0 && (
            <p className="text-sm italic text-muted-foreground">
              {t("coursePlan.media.noMedia")}
            </p>
          )}
          {items.map((m) => (
            <MediaTile
              key={m.id}
              media={m}
              onDelete={() => {
                if (!window.confirm(t("common.confirm", "Are you sure?"))) return;
                if (!studentPreview) deleteMut.mutate(m.id);
              }}
              canDelete={!studentPreview}
            />
          ))}
        </div>
      </SheetContent>
    </Sheet>
  );
}

function MediaTile({
  media,
  onDelete,
  canDelete,
}: {
  media: CoursePlanMedia;
  onDelete: () => void;
  canDelete: boolean;
}) {
  const { t } = useTranslation();
  // The streaming endpoint accepts the JWT either via the Authorization
  // header (REST clients) or via ``?token=…`` (so plain media tags work,
  // since browsers can't attach custom headers to <img>/<audio>/<video>
  // element fetches). We always go through ``withAuthQuery`` here so the
  // preview renders for the currently logged-in user without requiring an
  // Odoo session cookie.
  const previewUrl = withAuthQuery(media.preview_url || media.download_url || "");
  const downloadUrl = withAuthQuery(media.download_url || "");
  return (
    <div className="rounded-md border p-3 space-y-2">
      <div className="flex items-center gap-2 flex-wrap">
        <Badge variant="outline" className="capitalize">
          {media.kind}
        </Badge>
        <span className="text-xs text-muted-foreground">{media.provider}</span>
        <span className="ml-auto flex gap-1">
          {previewUrl && (
            <Button asChild variant="ghost" size="icon" className="h-7 w-7">
              <a href={previewUrl} target="_blank" rel="noopener noreferrer">
                <ExternalLink className="h-4 w-4" />
              </a>
            </Button>
          )}
          {downloadUrl && (
            <Button asChild variant="ghost" size="icon" className="h-7 w-7">
              <a href={downloadUrl} download>
                <Download className="h-4 w-4" />
              </a>
            </Button>
          )}
          {canDelete && (
            <Button
              variant="ghost"
              size="icon"
              className="h-7 w-7"
              onClick={onDelete}
            >
              <Trash2 className="h-4 w-4" />
            </Button>
          )}
        </span>
      </div>
      {media.kind === "audio" && previewUrl && (
        <audio src={previewUrl} controls className="w-full" />
      )}
      {media.kind === "image" && previewUrl && (
        <img
          src={previewUrl}
          alt={media.title}
          className="w-full rounded-md max-h-64 object-contain bg-muted/30"
        />
      )}
      {media.kind === "video" && previewUrl && (
        <video src={previewUrl} controls className="w-full rounded-md max-h-64" />
      )}
      {media.status === "failed" && media.error && (
        <p className="text-xs text-destructive">{media.error}</p>
      )}
      {media.status !== "ready" && media.status !== "failed" && (
        <p className="text-xs text-muted-foreground inline-flex items-center gap-1">
          <Play className="h-3 w-3" />
          {t("coursePlan.media.generating")}
        </p>
      )}
    </div>
  );
}
