// ═══════════════════════════════════════════════════════════════════════
// Data store — Cloudflare Pages Functions (KV + R2) with a localStorage
// fallback.
//
// When this site is deployed on Cloudflare Pages with the /functions/api
// routes wired up (see wrangler.toml + SETUP_CLOUDFLARE.md), content lives
// in a KV namespace and media lives in an R2 bucket, served through those
// Functions. Until that's deployed — or when opening the file locally
// without `wrangler pages dev` — everything falls back to browser
// localStorage so the site still works end-to-end.
// ═══════════════════════════════════════════════════════════════════════

const LS_KEY = "abdallah-portfolio:v2";

// Default content — what visitors see before you've edited anything.
const DEFAULT_DATA = {
  hero: {
    eyebrow: "Studio · 2026 · open for commissions",
    firstName: "Abdallah",
    lastName: "Karim",
    title: "Multimedia designer working across motion, 3D, and graphic design. I make brands move — and stay moving long after the loop ends.",
    available: "Available · Q3 → Q4 '26",
    location: "Cairo, EG · GMT+2",
    role: "Multimedia Designer",
    yearsExp: "8",
    deliverables: "240+",
    studios: "12",
  },
  disciplines: [
    "Motion graphics",
    "3D animation",
    "Brand identity",
    "Editorial design",
    "Art direction",
    "Type design",
    "Sound + sync",
    "Compositing",
  ],
  marquee: [
    "Now booking · Q3 2026",
    "Motion design",
    "3D / Cinema 4D / Blender",
    "Brand systems",
    "Available worldwide",
    "Studio of one",
    "Visit-by-appointment",
  ],
  about: {
    portrait: "",
    portraitLabel: "PORTRAIT_01",
    paragraphs: [
      "I design for screens that move — title sequences, brand films, social campaigns, product launches, the occasional music video. Eight years across studios and one solo practice.",
      "I am most comfortable at the intersection of typography, code, and the bezier curve. The job, as I see it, is making complicated things feel inevitable.",
    ],
    readout: [
      { k: "Based",     v: "Cairo, Egypt",                acc: false },
      { k: "Working in",v: "Worldwide — async-first",     acc: false },
      { k: "Languages", v: "Arabic · English · French",   acc: false },
      { k: "Software",  v: "AE · C4D · Blender · Figma",  acc: false },
      { k: "Status",    v: "Open · Q3–Q4 2026",           acc: true },
      { k: "Response",  v: "Within 24 hours",             acc: false },
    ],
  },
  skills: [
    {
      name: "Motion",
      items: [
        { name: "After Effects",      level: 0.95 },
        { name: "Cavalry",            level: 0.85 },
        { name: "Cel animation",      level: 0.75 },
        { name: "Compositing / VFX",  level: 0.85 },
        { name: "Sound design / sync",level: 0.7 },
      ],
    },
    {
      name: "3D",
      items: [
        { name: "Cinema 4D",          level: 0.9 },
        { name: "Octane / Redshift",  level: 0.85 },
        { name: "Blender",            level: 0.8 },
        { name: "Houdini",            level: 0.55 },
        { name: "Unreal Engine",      level: 0.5 },
      ],
    },
    {
      name: "Graphic",
      items: [
        { name: "Brand identity",     level: 0.95 },
        { name: "Editorial layout",   level: 0.9 },
        { name: "Typography",         level: 0.95 },
        { name: "Illustration",       level: 0.7 },
        { name: "Web design / Figma", level: 0.9 },
      ],
    },
  ],
  videos: [
    {
      title: "Lumen — brand film",
      year: "2026",
      description: "60-second brand launch for a freelancer banking platform. Concept, direction, animation, sound.",
      url: "",
      tags: ["motion", "3d", "brand"],
      timecode: "00:01:00:00",
      span: 12,
    },
    {
      title: "Kalimāt — type in motion",
      year: "2025",
      description: "Animated identity for an Arabic poetry reader. RTL typography in motion.",
      url: "",
      tags: ["motion", "type", "identity"],
      timecode: "00:00:42:11",
      span: 7,
    },
    {
      title: "Atlas Notes — promo",
      year: "2025",
      description: "Spatial UI demo film. Cinema 4D + After Effects.",
      url: "",
      tags: ["3d", "ui"],
      timecode: "00:00:30:00",
      span: 5,
    },
    {
      title: "Field Guide — title sequence",
      year: "2024",
      description: "Title sequence for a botanical documentary series.",
      url: "",
      tags: ["motion", "title"],
      timecode: "00:01:12:00",
      span: 6,
    },
    {
      title: "Pace — wearable launch",
      year: "2024",
      description: "Launch campaign for a running watch face. UI motion + product film.",
      url: "",
      tags: ["motion", "ui", "product"],
      timecode: "00:00:55:08",
      span: 6,
    },
  ],
  certs: [
    { title: "Motion Design Master",      issuer: "School of Motion",         year: "2024", image: "" },
    { title: "Advanced Cinema 4D",        issuer: "Greyscalegorilla",         year: "2023", image: "" },
    { title: "Houdini Fundamentals",      issuer: "SideFX",                   year: "2023", image: "" },
    { title: "Brand Identity Design",     issuer: "School of Visual Arts",    year: "2021", image: "" },
    { title: "Typography Workshop",       issuer: "Type@Cooper",              year: "2020", image: "" },
    { title: "Creative Direction",        issuer: "RGD",                      year: "2022", image: "" },
  ],
  cv: {
    headline: "The full reel.",
    summary: "Eight years, twelve studios, two hundred and forty deliverables. Roles, clients, awards — the whole document, in PDF.",
    pdfUrl: "",
    updated: "May 2026",
    docName: "Abdallah Karim",
    docRole: "Multimedia designer · CV",
    docSection1: "// Profile",
    docSection2: "// Experience",
    docSection3: "// Education",
  },
  contact: {
    email: "hello@abdallah.studio",
    lede: "If you've got a brief — or even a fragment of one — send it through. I read every message and respond within 24 hours.",
    socials: [
      { name: "Instagram",  url: "#", handle: "@abdallah.designs" },
      { name: "Behance",    url: "#", handle: "/abdallahkarim" },
      { name: "Vimeo",      url: "#", handle: "/abdallah" },
      { name: "LinkedIn",   url: "#", handle: "/in/abdallah" },
      { name: "Are.na",     url: "#", handle: "@abdallahk" },
    ],
  },
  theme: "lime",  // "lime" | "cyan" | "pink" | "warn"
  signature: "ABDALLAH",
  site: {
    title: "Abdallah — Multimedia Designer",
    description: "Motion · 3D · Brand · Editorial. Selected reel & CV.",
  },

  // ─── All UI chrome strings — every visible label is editable here ─────
  // Title fields use a 3-part split (Pre · Accent · Suf) so the accent
  // highlight stays as its own clickable target in admin mode.
  labels: {
    // Brand (top-left nav text)
    brandTagline: "studio",

    // Nav
    navAbout: "About",
    navSkills: "Skills",
    navReel: "Reel",
    navCerts: "Certs",
    navCv: "CV",
    navContact: "Contact",

    // Hero
    heroRefLabel: "REF",
    heroRefValue: "2026.05.15",
    heroSessionLabel: "SESSION",
    heroStatYearsLbl: "// Years experience",
    heroStatYearsUnit: "years",
    heroStatDeliverablesLbl: "// Deliverables shipped",
    heroStatDeliverablesUnit: "projects",
    heroStatStudiosLbl: "// Studios collaborated",
    heroStatStudiosUnit: "teams",
    heroStatStatusLbl: "// Status",
    heroStatLocationLbl: "// Based in",
    heroStatRoleLbl: "// Role",
    heroDisciplinesLbl: "// Disciplines",

    // About section
    aboutNum: "[ 01 · about ]",
    aboutTitlePre: "A multimedia ",
    aboutTitleAccent: "practice",
    aboutTitleSuf: ".",
    aboutAside1: "Operating",
    aboutAside2: "since 2018",

    // Skills
    skillsNum: "[ 02 · capabilities ]",
    skillsTitlePre: "Tools, ",
    skillsTitleAccent: "fluency",
    skillsTitleSuf: ".",
    skillsAside1: "Continuously",
    skillsAside2: "iterating",

    // Reel
    videosNum: "[ 03 · selected reel ]",
    videosTitlePre: "Recent ",
    videosTitleAccent: "motion",
    videosTitleSuf: ".",
    videosAside1: "Reel updated",
    videosAside2: "2026.05",

    // Certs
    certsNum: "[ 04 · credentials ]",
    certsTitlePre: "Marks of ",
    certsTitleAccent: "study",
    certsTitleSuf: ".",
    certsAside1: "Certifications",
    certsAside2: "& training",

    // CV
    cvNum: "[ 05 · curriculum vitae ]",
    cvTitlePre: "The ",
    cvTitleAccent: "whole",
    cvTitleSuf: " document.",
    cvAside1: "PDF ·",
    cvAside2: "4 pages",
    cvDownloadBtn: "Download PDF",
    cvViewBtn: "View in browser →",
    cvNoFileMsg: "CV available on request.",

    // Contact
    contactNum: "06 · transmission open",
    contactTitleL1: "Let's make",
    contactTitleL2: "things ",
    contactTitleAccent: "move",
    contactTitleSuf: ".",
    contactEmailLbl: "// Email",
    contactSocialsLbl: "// Elsewhere",

    // Footer
    footerTagline: "Studio of one",
    footerTypeLine: "Set in Space Grotesk · JetBrains Mono · Inter",
    footerVersion: "v2.6",
  },
};

