// src/LibrarySourcesSection.jsx — Settings → PROJECT · SONG FOLDERS (24 Sep 2026).
//
// Hugo: "the songs need to be pulled from the folder i already gave, we need to copy them and keep
// them up to date with the google drive folder". A music project lists the Google Drive folders its
// songs, masters and lyrics come from; the local server copies new and changed files into the
// project's library/songs folder (services/librarySync.js) at boot, every 10 minutes and on
// "Check now". The folders are only READ. The list is saved on this computer only (never synced).
//
// 24 Sep 2026 — SONG RULES under the folders, same Save/Cancel: "Hidden songs" (a dropped song:
// not listed or played, its copy kept) and "Same song as" (two titles for one song, so its lyrics
// or master pair up). Stored with the folders, never synced.
//
// Edits are a draft until Save (so Remove needs no confirm and Cancel undoes it). Every line that
// changes (status, per-folder result, note) is always rendered at its full height (invariant #20).
// Shown only for a project whose template has a songs library (GET says enabled).
(function () {
  "use strict";

  const API = "/api/settings/library-sources";
  const fetchJson = (url, opts) => (window.authFetch || fetch)(url, opts)
    .then((r) => r.json().catch(() => ({})).then((j) => ({ ok: r.ok, j })));

  const when = (iso) => {
    if (!iso) return "";
    const d = new Date(iso);
    if (!Number.isFinite(d.getTime())) return "";
    const t = d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
    const today = new Date();
    return d.toDateString() === today.toDateString() ? t : d.toLocaleDateString([], { day: "numeric", month: "short" }) + " " + t;
  };
  const plural = (n, one, many) => n + " " + (n === 1 ? one : many);
  const guessHolds = (p) => (/lyric/i.test(String(p || "")) ? "lyrics" : "songs");
  const draftOf = (list) => (list || []).map((s) => ({ path: s.path, holds: s.holds, album: s.album || "", quality: s.quality || "auto", albums: s.albums || {}, id: s.id }));
  // the same slug the server matches titles by (db/musicLibrary.js slugify)
  const slug = (x) => String(x == null ? "" : x).normalize("NFKD").replace(/[\u0300-\u036f]/g, "")
    .replace(/[øØ]/g, "o").replace(/[æÆ]/g, "ae").replace(/ß/g, "ss")
    .toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  const rulesOf = (list) => (list || []).map((r) => ({ kind: r.kind, title: r.title || "", same_as: r.same_as || "", album: r.album || "" }));
  const ruleKey = (r) => [r.kind, slug(r.title), slug(r.kind === "same" ? r.same_as : ""), slug(r.album)].join("|");
  const sameRules = (a, b) => JSON.stringify(a.map((r) => [r.kind, r.title.trim(), r.same_as.trim(), r.album.trim()]))
    === JSON.stringify(b.map((r) => [r.kind, r.title.trim(), r.same_as.trim(), r.album.trim()]));
  const sameList = (a, b) => JSON.stringify(a.map((s) => [s.path.trim(), s.holds, (s.album || "").trim(), s.quality]))
    === JSON.stringify(b.map((s) => [s.path.trim(), s.holds, (s.album || "").trim(), s.quality]));

  function LibrarySourcesSection() {
    const pid = window.__activeProjectId || null;
    const [state, setState] = React.useState(null);
    const [draft, setDraft] = React.useState([]);
    const [rules, setRules] = React.useState([]);
    const [note, setNote] = React.useState("");
    const [busy, setBusy] = React.useState(false);
    const [picking, setPicking] = React.useState(-2);      // row index being picked, -1 = a new row
    const [canPick, setCanPick] = React.useState(false);
    const alive = React.useRef(true);
    const role = (() => {
      const r = window.__effectiveRole;
      return String((typeof r === "function" ? r() : r) || (window.__currentUser && window.__currentUser.role) || "");
    })();
    const canEdit = role === "admin" || role === "producer";

    const load = React.useCallback((resetDraft) => {
      return fetchJson(API).then(({ ok, j }) => {
        if (!alive.current || !ok) return;
        setState(j);
        if (resetDraft) { setDraft(draftOf(j.sources)); setRules(rulesOf(j.rules)); }
      }).catch(() => {});
    }, []);
    React.useEffect(() => {
      alive.current = true;
      load(true);
      fetchJson("/api/fs/pick-folder/available").then(({ j }) => { if (alive.current) setCanPick(!!(j && j.available)); }).catch(() => {});
      return () => { alive.current = false; };
    }, [pid]);
    // while a check runs, follow it
    const running = !!(state && (state.running || state.queued));
    React.useEffect(() => {
      if (!running) return undefined;
      const t = setInterval(() => load(false), 2000);
      return () => clearInterval(t);
    }, [running]);

    if (!state || !state.enabled) return null;

    const saved = draftOf(state.sources);
    const savedRules = rulesOf(state.rules);
    const dirty = !sameList(draft, saved) || !sameRules(rules, savedRules);
    const st = state.status || null;
    const perSource = {};
    for (const s of (st && st.sources) || []) perSource[s.source_id] = s;
    const errFor = (row) => {
      const hit = row.id && perSource[row.id];
      return hit || null;
    };
    const editable = canEdit && state.has_folder;

    const patchRow = (i, patch) => { setDraft((d) => d.map((r, k) => (k === i ? { ...r, ...patch } : r))); setNote(""); };
    const removeRow = (i) => { setDraft((d) => d.filter((_, k) => k !== i)); setNote(""); };
    const pick = (i) => {
      if (picking !== -2) return;
      setPicking(i); setNote("");
      const initial = i >= 0 ? draft[i].path : (draft.length ? draft[draft.length - 1].path : "");
      fetchJson("/api/fs/pick-folder", { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ initial: initial || undefined, title: "Pick a song or lyrics folder" }) })
        .then(({ ok, j }) => {
          if (!ok) throw new Error((j && j.error) || "The folder picker could not open.");
          if (!j.path) return;
          if (i >= 0) patchRow(i, { path: j.path, id: undefined });
          else setDraft((d) => d.concat([{ path: j.path, holds: guessHolds(j.path), album: "", quality: "auto", albums: {} }]));
        })
        .catch((e) => setNote(e.message))
        .finally(() => { if (alive.current) setPicking(-2); });
    };
    const addTyped = () => setDraft((d) => d.concat([{ path: "", holds: "songs", album: "", quality: "auto", albums: {} }]));
    const save = () => {
      if (!dirty || busy) return;
      if (draft.some((r) => !r.path.trim())) { setNote("Each folder needs a path - pick one or remove the empty row."); return; }
      if (rules.some((r) => !r.title.trim() || (r.kind === "same" && !r.same_as.trim()))) { setNote("Each song rule needs its titles - fill them in or remove the row."); return; }
      setBusy(true); setNote("Saving…");
      fetchJson(API, { method: "PUT", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ sources: draft.map((r) => ({ path: r.path.trim(), holds: r.holds, album: r.album.trim() || null, quality: r.quality, albums: r.albums })),
          rules: rules.map((r) => ({ kind: r.kind, title: r.title.trim(), same_as: r.kind === "same" ? r.same_as.trim() : undefined, album: r.album.trim() || null })) }) })
        .then(({ ok, j }) => {
          if (!ok) throw new Error((j && j.error) || "Could not save.");
          setState(j); setDraft(draftOf(j.sources)); setRules(rulesOf(j.rules));
          setNote(j.sources.length ? "Saved. Checking the folders now." : "Saved.");
        })
        .catch((e) => setNote(e.message))
        .finally(() => { if (alive.current) setBusy(false); });
    };
    const checkNow = () => {
      if (busy || running) return;
      setBusy(true); setNote("");
      fetchJson(API + "/check", { method: "POST" })
        .then(({ ok, j }) => { if (!ok) throw new Error((j && j.error) || "Could not start the check."); setState(j); })
        .catch((e) => setNote(e.message))
        .finally(() => { if (alive.current) setBusy(false); });
    };

    // the status line (one line, always there)
    let statusLine;
    if (state.running && state.progress) {
      const p = state.progress;
      statusLine = p.total
        ? "Checking now - " + (p.current ? "copying " + (p.done + 1) + " of " + p.total + ": " + p.current : p.done + " of " + p.total + " files checked")
        : "Checking now - reading the folders…";
    } else if (state.queued) statusLine = "Waiting for another project's check to finish…";
    else if (st && st.checked_at) {
      const bits = ["Last checked " + when(st.checked_at), plural(st.songs || 0, "song", "songs"), (st.new || 0) + " new"];
      if (st.changed) bits.push(st.changed + " updated");
      if (st.missing) bits.push(st.missing + " missing in Drive");
      if (st.hidden) bits.push(st.hidden + " hidden");
      statusLine = bits.join(", ");
    } else statusLine = saved.length ? "Not checked yet." : "No folders yet.";
    const problems = [];
    if (st && !state.running) {
      for (const e of st.errors || []) if (!e.source_id) problems.push(e.message);
      if ((st.copy_errors || []).length) problems.push(plural(st.copy_errors.length, "file", "files") + " could not be copied, e.g. " + st.copy_errors[0].file + ": " + st.copy_errors[0].message);
    }
    const bottomNote = note
      || problems[0]
      || (!state.has_folder ? "Set this on the computer that has the project folder."
        : !state.runs_here ? "The main app on this computer copies the songs; this copy only shows them."
        : !canEdit ? "Only a producer or an admin can change this."
        : "Copied into " + (state.songs_folder || "library/songs") + "/<album>/<song>/ every " + (state.every_minutes || 10) + " minutes. Nothing in these folders is ever changed.");

    return (
      <div className="settings-section glass lsrc">
        <div className="lsrc-head">
          <div>
            <div className="settings-section-head">PROJECT · SONG FOLDERS</div>
            <div className="settings-section-sub">The Google Drive folders this project's songs, masters and lyrics come from. New and changed files are copied into the project; the player plays the master when there is one.</div>
          </div>
          <div className={"lsrc-status" + (state.running ? " is-running" : "")} aria-live="polite">{statusLine}</div>
        </div>

        <div className="lsrc-list">
          {draft.map((r, i) => {
            const res = errFor(r);
            const line = !r.id ? (r.path ? "New - saved and checked when you click Save." : "Pick or paste a folder.")
              : res && res.error ? res.error
              : res ? plural(res.files || 0, r.holds === "lyrics" ? "lyrics file" : "song file", r.holds === "lyrics" ? "lyrics files" : "song files") + " found" + (r.album ? " for " + r.album : "")
              : "Not checked yet.";
            return (
              <div className="lsrc-row" key={i}>
                <div className="setting-select lsrc-kind">
                  {[["songs", "Songs"], ["lyrics", "Lyrics"]].map(([id, label]) => (
                    <button key={id} type="button" disabled={!editable} className={"setting-select-btn" + (r.holds === id ? " is-active" : "")}
                      onClick={() => patchRow(i, { holds: id, id: undefined })}>{label}</button>
                  ))}
                </div>
                <input className="np-input npi-input lsrc-path" type="text" value={r.path} disabled={!editable} spellCheck={false}
                  placeholder="Paste a folder path, or Browse…" aria-label="Folder"
                  onChange={(e) => patchRow(i, { path: e.target.value, id: undefined })}/>
                <input className="np-input npi-input lsrc-album" type="text" value={r.album} disabled={!editable}
                  placeholder="Album (blank: by title)" aria-label="Album"
                  onChange={(e) => patchRow(i, { album: e.target.value })}/>
                <div className={"setting-select lsrc-q" + (r.holds === "songs" ? "" : " is-off")} aria-hidden={r.holds === "songs" ? undefined : true}>
                  {[["auto", "Auto"], ["master", "Master"], ["demo", "Demo"]].map(([id, label]) => (
                    <button key={id} type="button" disabled={!editable || r.holds !== "songs"} tabIndex={r.holds === "songs" ? 0 : -1}
                      className={"setting-select-btn" + (r.quality === id ? " is-active" : "")}
                      onClick={() => patchRow(i, { quality: id })}>{label}</button>
                  ))}
                </div>
                <div className="lsrc-actions">
                  {canPick && <button type="button" className="np-btn np-btn-cancel npi-browse" disabled={!editable || picking !== -2} onClick={() => pick(i)}>{picking === i ? "Picking…" : "Browse…"}</button>}
                  <button type="button" className="np-btn np-btn-cancel npi-browse" disabled={!editable} onClick={() => removeRow(i)}>Remove</button>
                </div>
                <div className={"lsrc-line" + (res && res.error && r.id ? " is-err" : "")}>{line}</div>
              </div>
            );
          })}
          {!draft.length && <div className="lsrc-empty">No folders yet. Add the Drive folder that holds the songs, and the one that holds the lyrics.</div>}
        </div>

        {(() => {
          const hits = {};
          for (const h of (st && st.rules) || []) hits[h.key] = h.files;
          const patchRule = (i, p) => { setRules((l) => l.map((r, k) => (k === i ? { ...r, ...p } : r))); setNote(""); };
          const dropRule = (i) => { setRules((l) => l.filter((_, k) => k !== i)); setNote(""); };
          const addRule = (kind) => { setRules((l) => l.concat([{ kind, title: "", same_as: "", album: "" }])); setNote(""); };
          const lineFor = (r) => {
            if (!r.title.trim() || (r.kind === "same" && !r.same_as.trim())) return "Type the song titles.";
            const saved1 = savedRules.some((x) => ruleKey(x) === ruleKey(r));
            if (!saved1 || !st || !st.checked_at) return "Applied at the next check after Save.";
            const n = hits[ruleKey(r)] || 0;
            if (!n) return "No file with this title in the folders.";
            return r.kind === "hide" ? "Hidden (" + plural(n, "file", "files") + " kept, not played)." : plural(n, "file", "files") + " joined the song.";
          };
          const col = (kind, label, addLabel) => (
            <div className="lsrc-rules-col">
              <div className="lsrc-rules-head">{label}</div>
              {rules.map((r, i) => r.kind !== kind ? null : (
                <div className="lsrc-rule" key={i}>
                  <input className="np-input npi-input lsrc-rule-title" type="text" value={r.title} disabled={!editable}
                    placeholder="Song title" aria-label={kind === "hide" ? "Hidden song title" : "Song title"}
                    onChange={(e) => patchRule(i, { title: e.target.value })}/>
                  {kind === "same" && <span className="lsrc-rule-word">is</span>}
                  {kind === "same" && <input className="np-input npi-input lsrc-rule-title" type="text" value={r.same_as} disabled={!editable}
                    placeholder="the same song as" aria-label="Same song as" onChange={(e) => patchRule(i, { same_as: e.target.value })}/>}
                  <input className="np-input npi-input lsrc-rule-album" type="text" value={r.album} disabled={!editable}
                    placeholder="Any album" aria-label="Album" onChange={(e) => patchRule(i, { album: e.target.value })}/>
                  <button type="button" className="np-btn np-btn-cancel npi-browse" disabled={!editable} onClick={() => dropRule(i)}>Remove</button>
                  <div className="lsrc-line">{lineFor(r)}</div>
                </div>
              ))}
              {!rules.some((r) => r.kind === kind) && <div className="lsrc-empty">{kind === "hide" ? "None - every song in the folders is listed." : "None - titles match by name."}</div>}
              <div><button type="button" className="np-btn np-btn-cancel npi-addbtn" disabled={!editable} onClick={() => addRule(kind)}>{addLabel}</button></div>
            </div>
          );
          return (
            <div className="lsrc-rules">
              {col("hide", "HIDDEN SONGS", "Hide a song…")}
              {col("same", "SAME SONG AS", "Add a pair…")}
            </div>
          );
        })()}

        <div className="npi-customrow lsrc-foot">
          <button type="button" className="np-btn np-btn-cancel npi-addbtn" disabled={!editable || picking !== -2}
            onClick={() => (canPick ? pick(-1) : addTyped())}>{picking === -1 ? "Picking…" : "Add folder…"}</button>
          <button type="button" className="np-btn np-btn-create npi-addbtn" disabled={!editable || !dirty || busy} onClick={save}>Save</button>
          <button type="button" className="np-btn np-btn-cancel npi-addbtn" disabled={!dirty || busy} onClick={() => { setDraft(saved); setRules(savedRules); setNote(""); }}>Cancel</button>
          <button type="button" className="np-btn np-btn-cancel npi-addbtn" disabled={!editable || !state.runs_here || dirty || busy || running || !saved.length} onClick={checkNow}>
            {running ? "Checking…" : "Check now"}
          </button>
          <span className={"npi-note npi-note--flat lsrc-note" + (problems[0] && !note ? " is-err" : "")}>{bottomNote}</span>
        </div>
      </div>
    );
  }

  window.LibrarySourcesSection = LibrarySourcesSection;
})();
