import React, { useState, useEffect, useCallback, useRef } from "react";
import "./fabula-ultima-sheet.css";
import { Fields, Bond, ClassEntry, Spell, CheckMap, SavedData } from "./types";
import {
  STATUSES,
  MARTIAL_ITEMS,
  DISCIPLINES,
  MUTUAL_EXCLUSIVE,
} from "./constants";
import Header from "./components/Header";
import MainPage from "./components/MainPage";
import ClassesPage from "./components/ClassesPage";
import SpellsPage from "./components/SpellsPage";
import ManagePage from "./components/ManagePage";

const BLANK_FIELDS: Fields = {
  charName: "",
  charPronouns: "",
  charIdentity: "",
  charTheme: "",
  charOrigin: "",
  charTraits: "",
  xpCurrent: 0,
  zenit: 0,
  initMod: 0,
  defense: 0,
  magDef: 0,
  dexBase: 6,
  dexCur: 6,
  insBase: 6,
  insCur: 6,
  migBase: 6,
  migCur: 6,
  wlpBase: 6,
  wlpCur: 6,
  hpMax: "",
  hpCur: "",
  mpMax: "",
  mpCur: "",
  ipMax: 6,
  ipCur: 6,
  backpack: "",
  heroicSkills: "",
  ritualsNotes: "",
  accName: "",
  accDesc: "",
  armName: "",
  armDesc: "",
  mhName: "",
  mhDesc: "",
  ohName: "",
  ohDesc: "",
};

const BLANK_BONDS: Bond[] = Array.from({ length: 6 }, () => ({
  name: "",
  feelings: [],
}));