// Deep merge defaults with stored data so adding a new field doesn't break
// an old saved state. Arrays are replaced wholesale (not merged) — that's
// what you want for things like skills/projects.
function mergeDefaults(stored, defaults = DEFAULT_DATA) {
  if (stored == null) return defaults;
  if (Array.isArray(defaults) || typeof defaults !== "object") return stored;
  const out = { ...defaults };
  for (const k of Object.keys(defaults)) {
    out[k] = mergeDefaults(stored[k], defaults[k]);
  }
  for (const k of Object.keys(stored)) {
    if (!(k in out)) out[k] = stored[k];
  }
  return out;
}

// ── Cloud availability detection ─────────────────────────────────────────
// The site is "cloud"-backed when /api/session (a Cloudflare Pages
// Function) responds with JSON. That's true once deployed to Cloudflare
// Pages with the /functions directory wired up. A plain static host (or
// opening index.html directly) returns no such route, so we fall back to
// localStorage/passcode mode.
let _cloudCheck = null;
function checkCloud() {
  if (_cloudCheck) return _cloudCheck;
  _cloudCheck = fetch("/api/session", { credentials: "include" })
    .then((res) => {
      const ct = res.headers.get("content-type") || "";
      return ct.includes("application/json");
    })
    .catch(() => false);
  return _cloudCheck;
}
// Kept for callers/UI that want a plain boolean without awaiting; resolves
// once checkCloud() has run at least once (App calls it on boot).
let _cloudAvailable = false;
function isConfigured() {
  return _cloudAvailable;
}

