// tomugi-utils.jsx — shared navigation, math, and state helpers

function scrollToId(id, offset = 64) {
  const el = id === "top" ? document.body : document.getElementById(id);
  if (!el) {
    window.scrollTo({ top: 0, behavior: "smooth" });
    return;
  }
  const y = id === "top" ? 0 : el.getBoundingClientRect().top + window.scrollY - offset;
  window.scrollTo({ top: y, behavior: "smooth" });
}

function getSeriesTotals(series) {
  return series.reduce(
    (totals, s) => ({ vols: totals.vols + s.vols, chaps: totals.chaps + s.chaps }),
    { vols: 0, chaps: 0 }
  );
}

function buildVolumes(series) {
  if (Array.isArray(series.volumePlan) && series.volumePlan.length) {
    return series.volumePlan.map((volume) => {
      const chapters = Array.isArray(volume.chapters)
        ? volume.chapters.map((num) => ({ num }))
        : [];
      const start = chapters.length ? chapters[0].num : volume.n;
      const end = chapters.length ? chapters[chapters.length - 1].num : volume.n;
      return {
        n: volume.n,
        start,
        end,
        count: volume.count || chapters.length || 0,
        chapters,
        files: Array.isArray(volume.files) ? volume.files : [],
        organized: !!volume.organized,
      };
    });
  }

  const out = [];
  const base = Math.floor(series.chaps / series.vols);
  let remainder = series.chaps - base * series.vols;
  let chapter = 1;

  for (let i = 0; i < series.vols; i++) {
    const count = base + (remainder > 0 ? 1 : 0);
    if (remainder > 0) remainder--;

    const start = chapter;
    const end = chapter + count - 1;
    const chapters = Array.from({ length: count }, (_, k) => ({ num: start + k }));

    out.push({
      n: i + 1,
      start,
      end,
      count,
      chapters,
    });
    chapter = end + 1;
  }

  return out;
}

// Recommend series similar to `current`. Shared genres (from AniList) dominate
// the score; same status and comparable length act as tie-breakers. Falls back
// gracefully to status + length when a series has no genre data yet.
function recommendSeries(current, all, limit = 6) {
  if (!current || !Array.isArray(all)) return [];
  const cg = new Set(current.genres || []);
  return all
    .filter((s) => s.id !== current.id)
    .map((s) => {
      const shared = (s.genres || []).filter((g) => cg.has(g)).length;
      let score = shared * 4;
      if (s.status === current.status) score += 1.5;
      score += Math.max(0, 2 - Math.abs((s.vols || 0) - (current.vols || 0)) / 6);
      return { s, score };
    })
    .sort((a, b) => b.score - a.score || a.s.title.localeCompare(b.s.title))
    .slice(0, limit)
    .map((x) => x.s);
}

function buildDemoVolumes(totalChaps, perVolume) {
  const volCount = Math.ceil(totalChaps / perVolume);
  return Array.from({ length: volCount }, (_, v) => {
    const start = v * perVolume + 1;
    const end = Math.min((v + 1) * perVolume, totalChaps);
    return { n: v + 1, start, end, count: end - start + 1 };
  });
}

// ---- Volume organizing logic (ported from manga-pulse) ----
// Volume boundaries are NOT computed; they come from the series' volumePlan
// (the same metadata manga-pulse uses). Organizing = parse each downloaded
// file's chapter number, then map it to the volume that owns that chapter.

function fmtChapter(n) {
  return Number.isInteger(n) ? String(n) : String(n);
}

function chapterNumberFromName(value) {
  const m = String(value || "").match(/\d+(?:\.\d+)?/);
  return m ? parseFloat(m[0]) : null;
}

function folderChapterNumber(name) {
  const m = String(name || "").match(/chapter\s+(\d+(?:\.\d+)?)/i);
  if (m) return parseFloat(m[1]);
  return chapterNumberFromName(name);
}

function isVolumeFolder(part) {
  const low = String(part || "").toLowerCase();
  return low.includes("vol") || low.startsWith("0 (");
}

// Find a chapter number for a file given its path segments (folders + filename).
function pathChapterNumber(parts) {
  const segs = parts.filter((p) => p && p !== ".");
  if (!segs.length) return null;
  for (const part of segs) {
    if (part.toLowerCase().includes("chapter")) {
      const n = folderChapterNumber(part);
      if (n !== null) return n;
    }
  }
  for (const part of segs) {
    if (isVolumeFolder(part)) continue;
    const n = chapterNumberFromName(part);
    if (n !== null && n > 0) return n;
  }
  return null;
}

