/* Versionen: Schnappschüsse des Arbeitsordners (wie Commits, ohne Zweige) und
   die Brücke zu git. Der Abschnitt „Versionen“ unter Dateien zeigt den Stand
   seit dem letzten Schnappschuss, die Liste und die git-Karte. Vergleich und
   Unterschied sind Reiter; der Unterschied läuft in Monacos Diff-Editor, nur
   lesend - geändert wird im Arbeitsordner, festgehalten mit dem nächsten
   Schnappschuss. */
"use strict";

S.versionen = { pid: null, liste: [], stand: null, git: null, lauf: 0, logs: {} };

const vPfad = (rest = "") => `/api/projects/${S.pid}/versionen${rest}`;
const vLeitung = () => Boolean(S.daten && ["admin", "manager"].includes(S.daten.role));
const vTitel = (stand) => stand === "ordner" ? "Arbeitsordner" : stand ? `#${stand}` : "leer";

async function versionenLaden() {
  const [liste, stand, git] = await Promise.all([api(vPfad()), api(vPfad("/status")), api(vPfad("/git"))]);
  Object.assign(S.versionen, { pid: S.pid, liste, stand, git });
}

/* -- Abschnitt ------------------------------------------------------------- */
ABSCHNITT_ZEICHNER["dateien.versionen"] = async (ziel, aktionen) => {
  if (!S.pid) {
    ziel.append(el("div", "explorer__hinweis", "Kein Projekt gewählt."));
    return;
  }
  const k = (symbol, titel, fn) => {
    const b = el("button");
    b.type = "button"; b.title = titel;
    b.append(ico(symbol));
    b.onclick = fn;
    return b;
  };
  aktionen.append(k("camera", "Schnappschuss anlegen", schnappschussAnlegen),
                  k("code-compare", "Änderungen seit dem letzten Schnappschuss", () => BEFEHLE["versionen.vergleichen"]()),
                  k("rotate", "Aktualisieren", () => abschnittZeichnen("dateien.versionen")));
  // Live-Ereignisse kurz nacheinander: nur der jüngste Lauf zeichnet (wie im Explorer).
  const lauf = ++S.versionen.lauf;
  try {
    await versionenLaden();
  } catch (e) {
    ziel.append(el("div", "explorer__hinweis", e.message));
    return;
  }
  if (lauf !== S.versionen.lauf) return;
  const { liste, stand, git } = S.versionen;
  const n = stand.neu.length + stand.geaendert.length + stand.geloescht.length;
  const zeile = el("div", "versionen__stand");
  if (!liste.length) {
    zeile.append(ico("circle-info"), el("span", "", `Noch kein Schnappschuss · ${stand.dateien} Dateien`));
  } else if (stand.gleich) {
    zeile.append(ico("circle-check"), el("span", "", `Unverändert seit #${stand.nr}`));
  } else {
    zeile.classList.add("is-geaendert");
    zeile.append(ico("pen"), el("span", "", `${n} Änderung${n === 1 ? "" : "en"} seit #${stand.nr}`));
    zeile.onclick = () => vergleichTab(stand.nr, "ordner");
    bedienbar(zeile, "Änderungen zeigen");
  }
  ziel.append(zeile);
  const l = el("div", "versionen__liste");
  if (!liste.length) {
    l.append(el("div", "explorer__hinweis", "Der erste Schnappschuss hält den Ordner fest - Kamera oben."));
  }
  liste.forEach((sn) => l.append(schnappschussZeile(sn)));
  ziel.append(l, gitKarte(git));
  abonnieren(`projekt:${S.pid}`);
};