// ── uploadFile ─────────────────────────────────────────────────────────
// Uploads a File/Blob through the Cloudflare Pages Function at /api/upload,
// which stores it in the R2 bucket bound as PORTFOLIO_MEDIA and returns its
// public URL. Falls back to a base64 data URL when the cloud API isn't
// available (LOCAL mode) — works for small images, will balk at large
// videos but at least doesn't crash. onProgress is called with 0..1.
async function uploadFile(file, { folder = "uploads", onProgress } = {}) {
  if (!file) throw new Error("No file provided.");

  if (await checkCloud()) {
    const form = new FormData();
    form.append("file", file);
    form.append("folder", folder);
    if (onProgress) onProgress(0);
    const res = await fetch("/api/upload", { method: "POST", body: form, credentials: "include" });
    const body = await res.json().catch(() => ({}));
    if (!res.ok || !body.ok) throw new Error(body.error || `Upload failed (${res.status}).`);
    if (onProgress) onProgress(1);
    return body.url;
  }

  // Local fallback — base64 data URL. Only sensible for small images.
  // For videos this will blow up localStorage; warn the caller.
  return new Promise((resolve, reject) => {
    if (file.size > 4 * 1024 * 1024) {
      reject(new Error(
        `File too large for local mode (${(file.size / 1024 / 1024).toFixed(1)} MB). ` +
        `Deploy to Cloudflare (see SETUP_CLOUDFLARE.md) to upload large files.`
      ));
      return;
    }
    const reader = new FileReader();
    reader.onload = () => {
      if (onProgress) onProgress(1);
      resolve(reader.result);
    };
    reader.onerror = () => reject(reader.error || new Error("File read failed."));
    reader.readAsDataURL(file);
  });
}