// Build chapter->volume map + per-volume chapter lists from a series.
function chapterVolumeMap(series) {
  const vols = buildVolumes(series);
  const map = {};
  const lists = {};
  for (const v of vols) {
    lists[v.n] = [];
    for (const c of v.chapters || []) {
      map[c.num] = v.n;
      lists[v.n].push(c.num);
    }
  }
  return { map, lists };
}

// Decimal/special chapters (e.g. 10.5): place with their base chapter's volume.
function inferSpecialVolume(number, lists, map) {
  if (Number.isInteger(number)) return null;
  for (const vol of Object.keys(lists)) {
    const sorted = lists[vol].slice().sort((a, b) => a - b);
    for (let i = 0; i < sorted.length - 1; i++) {
      const start = sorted[i];
      const end = sorted[i + 1];
      if (!Number.isInteger(start) && start < number && number < end && Math.trunc(number) === Math.trunc(start)) {
        return Number(vol);
      }
    }
  }
  const base = Math.trunc(number);
  return map[base] !== undefined ? map[base] : null;
}

function chaptersToRange(nums) {
  const arr = nums.slice().sort((a, b) => a - b);
  if (!arr.length) return "";
  const ranges = [];
  let start = arr[0];
  let prev = arr[0];
  for (let i = 1; i < arr.length; i++) {
    const n = arr[i];
    if (Number.isInteger(prev) && Number.isInteger(n) && n === prev + 1) { prev = n; continue; }
    ranges.push(start === prev ? fmtChapter(start) : `${fmtChapter(start)}–${fmtChapter(prev)}`);
    start = prev = n;
  }
  ranges.push(start === prev ? fmtChapter(start) : `${fmtChapter(start)}–${fmtChapter(prev)}`);
  return ranges.join(", ");
}

// Group a list of file paths into volumes using the series' real volume plan.
// Group items (file paths or {path, handle} entries) into volumes using the
// series' real volume plan. getPath extracts the path string from each item.
function groupFilesByVolume(series, items, getPath) {
  getPath = getPath || ((x) => String(x));
  const { map, lists } = chapterVolumeMap(series);
  const volumes = {};        // vol -> { chapters:Set-ish, items:[] }
  const chaptersByVol = {};  // vol -> Set of chapter numbers
  const unmatchedSet = {};
  for (const item of items) {
    const ch = pathChapterNumber(getPath(item).split("/"));
    if (ch === null) continue;
    let vol = map[ch];
    if (vol === undefined) vol = inferSpecialVolume(ch, lists, map);
    if (vol === undefined || vol === null) { unmatchedSet[ch] = true; continue; }
    const e = volumes[vol] = volumes[vol] || { items: [] };
    e.items.push({ item, chapter: ch, path: getPath(item) });
    (chaptersByVol[vol] = chaptersByVol[vol] || {})[ch] = true;
  }
  const result = Object.keys(volumes)
    .map((v) => ({
      n: Number(v),
      chapters: Object.keys(chaptersByVol[v]).map(parseFloat).sort((a, b) => a - b),
      items: volumes[v].items.sort((a, b) => (a.chapter - b.chapter) || (a.path < b.path ? -1 : 1)),
      files: volumes[v].items.length,
    }))
    .sort((a, b) => a.n - b.n);
  const allChapters = {};
  for (const v of result) for (const c of v.chapters) allChapters[c] = true;
  return {
    volumes: result,
    unmatched: Object.keys(unmatchedSet).map(parseFloat).sort((a, b) => a - b),
    totalChapters: Object.keys(allChapters).length + Object.keys(unmatchedSet).length,
    totalFiles: items.length,
  };
}

function organizePreview(series, filePaths) {
  return groupFilesByVolume(series, filePaths, (p) => String(p));
}

const TomugiUtils = {
  scrollToId,
  getSeriesTotals,
  buildVolumes,
  buildDemoVolumes,
  recommendSeries,
  chaptersToRange,
  organizePreview,
  groupFilesByVolume,
};

Object.assign(window, {
  TomugiUtils,
  scrollToId,
  buildVolumes,
});
