// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts"
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
import { useEffect, useRef } from "preact/hooks";
import { useSignal } from "@preact/signals";
import {
  calculateWeightedAverage,
  getEffectiveNote,
  roundGrade,
} from "$root/logic/grades.ts";
import ImportResultPopup, {
  type ImportDetail,
  type ImportResult,
} from "$root/defaults/ImportResultPopup.tsx";

type Student = { numEtud: number; nom: string; prenom: string };
type ColumnInfo = {
  index: number;
  code: string;
  name: string;
  coeff: number | null;
  type: "module" | "malus" | "ue" | "semester" | "unknown";
};

function parseHeader(header: string): { code: string; name: string } {
  const parts = header.split(" - ");
  if (parts.length >= 2) {
    return { code: parts[0].trim(), name: parts.slice(1).join(" - ").trim() };
  }
  return { code: header.trim(), name: header.trim() };
}

function detectColumnType(
  header: string,
  _coeff: number | null,
): ColumnInfo["type"] {
  const h = header.trim();
  if (/^MALUS/i.test(h)) return "malus";
  if (/^S\d+$/i.test(h)) return "semester";
  // UE codes typically contain "U" followed by digits (e.g., JIN5U05, JTR5U01)
  const { code } = parseHeader(h);
  if (/U\d/i.test(code) && !/^MALUS/i.test(code)) return "ue";
  return "module";
}