function useStore() {
  const [data, setData] = React.useState(DEFAULT_DATA);
  const [status, setStatus] = React.useState({ mode: "local", ready: false, error: null });

  const writeTimerRef = React.useRef(null);
  const pendingWriteRef = React.useRef(null);

  React.useEffect(() => {
    let cancelled = false;

    function loadLocal() {
      try {
        const raw = localStorage.getItem(LS_KEY);
        if (raw) setData(mergeDefaults(JSON.parse(raw)));
      } catch (e) {
        console.warn("[store] localStorage read failed:", e);
      }
      setStatus((s) => ({ ...s, mode: "local", ready: true }));
    }

    (async () => {
      const cloud = await checkCloud();
      if (cancelled) return;
      _cloudAvailable = cloud;
      if (!cloud) { loadLocal(); return; }

      try {
        const res = await fetch("/api/data");
        const body = await res.json();
        if (cancelled) return;
        if (body) {
          setData(mergeDefaults(body));
        } else {
          // No document yet. DO NOT auto-write defaults — that risks
          // clobbering real data on a transient read hiccup. Show
          // defaults locally; only persist on the first explicit edit.
          console.warn("[cloud] no saved data yet — showing defaults until first edit.");
          setData(DEFAULT_DATA);
        }
        setStatus({ mode: "cloud", ready: true, error: null });
      } catch (e) {
        console.error("[cloud] fetch failed:", e);
        if (!cancelled) { setStatus({ mode: "local", ready: true, error: e.message }); loadLocal(); }
      }
    })();

    return () => { cancelled = true; };
  }, []);

  const persist = React.useCallback((nextData) => {
    if (writeTimerRef.current) clearTimeout(writeTimerRef.current);
    pendingWriteRef.current = nextData;
    writeTimerRef.current = setTimeout(() => {
      const payload = pendingWriteRef.current;
      pendingWriteRef.current = null;
      if (_cloudAvailable) {
        fetch("/api/data", {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          credentials: "include",
          body: JSON.stringify(payload),
        }).catch((e) => console.error("[cloud] write failed:", e));
      } else {
        try { localStorage.setItem(LS_KEY, JSON.stringify(payload)); }
        catch (e) { console.warn("[store] localStorage write failed:", e); }
      }
    }, 350);
  }, []);

  const update = React.useCallback((section, patch) => {
    setData((prev) => {
      const cur = prev[section];
      let next;
      if (Array.isArray(patch) || typeof patch !== "object" || patch === null) {
        next = { ...prev, [section]: patch };
      } else if (Array.isArray(cur) || typeof cur !== "object" || cur === null) {
        next = { ...prev, [section]: patch };
      } else {
        next = { ...prev, [section]: { ...cur, ...patch } };
      }
      persist(next);
      return next;
    });
  }, [persist]);

  const replace = React.useCallback((nextData) => {
    setData(nextData);
    persist(nextData);
  }, [persist]);

  // Export the current data as a JSON file the user can download as a
  // local backup. Reads the current React state — which already reflects
  // the latest Firestore snapshot — so it's always in sync.
  const exportBackup = React.useCallback(() => {
    const payload = {
      __exportedAt: new Date().toISOString(),
      __version: 1,
      data,
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
    a.download = `portfolio-backup-${stamp}.json`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }, [data]);

  // Restore from a JSON backup file. Expects the payload shape produced
  // by exportBackup, but is lenient — also accepts a bare data object.
  const importBackup = React.useCallback(async (file) => {
    if (!file) throw new Error("No file provided.");
    const text = await file.text();
    let parsed;
    try { parsed = JSON.parse(text); }
    catch (e) { throw new Error("File is not valid JSON."); }
    const incoming = parsed && parsed.data && typeof parsed.data === "object" ? parsed.data : parsed;
    if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) {
      throw new Error("Backup file doesn't contain portfolio data.");
    }
    const merged = mergeDefaults(incoming);
    replace(merged);
    return merged;
  }, [replace]);

  return { data, update, replace, status, exportBackup, importBackup };
}