function schnappschussZeile(sn) {
  const z = el("div", "versionen__zeile");
  z.dataset.nr = sn.nr;
  const kopf = el("div", "versionen__kopf");
  kopf.append(el("span", "versionen__nr", `#${sn.nr}`), el("span", "versionen__msg", sn.message || "(ohne Text)"));
  if (sn.label) kopf.append(el("span", "pill pill--label", sn.label));
  if (sn.quelle !== "manuell") kopf.append(el("span", "pill pill--" + sn.quelle, sn.quelle));
  const akt = el("span", "explorer__aktionen versionen__aktionen");
  const k = (symbol, titel, fn) => {
    const b = el("button");
    b.type = "button"; b.title = titel;
    b.append(ico(symbol));
    b.onclick = (e) => { e.stopPropagation(); fn(); };
    return b;
  };
  akt.append(k("code-compare", "Mit dem vorigen Schnappschuss vergleichen", () => vergleichTab(sn.nr - 1, sn.nr)),
             k("folder-tree", "Mit dem Arbeitsordner vergleichen", () => vergleichTab(sn.nr, "ordner")),
             k("tag", "Markierung setzen", () => markieren(sn)));
  if (vLeitung()) akt.append(k("clock-rotate-left", "Arbeitsordner auf diesen Stand setzen", () => wiederherstellen(sn)));
  kopf.append(akt);
  const wer = sn.created_by ? sn.created_by.name : (sn.quelle === "git" ? "git" : "–");
  const meta = [wer, `${kurzDatum(sn.created_at)} ${uhr(sn.created_at)}`, `${sn.dateien} Dateien`, bytes(sn.bytes)];
  if (sn.git_commit) meta.push(sn.git_commit.slice(0, 7));
  z.append(kopf, el("div", "versionen__meta", meta.join(" · ")));
  z.title = sn.message;
  z.onclick = () => vergleichTab(sn.nr - 1, sn.nr);
  return bedienbar(z, `Schnappschuss #${sn.nr}`);
}

/* -- Aktionen -------------------------------------------------------------- */
async function schnappschussAnlegen() {
  if (!S.pid) return;
  const text = await nameAbfragen("Schnappschuss anlegen", "", "Was hat sich geändert? Kurz, wie eine Commit-Nachricht.");
  if (text === null) return;
  try {
    const sn = await jsonRuf(vPfad(), "POST", { message: text });
    meldung(`Schnappschuss #${sn.nr} angelegt`);
  } catch (e) {
    meldung(e.message, true);
  }
  abschnittZeichnen("dateien.versionen");
}

async function markieren(sn) {
  const label = await nameAbfragen(`Markierung für #${sn.nr}`, sn.label || "", "Kurz, etwa „v1.0“ oder „Abgabe Kunde“.");
  if (label === null) return;
  try {
    await jsonRuf(vPfad(`/${sn.nr}`), "PATCH", { label });
  } catch (e) {
    meldung(e.message, true);
  }
  abschnittZeichnen("dateien.versionen");
}

async function wiederherstellen(sn, pfad = null) {
  const frage = pfad ? `„${pfad}“ aus #${sn.nr} zurückholen? Der Stand im Arbeitsordner wird überschrieben.`
    : `Arbeitsordner auf Schnappschuss #${sn.nr} setzen? Dateien, die es dort nicht gibt, werden gelöscht; vorher wird ein Schnappschuss angelegt.`;
  if (!confirm(frage)) return;
  try {
    const r = await jsonRuf(vPfad(`/${sn.nr}/wiederherstellen`), "POST", pfad ? { pfad } : {});
    meldung(`#${sn.nr}: ${r.geschrieben} geschrieben, ${r.geloescht} gelöscht` + (r.vorher ? ` · vorher als #${r.vorher} gesichert` : ""));
  } catch (e) {
    meldung(e.message, true);
  }
  abschnittZeichnen("dateien.versionen");
}

/* -- Vergleich als Reiter: a = älterer Stand (Nr oder 0), b = neuerer (Nr oder "ordner") */
function vergleichTab(a, b) {
  return oeffneTab({ id: `versionen:vergleich:${a}:${b}`, art: "vergleich", pid: S.pid, a, b,
                     titel: `${vTitel(a)} → ${vTitel(b)}`, symbol: "code-compare",
                     hash: `versionen/${a}/${b}`, render: vergleichRendern });
}