// URL-safe base64 (RFC 4648 §5): avoids +, /, = so the result needs no
// percent-encoding in a query string. Accepts standard base64 on decode too,
// so links shared before this change still load.
function bytesToBase64Url(bytes: Uint8Array) {
  let binary = "";
  for (let i = 0; i < bytes.length; i++)
    binary += String.fromCharCode(bytes[i]);
  return btoa(binary)
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

function base64UrlToBytes(b64: string) {
  let std = b64.replace(/-/g, "+").replace(/_/g, "/");
  if (std.length % 4) std += "=".repeat(4 - (std.length % 4));
  return Uint8Array.from(atob(std), (c) => c.charCodeAt(0));
}

async function compressToBase64(str: string) {
  const stream = new CompressionStream("deflate-raw");
  const writer = stream.writable.getWriter();
  writer.write(new TextEncoder().encode(str));
  writer.close();
  const buf = await new Response(stream.readable).arrayBuffer();
  return bytesToBase64Url(new Uint8Array(buf));
}

async function decompressFromBase64(b64: string) {
  try {
    const bytes = base64UrlToBytes(b64);
    const stream = new DecompressionStream("deflate-raw");
    const writer = stream.writable.getWriter();
    writer.write(bytes);
    writer.close();
    const buf = await new Response(stream.readable).arrayBuffer();
    return new TextDecoder().decode(buf);
  } catch {
    return decodeURIComponent(escape(atob(b64)));
  }
}

// Drop empty strings and empty arrays before encoding: a sheet is mostly blank
// text fields, and applyData() restores any missing key to its default.
function pruneEmpty(data: Record<string, unknown>) {
  return Object.fromEntries(
    Object.entries(data).filter(
      ([, v]) => v !== "" && !(Array.isArray(v) && v.length === 0),
    ),
  );
}

// Resolve a share-service path relative to the current page so it works under
// any base path (e.g. /fabula/) where Caddy reverse-proxies /api/* to the
// backend. Returns e.g. https://host/fabula/api/s.
function apiURL(path: string) {
  return new URL(path, window.location.href).toString();
}

// Copy text to the clipboard. navigator.clipboard is only available in a
// secure context (HTTPS or localhost); over plain HTTP it's undefined, so we
// fall back to the deprecated execCommand("copy"), which still works there.
// Must be called from within a user gesture (e.g. a click handler).
async function copyToClipboard(text: string) {
  if (navigator.clipboard && window.isSecureContext) {
    await navigator.clipboard.writeText(text);
    return;
  }
  const ta = document.createElement("textarea");
  ta.value = text;
  ta.style.position = "fixed";
  ta.style.opacity = "0";
  document.body.appendChild(ta);
  ta.focus();
  ta.select();
  try {
    document.execCommand("copy");
  } finally {
    document.body.removeChild(ta);
  }
}

export default function CharacterSheet() {
  const [activeTab, setActiveTab] = useState("main");
  const [urlMode, setUrlMode] = useState(false);
  const [saveStatus, setSaveStatus] = useState(false);
  const [copyStatus, setCopyStatus] = useState(false);
  const [level, setLevel] = useState(1);
  const [fp, setFp] = useState(0);
  const [fields, setFields] = useState<Fields>(BLANK_FIELDS);
  const [bonds, setBonds] = useState<Bond[]>(BLANK_BONDS);
  const [primaryClasses, setPrimaryClasses] = useState<ClassEntry[]>([]);
  const [otherClasses, setOtherClasses] = useState<ClassEntry[]>([]);
  const [spells, setSpells] = useState<Spell[]>([]);
  const [spellPickerOpen, setSpellPickerOpen] = useState(false);
  const [weaponPickerOpen, setWeaponPickerOpen] = useState(false);
  const [weaponCategory, setWeaponCategory] = useState<string>("all");
  const [statuses, setStatuses] = useState<CheckMap>({});
  const [martial, setMartial] = useState<CheckMap>({});
  const [disciplines, setDisciplines] = useState<CheckMap>({});
  const [theme, setTheme] = useState<string>(
    () =>
      localStorage.getItem("fabulaUltimaTheme") ||
      (window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"),
  );

  const importFileRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem("fabulaUltimaTheme", theme);
  }, [theme]);

  const f = useCallback(
    <K extends keyof Fields>(key: K, val: Fields[K]) =>
      setFields((prev) => ({ ...prev, [key]: val })),
    [],
  );

  const xp = parseInt(String(fields.xpCurrent), 10) || 0;
  const xpPct = Math.min((xp % 10) * 10, 100);
  const hpMax = parseInt(String(fields.hpMax), 10) || 0;
  const hpCur = parseInt(String(fields.hpCur), 10) || 0;
  const inCrisis = hpMax > 0 && hpCur <= Math.floor(hpMax / 2);
  const fpTotal = Math.max(10, fp);

  const collectData = useCallback(
    () => ({
      n: fields.charName,
      pn: fields.charPronouns,
      id: fields.charIdentity,
      th: fields.charTheme,
      og: fields.charOrigin,
      tr: fields.charTraits,
      lv: level,
      xp: fields.xpCurrent,
      z: fields.zenit,
      im: fields.initMod,
      df: fields.defense,
      md: fields.magDef,
      dxb: fields.dexBase,
      dxc: fields.dexCur,
      inb: fields.insBase,
      inc: fields.insCur,
      mgb: fields.migBase,
      mgc: fields.migCur,
      wpb: fields.wlpBase,
      wpc: fields.wlpCur,
      hx: fields.hpMax,
      hc: fields.hpCur,
      mx: fields.mpMax,
      mc: fields.mpCur,
      ix: fields.ipMax,
      ic: fields.ipCur,
      fp,
      bp: fields.backpack,
      acn: fields.accName,
      acd: fields.accDesc,
      amn: fields.armName,
      amd: fields.armDesc,
      mhn: fields.mhName,
      mhd: fields.mhDesc,
      ohn: fields.ohName,
      ohd: fields.ohDesc,
      hs: fields.heroicSkills,
      rn: fields.ritualsNotes,
      sa: STATUSES.filter((s) => statuses[s]),
      ma: MARTIAL_ITEMS.filter((m) => martial[m]),
      da: DISCIPLINES.filter((d) => disciplines[d]),
      bo: bonds.map((b) => ({ n: b.name, f: b.feelings })),
      pc: primaryClasses.map((c) => ({
        n: c.name,
        lv: c.level,
        b: c.benefits,
        s: c.skills,
      })),
      oc: otherClasses.map((c) => ({
        n: c.name,
        lv: c.level,
        b: c.benefits,
        s: c.skills,
      })),
      sp: spells.map((s) => ({
        n: s.name,
        cl: s.spellClass,
        nt: s.notes,
        mp: s.mp,
        tg: s.targets,
        dr: s.duration,
      })),
    }),
    [
      fields,
      level,
      fp,
      bonds,
      primaryClasses,
      otherClasses,
      spells,
      statuses,
      martial,
      disciplines,
    ],
  );

  const applyData = useCallback((d: SavedData) => {
    setFields({
      charName: d.n ?? d.name ?? "",
      charPronouns: d.pn ?? d.pronouns ?? "",
      charIdentity: d.id ?? d.identity ?? "",
      charTheme: d.th ?? d.theme ?? "",
      charOrigin: d.og ?? d.origin ?? "",
      charTraits: d.tr ?? d.traits ?? "",
      xpCurrent: d.xp ?? 0,
      zenit: d.z ?? d.zenit ?? 0,
      initMod: d.im ?? d.initMod ?? 0,
      defense: d.df ?? d.defense ?? 0,
      magDef: d.md ?? d.magDef ?? 0,
      dexBase: d.dxb ?? d.dexBase ?? 6,
      dexCur: d.dxc ?? d.dexCur ?? 6,
      insBase: d.inb ?? d.insBase ?? 6,
      insCur: d.inc ?? d.insCur ?? 6,
      migBase: d.mgb ?? d.migBase ?? 6,
      migCur: d.mgc ?? d.migCur ?? 6,
      wlpBase: d.wpb ?? d.wlpBase ?? 6,
      wlpCur: d.wpc ?? d.wlpCur ?? 6,
      hpMax: d.hx ?? d.hpMax ?? "",
      hpCur: d.hc ?? d.hpCur ?? "",
      mpMax: d.mx ?? d.mpMax ?? "",
      mpCur: d.mc ?? d.mpCur ?? "",
      ipMax: d.ix ?? d.ipMax ?? 6,
      ipCur: d.ic ?? d.ipCur ?? 6,
      backpack: d.bp ?? d.backpack ?? "",
      accName: d.acn ?? d.accName ?? "",
      accDesc: d.acd ?? d.accDesc ?? "",
      armName: d.amn ?? d.armName ?? "",
      armDesc: d.amd ?? d.armDesc ?? "",
      mhName: d.mhn ?? d.mhName ?? "",
      mhDesc: d.mhd ?? d.mhDesc ?? "",
      ohName: d.ohn ?? d.ohName ?? "",
      ohDesc: d.ohd ?? d.ohDesc ?? "",
      heroicSkills: d.hs ?? d.heroicSkills ?? "",
      ritualsNotes: d.rn ?? d.ritualsNotes ?? "",
    });
    setLevel(d.lv ?? d.level ?? 1);
    setFp(parseInt(d.fp) || 0);

    const sa = d.sa ?? d.statusesActive ?? [];
    setStatuses(Object.fromEntries(STATUSES.map((s) => [s, sa.includes(s)])));
    const ma = d.ma ?? d.martialChecked ?? [];
    setMartial(
      Object.fromEntries(MARTIAL_ITEMS.map((m) => [m, ma.includes(m)])),
    );
    const da = d.da ?? d.disciplinesChecked ?? [];
    setDisciplines(
      Object.fromEntries(DISCIPLINES.map((disc) => [disc, da.includes(disc)])),
    );

    const rawBonds = d.bo ?? d.bonds;
    if (rawBonds)
      setBonds(
        rawBonds.map((b: SavedData) => ({
          name: b.n ?? b.name ?? "",
          feelings: b.f ?? b.feelings ?? [],
        })),
      );

    const rawPrimary = d.pc ?? d.primaryClasses;
    if (rawPrimary)
      setPrimaryClasses(
        rawPrimary.map((c: SavedData) => ({
          name: c.n ?? c.name ?? "",
          level: c.lv ?? c.level ?? 1,
          benefits: c.b ?? c.benefits ?? "",
          skills: c.s ?? c.skills ?? "",
        })),
      );

    const rawOther = d.oc ?? d.otherClasses;
    if (rawOther)
      setOtherClasses(
        rawOther.map((c: SavedData) => ({
          name: c.n ?? c.name ?? "",
          level: c.lv ?? c.level ?? 1,
          benefits: c.b ?? c.benefits ?? "",
          skills: c.s ?? c.skills ?? "",
        })),
      );

    const rawSpells = d.sp ?? d.spells;
    if (rawSpells)
      setSpells(
        rawSpells.map((s: SavedData) => ({
          name: s.n ?? s.name ?? "",
          spellClass: s.cl ?? s.spellClass ?? "",
          notes: s.nt ?? s.notes ?? "",
          mp: s.mp ?? "",
          targets: s.tg ?? s.targets ?? "",
          duration: s.dr ?? s.duration ?? "",
        })),
      );
  }, []);

  // Init
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const shortId = params.get("s");
    const encoded = params.get("c");
    if (shortId) {
      // Short link: fetch the sheet JSON from the share service.
      fetch(apiURL(`api/s/${encodeURIComponent(shortId)}`))
        .then((res) => {
          if (!res.ok) throw new Error(String(res.status));
          return res.json();
        })
        .then((data) => {
          applyData(data);
          setUrlMode(true);
        })
        .catch(() => tryAutoLoad());
    } else if (encoded) {
      // Legacy/offline self-contained link with the data inline.
      decompressFromBase64(encoded)
        .then((json) => {
          applyData(JSON.parse(json));
          setUrlMode(true);
        })
        .catch(() => tryAutoLoad());
    } else {
      tryAutoLoad();
    }
    function tryAutoLoad() {
      const raw = localStorage.getItem("fabulaUltimaSheet");
      if (raw)
        try {
          applyData(JSON.parse(raw));
        } catch (_e) {}
    }
  }, [applyData]);

  // Auto-save every 30s
  useEffect(() => {
    const id = setInterval(() => {
      if (urlMode) return;
      localStorage.setItem("fabulaUltimaSheet", JSON.stringify(collectData()));
    }, 30000);
    return () => clearInterval(id);
  }, [urlMode, collectData]);

  const saveSheet = useCallback(() => {
    if (urlMode) return;
    localStorage.setItem("fabulaUltimaSheet", JSON.stringify(collectData()));
    setSaveStatus(true);
    setTimeout(() => setSaveStatus(false), 2000);
  }, [urlMode, collectData]);

  const loadSheet = useCallback(() => {
    const raw = localStorage.getItem("fabulaUltimaSheet");
    if (!raw) return alert("No saved sheet found.");
    try {
      applyData(JSON.parse(raw));
    } catch {
      alert("Could not load sheet.");
    }
  }, [applyData]);

  const exportSheet = useCallback(() => {
    const data = collectData();
    const blob = new Blob([JSON.stringify(data, null, 2)], {
      type: "application/json",
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download =
      ((data.n || "character").replace(/[^a-z0-9_\- ]/gi, "").trim() ||
        "character") + "-fabula-ultima.json";
    a.click();
    URL.revokeObjectURL(url);
  }, [collectData]);

  const handleImportFile = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const file = e.target.files?.[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = (ev) => {
        try {
          applyData(JSON.parse(ev.target?.result as string));
          saveSheet();
        } catch {
          alert("Could not import: invalid JSON file.");
        }
      };
      reader.readAsText(file);
    },
    [applyData, saveSheet],
  );

  const copyShareURL = useCallback(async () => {
    const json = JSON.stringify(pruneEmpty(collectData()));
    const base = window.location.origin + window.location.pathname;
    let shareURL: string;
    try {
      // Preferred: store the sheet server-side and share a short ?s=<id> link.
      const res = await fetch(apiURL("api/s"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: json,
      });
      if (!res.ok) throw new Error(String(res.status));
      const { id } = await res.json();
      shareURL = `${base}?s=${id}`;
    } catch {
      // Backend unreachable: fall back to a self-contained inline ?c= link.
      shareURL = `${base}?c=${await compressToBase64(json)}`;
    }
    await copyToClipboard(shareURL);
    setCopyStatus(true);
    setTimeout(() => setCopyStatus(false), 2000);
  }, [collectData]);

  const calcHP = useCallback(() => {
    const mig = parseInt(String(fields.migBase), 10) || 6;
    const max = mig * 5 + level;
    setFields((prev) => ({ ...prev, hpMax: max, hpCur: prev.hpCur || max }));
  }, [fields.migBase, level]);

  const calcMP = useCallback(() => {
    const wlp = parseInt(String(fields.wlpBase), 10) || 6;
    const max = wlp * 5 + level;
    setFields((prev) => ({ ...prev, mpMax: max, mpCur: prev.mpCur || max }));
  }, [fields.wlpBase, level]);

  const toggleStatus = (s: string) =>
    setStatuses((prev) => ({ ...prev, [s]: !prev[s] }));
  const toggleMartial = (m: string) =>
    setMartial((prev) => ({ ...prev, [m]: !prev[m] }));
  const toggleDisc = (d: string) =>
    setDisciplines((prev) => ({ ...prev, [d]: !prev[d] }));

  const toggleFeeling = (bondIdx: number, feeling: string) => {
    setBonds((prev) =>
      prev.map((b, i) => {
        if (i !== bondIdx) return b;
        const has = b.feelings.includes(feeling);
        const partner = MUTUAL_EXCLUSIVE[feeling];
        if (!has && partner && b.feelings.includes(partner)) return b;
        return {
          ...b,
          feelings: has
            ? b.feelings.filter((f) => f !== feeling)
            : [...b.feelings.filter((f) => f !== partner), feeling],
        };
      }),
    );
  };

  return (
    <>
      <Header
        activeTab={activeTab}
        setActiveTab={setActiveTab}
        theme={theme}
        setTheme={setTheme}
        saveStatus={saveStatus}
      />

      {urlMode && (
        <div className="url-banner">
          <span className="url-banner-icon">⚑</span> Viewing a shared character — auto-save is
          disabled. Use <strong>Manage → Save to Browser</strong> to keep any changes.
        </div>
      )}

      <MainPage
        isActive={activeTab === "main"}
        fields={fields}
        f={f}
        level={level}
        setLevel={setLevel}
        xp={xp}
        xpPct={xpPct}
        fp={fp}
        setFp={setFp}
        fpTotal={fpTotal}
        inCrisis={inCrisis}
        calcHP={calcHP}
        calcMP={calcMP}
        statuses={statuses}
        toggleStatus={toggleStatus}
        martial={martial}
        toggleMartial={toggleMartial}
        bonds={bonds}
        setBonds={setBonds}
        toggleFeeling={toggleFeeling}
        weaponPickerOpen={weaponPickerOpen}
        setWeaponPickerOpen={setWeaponPickerOpen}
        weaponCategory={weaponCategory}
        setWeaponCategory={setWeaponCategory}
      />

      <ClassesPage
        isActive={activeTab === "classes"}
        primaryClasses={primaryClasses}
        setPrimaryClasses={setPrimaryClasses}
        otherClasses={otherClasses}
        setOtherClasses={setOtherClasses}
        heroicSkills={fields.heroicSkills}
        onHeroicSkillsChange={(v) => f("heroicSkills", v)}
      />

      <SpellsPage
        isActive={activeTab === "spells"}
        spells={spells}
        setSpells={setSpells}
        disciplines={disciplines}
        toggleDisc={toggleDisc}
        ritualsNotes={fields.ritualsNotes}
        onRitualsNotesChange={(v) => f("ritualsNotes", v)}
        spellPickerOpen={spellPickerOpen}
        setSpellPickerOpen={setSpellPickerOpen}
      />

      <ManagePage
        isActive={activeTab === "manage"}
        saveSheet={saveSheet}
        loadSheet={loadSheet}
        exportSheet={exportSheet}
        handleImportFile={handleImportFile}
        importFileRef={importFileRef}
        copyShareURL={copyShareURL}
        copyStatus={copyStatus}
      />
    </>
  );
}