function useAdmin() {
  const [isAdmin, setIsAdmin] = React.useState(false);
  const [showLogin, setShowLogin] = React.useState(false);

  // Bootstrap: ask the Worker if our session cookie is still valid (cloud
  // mode), or check sessionStorage (local passcode fallback).
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      const cloud = await checkCloud();
      if (cancelled) return;
      if (cloud) {
        try {
          const res = await fetch("/api/session", { credentials: "include" });
          const body = await res.json();
          if (!cancelled) setIsAdmin(!!body.isAdmin);
        } catch (e) {
          console.error("[auth] session check failed:", e);
        }
        return;
      }
      try { setIsAdmin(sessionStorage.getItem("admin") === "1"); } catch (e) {}
    })();
    return () => { cancelled = true; };
  }, []);

  // login(password) — always a single password/passcode field. In cloud
  // mode it's checked server-side against the ADMIN_PASSWORD secret; in
  // local mode it's checked against window.ADMIN_PASSCODE.
  // Returns { ok: bool, error?: string }.
  const login = React.useCallback(async (password) => {
    if (await checkCloud()) {
      try {
        const res = await fetch("/api/login", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          credentials: "include",
          body: JSON.stringify({ password }),
        });
        const body = await res.json().catch(() => ({}));
        if (!res.ok || !body.ok) return { ok: false, error: body.error || "Sign-in failed." };
        setIsAdmin(true);
        setShowLogin(false);
        return { ok: true };
      } catch (e) {
        return { ok: false, error: "Network problem. Check your connection." };
      }
    }
    // Local passcode mode
    if (password === (window.ADMIN_PASSCODE || "changeme")) {
      try { sessionStorage.setItem("admin", "1"); } catch (e) {}
      setIsAdmin(true);
      setShowLogin(false);
      return { ok: true };
    }
    return { ok: false, error: "Invalid passcode." };
  }, []);

  const logout = React.useCallback(async () => {
    if (await checkCloud()) {
      try { await fetch("/api/logout", { method: "POST", credentials: "include" }); } catch (e) {}
      setIsAdmin(false);
      return;
    }
    try { sessionStorage.removeItem("admin"); } catch (e) {}
    setIsAdmin(false);
  }, []);

  return { isAdmin, login, logout, showLogin, setShowLogin };
}

window.useStore = useStore;
window.useAdmin = useAdmin;
window.DEFAULT_DATA = DEFAULT_DATA;
window.isCloudConfigured = isConfigured;
window.uploadFile = uploadFile;
window.isCloudStorageAvailable = isConfigured;