export default function ImportNotes() {
  const file = useSignal<File | null>(null);
  const dragging = useSignal(false);
  const uploading = useSignal(false);
  const error = useSignal<string | null>(null);
  const importResult = useSignal<ImportResult | null>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const students = useSignal<Student[]>([]);
  const columns = useSignal<ColumnInfo[]>([]);
  const sheetNames = useSignal<string[]>([]);
  const selectedSheet = useSignal("");
  const session = useSignal<"1" | "2">("1");
  const workbookRef = useRef<XLSX.WorkBook | null>(null);

  useEffect(() => {
    fetch("/students/api/students")
      .then((r) => (r.ok ? r.json() : []))
      .then((data) => (students.value = data));
  }, []);

  function pickFile(f: File) {
    if (!f.name.match(/\.xlsx?$/i)) {
      error.value = "Fichier invalide — format attendu : .xlsx";
      return;
    }
    file.value = f;
    error.value = null;
    importResult.value = null;
    columns.value = [];

    f.arrayBuffer().then((buf) => {
      try {
        const wb = XLSX.read(buf, { type: "array" });
        workbookRef.current = wb;
        sheetNames.value = wb.SheetNames;
        if (wb.SheetNames.length > 0) {
          selectedSheet.value = wb.SheetNames[0];
          parseSheet(wb, wb.SheetNames[0]);
        }
      } catch {
        error.value = "Impossible de lire le fichier.";
      }
    });
  }

  function parseSheet(wb: XLSX.WorkBook, sheetName: string) {
    const sheet = wb.Sheets[sheetName];
    const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, {
      header: 1,
    });
    if (rows.length < 2) {
      columns.value = [];
      return;
    }

    const headerRow = rows[0];
    const coeffRow = rows[1];

    const cols: ColumnInfo[] = [];
    // First 2 columns are nom/prenom, skip them
    for (let i = 2; i < headerRow.length; i++) {
      const h = headerRow[i];
      if (h == null || String(h).trim() === "") continue;
      const header = String(h).trim();
      const coeff = typeof coeffRow[i] === "number" ? coeffRow[i] : null;
      const { code, name } = parseHeader(header);
      const type = detectColumnType(header, coeff as number | null);
      cols.push({ index: i, code, name, coeff: coeff as number | null, type });
    }
    columns.value = cols;
  }

  function onSheetChange(name: string) {
    selectedSheet.value = name;
    if (workbookRef.current) {
      parseSheet(workbookRef.current, name);
    }
  }

  function findStudent(
    nom: string,
    prenom: string,
  ): Student | undefined {
    const normNom = nom.toUpperCase().trim();
    const normPrenom = prenom.toUpperCase().trim();
    return students.value.find(
      (s) =>
        s.nom.toUpperCase().trim() === normNom &&
        s.prenom.toUpperCase().trim() === normPrenom,
    );
  }

  async function doImport() {
    if (!workbookRef.current || !selectedSheet.value) return;
    uploading.value = true;
    error.value = null;
    importResult.value = null;

    try {
      const sheet = workbookRef.current.Sheets[selectedSheet.value];
      const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, {
        header: 1,
      });

      const moduleCols = columns.value.filter((c) => c.type === "module");

      let added = 0;
      let modified = 0;
      let ignored = 0;
      let errors = 0;
      const details: ImportDetail[] = [];

      // Process data rows (skip header + coeff rows)
      for (let r = 2; r < rows.length; r++) {
        const row = rows[r];
        if (!row || row.length < 3) continue;

        const nom = row[0] != null ? String(row[0]).trim() : "";
        const prenom = row[1] != null ? String(row[1]).trim() : "";
        if (!nom || !prenom) continue;

        const student = findStudent(nom, prenom);
        if (!student) {
          ignored++;
          details.push({
            type: "error",
            message: `${nom} ${prenom} : Etudiant non trouve`,
          });
          continue;
        }

        // Import module notes
        for (const col of moduleCols) {
          const val = row[col.index];
          if (val == null || typeof val !== "number") {
            if (val != null && typeof val !== "number") {
              errors++;
              details.push({
                type: "error",
                message:
                  `${student.numEtud} : ${col.code} : Note "${val}" invalide`,
              });
            }
            continue;
          }
          if (val < 0 || val > 20) {
            errors++;
            details.push({
              type: "error",
              message:
                `${student.numEtud} : ${col.code} : Note ${val} hors limites`,
            });
            continue;
          }

          const noteField = session.value === "2" ? "noteSession2" : "note";

          // Try PUT first (update), then POST (create)
          const putRes = await fetch(
            `/notes/api/notes/${student.numEtud}/${
              encodeURIComponent(col.code)
            }`,
            {
              method: "PUT",
              headers: { "content-type": "application/json" },
              body: JSON.stringify({ [noteField]: val }),
            },
          );

          if (putRes.ok) {
            const prev = await putRes.json();
            const oldVal = session.value === "2"
              ? prev.noteSession2
              : prev.note;
            modified++;
            details.push({
              type: "change",
              message: `${student.numEtud} : ${col.code} : ${
                oldVal ?? "null"
              } -> ${val}`,
            });
          } else if (putRes.status === 404) {
            // Note doesn't exist yet, create it
            const body: Record<string, unknown> = {
              numEtud: student.numEtud,
              idModule: col.code,
              note: session.value === "1" ? val : 0,
            };
            if (session.value === "2") body.noteSession2 = val;

            const postRes = await fetch("/notes/api/notes", {
              method: "POST",
              headers: { "content-type": "application/json" },
              body: JSON.stringify(body),
            });
            if (postRes.ok) {
              added++;
              details.push({
                type: "change",
                message: `${student.numEtud} : ${col.code} : null -> ${val}`,
              });
            } else {
              errors++;
              details.push({
                type: "error",
                message:
                  `${student.numEtud} : ${col.code} : Matiere non trouvee`,
              });
            }
          } else {
            errors++;
            details.push({
              type: "error",
              message: `${student.numEtud} : ${col.code} : Erreur serveur`,
            });
          }
        }
      }

      importResult.value = { added, modified, ignored, errors, details };
    } catch {
      error.value = "Erreur lors de l'import.";
    } finally {
      uploading.value = false;
    }
  }

  function downloadTemplate() {
    globalThis.open("/templates/modele_notes.xlsx", "_blank");
  }

  function _downloadExport() {
    // Export notes from the API in the same format
    Promise.all([
      fetch("/students/api/students").then((r) => r.json()),
      fetch("/notes/api/notes").then((r) => r.json()),
      fetch("/notes/api/modules").then((r) => r.json()),
      fetch("/notes/api/ue-modules").then((r) => r.json()),
      fetch("/notes/api/ues").then((r) => r.json()),
    ]).then(
      ([
        studentsData,
        notesData,
        modulesData,
        ueModulesData,
        uesData,
      ]) => {
        // Build module map
        const modMap = new Map<string, string>(
          modulesData.map((m: { id: string; nom: string }) => [m.id, m.nom]),
        );

        // Get unique module IDs from notes
        const moduleIds = [
          ...new Set(
            notesData.map((n: { idModule: string }) => n.idModule),
          ),
        ] as string[];

        // Group ue-modules by UE
        const ueMap = new Map<number, string>(
          uesData.map((u: { id: number; nom: string }) => [u.id, u.nom]),
        );
        const umByUE = new Map<number, typeof ueModulesData>();
        for (const um of ueModulesData) {
          if (!umByUE.has(um.idUE)) umByUE.set(um.idUE, []);
          umByUE.get(um.idUE)!.push(um);
        }

        // Build column order: group modules by UE, add UE avg columns
        const orderedCols: {
          id: string;
          header: string;
          coeff: number | null;
          type: "module" | "ue";
          ueId?: number;
        }[] = [];

        const usedModules = new Set<string>();
        for (const [ueId, ums] of umByUE) {
          for (const um of ums) {
            if (!moduleIds.includes(um.idModule)) continue;
            orderedCols.push({
              id: um.idModule,
              header: `${um.idModule} - ${
                modMap.get(um.idModule) || um.idModule
              }`,
              coeff: um.coeff,
              type: "module",
              ueId,
            });
            usedModules.add(um.idModule);
          }
          const ueName = ueMap.get(ueId) || `UE ${ueId}`;
          orderedCols.push({
            id: `ue_${ueId}`,
            header: ueName,
            coeff: ums.reduce(
              (s: number, um: { coeff: number }) => s + um.coeff,
              0,
            ),
            type: "ue",
            ueId,
          });
        }
        // Add modules not linked to any UE
        for (const mId of moduleIds) {
          if (usedModules.has(mId)) continue;
          orderedCols.push({
            id: mId,
            header: `${mId} - ${modMap.get(mId) || mId}`,
            coeff: null,
            type: "module",
          });
        }

        // Build note lookup: numEtud -> idModule -> note
        const noteLookup = new Map<
          number,
          Map<string, { note: number; noteSession2: number | null }>
        >();
        for (const n of notesData) {
          if (!noteLookup.has(n.numEtud)) noteLookup.set(n.numEtud, new Map());
          noteLookup.get(n.numEtud)!.set(n.idModule, {
            note: n.note,
            noteSession2: n.noteSession2,
          });
        }

        // Get students who have notes
        const studentsWithNotes = studentsData.filter(
          (s: Student) => noteLookup.has(s.numEtud),
        );

        // Build header rows
        const headerRow: (string | null)[] = [null, null];
        const coeffRow: (number | null)[] = [null, null];
        for (const col of orderedCols) {
          headerRow.push(col.header);
          coeffRow.push(col.coeff);
        }

        // Build session 1 data rows
        const s1Rows: (string | number | null)[][] = [];
        for (const s of studentsWithNotes) {
          const row: (string | number | null)[] = [s.nom, s.prenom];
          const sNotes = noteLookup.get(s.numEtud) || new Map();
          for (const col of orderedCols) {
            if (col.type === "module") {
              const n = sNotes.get(col.id);
              row.push(n ? n.note : null);
            } else {
              // UE average - calculate
              const ueMods = orderedCols.filter(
                (c) => c.type === "module" && c.ueId === col.ueId,
              );
              
              const notesRecord: Record<string, { note: number; noteSession2: number | null }> = {};
              ueMods.forEach(um => {
                const n = sNotes.get(um.id);
                if (n) notesRecord[um.id] = n;
              });

              const avg = calculateWeightedAverage(
                ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })),
                notesRecord
              );

              row.push(avg !== null ? roundGrade(avg) : null);
            }
          }
          s1Rows.push(row);
        }

        // Build session 2 data rows
        const s2Rows: (string | number | null)[][] = [];
        for (const s of studentsWithNotes) {
          const row: (string | number | null)[] = [s.nom, s.prenom];
          const sNotes = noteLookup.get(s.numEtud) || new Map();
          for (const col of orderedCols) {
            if (col.type === "module") {
              const n = sNotes.get(col.id);
              // Use session 2 note if available, else session 1
              row.push(n ? (n.noteSession2 ?? n.note) : null);
            } else {
              const ueMods = orderedCols.filter(
                (c) => c.type === "module" && c.ueId === col.ueId,
              );

              const notesRecord: Record<string, { note: number; noteSession2: number | null }> = {};
              ueMods.forEach(um => {
                const n = sNotes.get(um.id);
                if (n) notesRecord[um.id] = n;
              });

              const avg = calculateWeightedAverage(
                ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })),
                notesRecord
              );

              row.push(avg !== null ? roundGrade(avg) : null);
            }
          }
          s2Rows.push(row);
        }

        const wb = XLSX.utils.book_new();
        const ws1 = XLSX.utils.aoa_to_sheet([headerRow, coeffRow, ...s1Rows]);
        XLSX.utils.book_append_sheet(wb, ws1, "Session 1");
        const ws2 = XLSX.utils.aoa_to_sheet([headerRow, coeffRow, ...s2Rows]);
        XLSX.utils.book_append_sheet(wb, ws2, "Session 2");
        const buf = XLSX.write(wb, { bookType: "xlsx", type: "array" });
        const blob = new Blob([buf], {
          type:
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "export_notes.xlsx";
        a.style.display = "none";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        setTimeout(() => URL.revokeObjectURL(url), 100);
      },
    );
  }

  return (
    <div>
      <input
        ref={inputRef}
        type="file"
        accept=".xlsx,.xls"
        style="display:none"
        onChange={(e) => {
          const f = (e.target as HTMLInputElement).files?.[0];
          if (f) pickFile(f);
        }}
      />

      <div
        class={`drop-zone${dragging.value ? " dragging" : ""}`}
        onDragOver={(e) => {
          e.preventDefault();
          dragging.value = true;
        }}
        onDragLeave={() => (dragging.value = false)}
        onDrop={(e) => {
          e.preventDefault();
          dragging.value = false;
          const f = e.dataTransfer?.files?.[0];
          if (f) pickFile(f);
        }}
        onClick={() => inputRef.current?.click()}
      >
        <span class="drop-zone-icon">⬇</span>
        {file.value ? <span class="drop-zone-file">{file.value.name}</span> : (
          <>
            <span class="drop-zone-text">Glisser le fichier .xlsx ici</span>
            <span class="drop-zone-hint">ou cliquer pour parcourir</span>
          </>
        )}
      </div>

      {error.value && <p class="state-error">{error.value}</p>}

      {importResult.value && (
        <ImportResultPopup
          result={importResult.value}
          onClose={() => (importResult.value = null)}
        />
      )}

      {/* Sheet + session selector */}
      {sheetNames.value.length > 0 && (
        <div style="display: flex; gap: 1rem; margin-bottom: 0.75rem; flex-wrap: wrap">
          <div>
            <label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 0.25rem">
              Feuille
            </label>
            <select
              class="filter-select"
              value={selectedSheet.value}
              onChange={(e) =>
                onSheetChange((e.target as HTMLSelectElement).value)}
            >
              {sheetNames.value.map((name) => (
                <option key={name} value={name}>{name}</option>
              ))}
            </select>
          </div>
          <div>
            <label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 0.25rem">
              Importer en tant que
            </label>
            <select
              class="filter-select"
              value={session.value}
              onChange={(e) => (session.value = (e.target as HTMLSelectElement)
                .value as "1" | "2")}
            >
              <option value="1">Session 1 (note)</option>
              <option value="2">Session 2 (noteSession2)</option>
            </select>
          </div>
        </div>
      )}

      {/* Column preview */}
      {columns.value.length > 0 && (
        <div style="margin-bottom: 1rem">
          <p style="font-size: 0.82rem; font-weight: 600; margin-bottom: 0.5rem">
            Colonnes detectees :
          </p>
          <div style="display: flex; flex-wrap: wrap; gap: 0.35rem">
            {columns.value.map((col) => (
              <span
                key={col.index}
                class={`numEtud-chip${
                  col.type === "module"
                    ? ""
                    : col.type === "malus"
                    ? " note-chip--fail"
                    : " note-chip--promo"
                }`}
                style="font-size: 0.72rem"
                title={`${col.type} — ${col.name}${
                  col.coeff != null ? ` (coef ${col.coeff})` : ""
                }`}
              >
                {col.type === "module"
                  ? "M"
                  : col.type === "ue"
                  ? "UE"
                  : col.type === "malus"
                  ? "X"
                  : "?"} {col.code}
              </span>
            ))}
          </div>
          <p class="col-dim" style="font-size: 0.72rem; margin-top: 0.35rem">
            M = ECUE (importe) | UE = moyenne UE (ignore) | X = malus
          </p>
        </div>
      )}

      <div class="upload-actions">
        <button
          type="button"
          class="btn btn-primary"
          onClick={doImport}
          disabled={!file.value || uploading.value ||
            columns.value.filter((c) => c.type === "module").length === 0}
        >
          {uploading.value ? "..." : "+ Importer"}
        </button>
        <button
          type="button"
          class="btn btn-secondary"
          onClick={downloadTemplate}
        >
          Telecharger Modele
        </button>
        {
          /* TODO: fix blob download in Fresh
        <button
          type="button"
          class="btn btn-secondary"
          onClick={downloadExport}
        >
          Exporter Notes
        </button>
        */
        }
      </div>

      <p class="upload-format">
        Format : <strong>Nom</strong> | <strong>Prenom</strong> |{" "}
        <strong>CODE - ECUE</strong> (colonnes notes){" "}
        — les colonnes UE et MALUS sont auto-detectees
      </p>
    </div>
  );
}