async function vergleichRendern(ziel, tab) {
  ziel.replaceChildren();
  const leiste = el("div", "editor__leiste");
  leiste.append(el("span", "editor__pfad", `Vergleich ${vTitel(tab.a)} → ${vTitel(tab.b)}`), el("span", "spacer"));
  const koerper = el("div", "editor__koerper versionen__vergleich");
  ziel.append(leiste, koerper);
  let d;
  try {
    d = tab.b === "ordner" ? await api(vPfad(`/status?nr=${tab.a}`))
      : await api(vPfad(`/${tab.b}/unterschied?gegen=${tab.a}`));
  } catch (e) {
    koerper.append(el("div", "empty", e.message));
    return;
  }
  if (!d.neu.length && !d.geaendert.length && !d.geloescht.length) {
    koerper.append(el("div", "empty", "Kein Unterschied."));
    return;
  }
  const gruppe = (titel, symbol, klasse, pfade) => {
    if (!pfade.length) return;
    koerper.append(el("h3", "versionen__gruppe", `${titel} (${pfade.length})`));
    pfade.forEach((p) => {
      const z = el("div", "versionen__datei " + klasse);
      z.append(ico(symbol), el("span", "", p));
      z.onclick = () => diffTab(p, tab.a, tab.b);
      koerper.append(bedienbar(z, p));
    });
  };
  gruppe("Neu", "plus", "is-neu", d.neu);
  gruppe("Geändert", "pen", "is-geaendert", d.geaendert);
  gruppe("Gelöscht", "minus", "is-geloescht", d.geloescht);
}

/* -- Unterschied einer Datei im Diff-Editor -------------------------------- */
function diffTab(pfad, a, b) {
  // „datei“ statt „pfad“: die Schale stellt Reiter mit pfad als Editor wieder her.
  return oeffneTab({ id: `versionen:diff:${a}:${b}:${pfad}`, art: "diff", pid: S.pid, datei: pfad, a, b,
                     titel: `${pfad.split("/").pop()} ${vTitel(a)} → ${vTitel(b)}`, symbol: "code-compare",
                     hash: `versionen/${a}/${b}/${encodeURIComponent(pfad)}`,
                     render: diffRendern, aufraeumen: diffAufraeumen });
}

/** Text der Datei in einem Stand: "" wenn sie dort fehlt, null wenn binär. */
async function seiteLaden(pfad, stand) {
  if (!stand) return "";
  const url = stand === "ordner" ? `/api/projects/${S.pid}/dateien/inhalt?pfad=${encodeURIComponent(pfad)}`
    : vPfad(`/${stand}/datei?pfad=${encodeURIComponent(pfad)}`);
  const antwort = await fetch(url);
  if (antwort.status === 404) return "";
  if (!antwort.ok) throw new Error(`Fehler ${antwort.status}`);
  if (!(antwort.headers.get("content-type") || "").startsWith("application/json")) return null;
  return (await antwort.json()).inhalt;
}

async function diffRendern(ziel, tab) {
  ziel.replaceChildren();
  const leiste = el("div", "editor__leiste");
  leiste.append(el("span", "editor__pfad", tab.datei), el("span", "spacer"));
  const koerper = el("div", "editor__koerper");
  ziel.append(leiste, koerper);
  let alt, neu, monaco;
  try {
    [alt, neu, monaco] = await Promise.all([seiteLaden(tab.datei, tab.a), seiteLaden(tab.datei, tab.b), monacoLaden()]);
  } catch (e) {
    koerper.append(el("div", "empty", e.message));
    return;
  }
  if (alt === null || neu === null) {
    koerper.append(el("div", "empty", "Binäre Datei - kein Textvergleich."));
    return;
  }
  if (vLeitung() && tab.a && tab.b === "ordner") {
    const zurueck = knopf("btn", "clock-rotate-left", `Aus ${vTitel(tab.a)} zurückholen`);
    zurueck.onclick = () => wiederherstellen({ nr: tab.a }, tab.datei);
    leiste.append(zurueck);
  }
  if (typeof oeffneDatei === "function") {
    const offen = knopf("btn", "pen", "Im Arbeitsordner öffnen");
    offen.onclick = () => oeffneDatei(tab.datei);
    leiste.append(offen);
  }
  const flaeche = el("div", "editor__monaco");
  koerper.append(flaeche);
  const original = monaco.editor.createModel(alt, sprache(tab.datei));
  const modified = monaco.editor.createModel(neu, sprache(tab.datei));
  const editor = monaco.editor.createDiffEditor(flaeche, {
    readOnly: true, originalEditable: false, automaticLayout: true, theme: "vs", fontSize: 13,
    renderSideBySide: true, ignoreTrimWhitespace: false, scrollBeyondLastLine: false,
    minimap: { enabled: false } });
  editor.setModel({ original, modified });
  tab.daten = { editor, original, modified };
}

function diffAufraeumen(tab) {
  const d = tab.daten;
  if (!d) return;
  d.editor.dispose();
  d.original.dispose();
  d.modified.dispose();
  tab.daten = null;
}

/* -- git-Brücke ------------------------------------------------------------ */
function gitKarte(g) {
  const karte = el("div", "versionen__git");
  karte.append(el("div", "panel__titel", "Git"));
  if (!g.git) {
    karte.append(el("div", "explorer__hinweis", "git ist auf diesem Rechner nicht installiert - Schnappschüsse gehen trotzdem."));
    return karte;
  }
  const info = el("div", "versionen__gitinfo");
  info.append(el("div", "", (g.repo ? `${g.commits} Commits im Spiegel` : "Noch kein Spiegel") + ` · ${g.offen} offen`));
  info.append(el("div", "", g.remote ? `GitHub: ${g.remote}` + (g.konnektor ? "" : " · kein Konnektor")
    : "Kein GitHub-Repository im Projekt eingetragen"));
  if (g.eigenes_git) info.append(el("div", "", "Der Arbeitsordner hat ein eigenes .git"));
  if (g.laeuft) info.append(el("div", "versionen__laeuft", "Ein git-Auftrag läuft …"));
  karte.append(info);
  const knoepfe = el("div", "versionen__gitknoepfe");
  const b = (symbol, text, aktion, quelle, erlaubt) => {
    const k = knopf("btn btn--ghost", symbol, text);
    k.disabled = Boolean(g.laeuft || !erlaubt);
    k.dataset.git = quelle ? `${aktion}:${quelle}` : aktion;
    k.onclick = () => gitStarten(aktion, quelle);
    knoepfe.append(k);
  };
  b("file-export", "Export", "export", null, true);
  b("cloud-arrow-up", "Push", "push", null, vLeitung() && g.remote && g.konnektor);
  b("cloud-arrow-down", "Import von GitHub", "import", "github", vLeitung() && g.remote && g.konnektor);
  if (g.eigenes_git) b("folder-tree", "Import aus dem eigenen .git", "import", "ordner", vLeitung());
  karte.append(knoepfe);
  return karte;
}

async function gitStarten(aktion, quelle) {
  try {
    const job = await jsonRuf(vPfad(`/git/${aktion}`), "POST", quelle ? { quelle } : {});
    gitLogTab(job);
    abschnittZeichnen("dateien.versionen");
  } catch (e) {
    meldung(e.message, true);
  }
}

function gitLogTab(job) {
  return oeffneTab({ id: `versionen:git:${job.id}`, art: "gitlog", pid: S.pid, job: job.id,
                     titel: job.titel || `Git #${job.id}`, symbol: "code-branch", render: gitLogRendern });
}

function gitLogRendern(ziel, tab) {
  ziel.replaceChildren();
  const pre = el("pre", "konsole__log versionen__log");
  ziel.append(pre);
  S.versionen.logs[tab.job] = pre;
  aufJob(tab.job, (job) => {
    delete S.versionen.logs[tab.job];
    const r = job.result || {};
    pre.textContent = (job.log || "").trimEnd() + `\n— ${job.status}`
      + (r.neu !== undefined ? ` · ${r.neu} neu` : "") + (r.gepusht ? " · gepusht" : "");
    abschnittZeichnen("dateien.versionen");
  });
}

/* -- Befehle und Ereignisse ------------------------------------------------ */
BEFEHLE["versionen.schnappschuss"] = schnappschussAnlegen;
BEFEHLE["versionen.vergleichen"] = () => {
  if (!S.pid) return;
  const letzte = S.versionen.pid === S.pid && S.versionen.liste[0];
  vergleichTab(letzte ? letzte.nr : 0, "ordner");
};

document.addEventListener("projekt-geladen", () => {
  if (S.versionen.pid !== S.pid) abschnittZeichnen("dateien.versionen");
});
document.addEventListener("live", (e) => {
  const d = e.detail;
  if (!d) return;
  const pre = d.job && S.versionen.logs[d.job];
  if (pre && d.log !== undefined) pre.textContent += d.log + "\n";
  if (d.projekt !== S.pid) return;
  if (d.art === "schnappschuss" || d.art === "arbeitsordner") abschnittZeichnen("dateien.versionen");
  // Jede Dateiänderung ändert den Stand; gebündelt, weil ein Speichern-Schwall
  // sonst den Ordner bei jedem Ereignis neu hasht.
  if (d.art === "datei") {
    clearTimeout(S.versionen.timer);
    S.versionen.timer = setTimeout(() => abschnittZeichnen("dateien.versionen"), 400);
  }
});
