/* PC Liquors — Admin portal shared layer: icons, API client, data hooks,
   per-module field specs and the sales mini-chart.
   Exported on window for the view + shell babel scripts. */

/* ---------- icons (Lucide-style) ---------- */
const A = (p) => <svg width={p.s||18} height={p.s||18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.w||1.8} strokeLinecap="round" strokeLinejoin="round" style={p.style}>{p.children}</svg>;
const Icons = {
  dash:(p)=><A {...p}><path d="M3 13h8V3H3zM13 21h8V3h-8zM3 21h8v-6H3z"/></A>,
  box:(p)=><A {...p}><path d="M21 8l-9-5-9 5 9 5 9-5zM3 8v8l9 5 9-5V8M12 13v8"/></A>,
  users:(p)=><A {...p}><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.9M16 3.1a4 4 0 010 7.8"/></A>,
  receipt:(p)=><A {...p}><path d="M5 2v20l3-2 3 2 3-2 3 2V2l-3 2-3-2-3 2-3-2zM9 8h6M9 12h6"/></A>,
  folder:(p)=><A {...p}><path d="M3 7a2 2 0 012-2h4l2 3h8a2 2 0 012 2v7a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></A>,
  tag:(p)=><A {...p}><path d="M20.6 13.4L13.4 20.6a2 2 0 01-2.8 0l-8-8V3h9.6l8.4 8.4a2 2 0 010 2zM7 7h.01"/></A>,
  megaphone:(p)=><A {...p}><path d="M3 11v2a1 1 0 001 1h2l4 4V6L6 10H4a1 1 0 00-1 1zM14 8a4 4 0 010 8M17.5 5a8 8 0 010 14"/></A>,
  bolt:(p)=><A {...p}><path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z"/></A>,
  settings:(p)=><A {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 00.3 1.8l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.6 1.6 0 00-2.7.6 1.6 1.6 0 01-3.2 0 1.6 1.6 0 00-2.7-.6l-.1.1a2 2 0 11-2.8-2.8l.1-.1a1.6 1.6 0 00-.6-2.7 1.6 1.6 0 010-3.2 1.6 1.6 0 00.6-2.7l-.1-.1a2 2 0 112.8-2.8l.1.1a1.6 1.6 0 001.8.3H9a1.6 1.6 0 001-1.5 1.6 1.6 0 013.2 0 1.6 1.6 0 001 1.5 1.6 1.6 0 001.8-.3l.1-.1a2 2 0 112.8 2.8l-.1.1a1.6 1.6 0 00-.3 1.8V9a1.6 1.6 0 001.5 1 1.6 1.6 0 010 3.2 1.6 1.6 0 00-1.5 1z"/></A>,
  logout:(p)=><A {...p}><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/></A>,
  plus:(p)=><A {...p}><path d="M12 5v14M5 12h14"/></A>,
  search:(p)=><A {...p}><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></A>,
  cal:(p)=><A {...p}><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></A>,
  up:(p)=><A {...p}><path d="M23 6l-9.5 9.5-5-5L1 18M17 6h6v6"/></A>,
  down:(p)=><A {...p}><path d="M23 18l-9.5-9.5-5 5L1 6M17 18h6v-6"/></A>,
  dollar:(p)=><A {...p}><path d="M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></A>,
  card:(p)=><A {...p}><rect x="1" y="4" width="22" height="16" rx="2"/><path d="M1 10h22"/></A>,
  mail:(p)=><A {...p}><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6l-10 7L2 6"/></A>,
  bank:(p)=><A {...p}><path d="M3 21h18M3 10h18M5 6l7-3 7 3M4 10v11M20 10v11M8 14v3M12 14v3M16 14v3"/></A>,
  edit:(p)=><A {...p}><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7M18.5 2.5a2.1 2.1 0 013 3L12 15l-4 1 1-4z"/></A>,
  trash:(p)=><A {...p}><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m2 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6"/></A>,
  download:(p)=><A {...p}><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></A>,
  upload:(p)=><A {...p}><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></A>,
  bell:(p)=><A {...p}><path d="M18 8a6 6 0 00-12 0c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 01-3.4 0"/></A>,
  sync:(p)=><A {...p}><path d="M23 4v6h-6M1 20v-6h6M3.5 9a9 9 0 0114.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0020.5 15"/></A>,
  check:(p)=><A {...p}><path d="M20 6L9 17l-5-5"/></A>,
  doc:(p)=><A {...p}><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8zM14 2v6h6"/></A>,
  lock:(p)=><A {...p}><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></A>,
  user:(p)=><A {...p}><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/></A>,
  shield:(p)=><A {...p}><path d="M12 2l8 4v6c0 5-3.4 8.5-8 10-4.6-1.5-8-5-8-10V6l8-4z"/></A>,
  alert:(p)=><A {...p}><path d="M12 9v4M12 17h.01M10.3 3.9L1.8 18a2 2 0 001.7 3h17a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z"/></A>,
  clock:(p)=><A {...p}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></A>,
  eye:(p)=><A {...p}><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"/><circle cx="12" cy="12" r="3"/></A>,
  activity:(p)=><A {...p}><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></A>,
};

/* ================= API client =================
   One thin wrapper over the serverless functions. Holds the session token in
   localStorage so a refresh keeps you signed in, and signals expiry upward. */
const TOKEN_KEY = "pc_admin_token";
const api = {
  token: () => { try { return localStorage.getItem(TOKEN_KEY); } catch (e) { return null; } },
  setToken(t) { try { t ? localStorage.setItem(TOKEN_KEY, t) : localStorage.removeItem(TOKEN_KEY); } catch (e) {} },
  onExpire: null,

  async call(path, { method = "GET", body } = {}) {
    const headers = {};
    const token = api.token();
    if (token) headers.Authorization = "Bearer " + token;
    if (body !== undefined) headers["Content-Type"] = "application/json";

    const res = await fetch(path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
    let data = null;
    try { data = await res.json(); } catch (e) { data = null; }

    if (res.status === 401) {
      api.setToken(null);
      if (api.onExpire) api.onExpire();
      throw new Error((data && data.error) || "Your session has expired — sign in again.");
    }
    if (!res.ok) throw new Error((data && data.error) || "Request failed (" + res.status + ")");
    return data;
  },

  login: (uid, password) => api.call("/api/admin-login", { method: "POST", body: { uid, password } }),
  me: () => api.call("/api/admin?action=me"),
  summary: () => api.call("/api/admin?action=summary"),
  auditLog: () => api.call("/api/admin?action=audit"),
  changePassword: (currentPassword, newPassword) =>
    api.call("/api/admin?action=password", { method: "POST", body: { currentPassword, newPassword } }),

  list: (entity) => api.call("/api/admin?action=list&entity=" + entity).then((d) => d.rows || []),
  save: (entity, values, id) =>
    api.call("/api/admin?action=save&entity=" + entity + (id ? "&id=" + encodeURIComponent(id) : ""),
      { method: "POST", body: values }),
  remove: (entity, id) =>
    api.call("/api/admin?action=delete&entity=" + entity + "&id=" + encodeURIComponent(id), { method: "POST" }),

  listAds: () => api.call("/api/ads").then((d) => d.ads || []),
  saveAd: (values, id) => api.call("/api/ads" + (id ? "?id=" + encodeURIComponent(id) : ""),
    { method: id ? "PATCH" : "POST", body: values }),
  removeAd: (id) => api.call("/api/ads?id=" + encodeURIComponent(id), { method: "DELETE" }),

  users: () => api.call("/api/admin?action=users").then((d) => d.users || []),
  createUser: (values) => api.call("/api/admin?action=users", { method: "POST", body: values }),
  updateUser: (uid, patch) =>
    api.call("/api/admin?action=users&id=" + encodeURIComponent(uid), { method: "PATCH", body: patch }),

  upload: (payload) => api.call("/api/admin?action=upload", { method: "POST", body: payload }),
  docUrl: (id) => api.call("/api/admin?action=doc-url&id=" + encodeURIComponent(id)),
};

/* ================= data hook =================
   Loads a module's rows once, exposes save/remove that keep the list in sync. */
function useCollection(entity) {
  const [rows, setRows] = React.useState(null);   // null = still loading
  const [error, setError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);

  const load = React.useCallback(() => {
    setError(null);
    const fetcher = entity === "ads" ? api.listAds() : api.list(entity);
    return fetcher.then(setRows).catch((e) => { setRows([]); setError(e.message); });
  }, [entity]);

  React.useEffect(() => { load(); }, [load]);

  const save = async (values, id) => {
    setBusy(true); setError(null);
    try {
      if (entity === "ads") await api.saveAd(values, id); else await api.save(entity, values, id);
      await load();
      return true;
    } catch (e) { setError(e.message); return false; }
    finally { setBusy(false); }
  };

  const remove = async (id) => {
    setBusy(true); setError(null);
    try {
      if (entity === "ads") await api.removeAd(id); else await api.remove(entity, id);
      await load();
      return true;
    } catch (e) { setError(e.message); return false; }
    finally { setBusy(false); }
  };

  return { rows, error, busy, reload: load, save, remove, setError };
}

/* ================= formatting ================= */
const money = (n) => "$" + Number(n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const money0 = (n) => "$" + Math.round(Number(n || 0)).toLocaleString("en-US");
const shortDate = (v) => {
  if (!v) return "—";
  const d = new Date(v.length === 10 ? v + "T12:00:00" : v);
  if (isNaN(d)) return "—";
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
};
const dateInput = (v) => (v ? String(v).slice(0, 10) : "");
const dtInput = (v) => {
  if (!v) return "";
  const d = new Date(v);
  if (isNaN(d)) return "";
  const pad = (n) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const bytes = (n) => {
  const b = Number(n || 0);
  if (!b) return "—";
  if (b < 1024) return b + " B";
  if (b < 1048576) return (b / 1024).toFixed(0) + " KB";
  return (b / 1048576).toFixed(1) + " MB";
};

/* ================= module field specs =================
   Each module's add/edit form is generated from this, so the dialog, the
   payload and the column whitelist on the server stay in step. */
const CATEGORIES = ["Whiskey", "Wine", "Vodka", "Gin", "Tequila", "Rum", "Cognac", "Beer", "Seltzer", "Mixers"];
const TONES = ["whiskey", "wine", "vodka", "gin", "tequila", "rum"];

const FIELDS = {
  products: [
    { k:"name", label:"Product name", required:true, placeholder:"Buffalo Trace" },
    { k:"sku", label:"SKU", required:true, mono:true, placeholder:"BT-750-001", half:true },
    { k:"category", label:"Category", type:"select", options:CATEGORIES, half:true },
    { k:"kind", label:"Shelf description", placeholder:"Kentucky Bourbon, 750ml" },
    { k:"price", label:"Retail price", type:"number", step:"0.01", required:true, def:0, half:true },
    { k:"cost", label:"Unit cost", type:"number", step:"0.01", half:true },
    { k:"stock", label:"On hand", type:"number", required:true, def:0, half:true },
    { k:"reorder_at", label:"Reorder at", type:"number", def:6, half:true, hint:"Flagged Low below this" },
    { k:"status", label:"Status", type:"select", options:["In stock","Low","Out"], half:true },
    { k:"tone", label:"Bottle colour", type:"select", options:TONES, half:true, hint:"Storefront card" },
    { k:"supplier", label:"Supplier", placeholder:"Southern Glazer's" },
    { k:"featured", label:"Feature on the storefront home page", type:"switch", def:false },
  ],
  employees: [
    { k:"name", label:"Full name", required:true, placeholder:"Jordan Ellis" },
    { k:"email", label:"Email", type:"email", half:true, placeholder:"jordan@pcliquors.com" },
    { k:"phone", label:"Phone", half:true, placeholder:"(737) 205-7029" },
    { k:"role", label:"Role", type:"select", half:true,
      options:["Store Manager","Sales Associate","Cashier","Stock & Delivery"] },
    { k:"hired_on", label:"Hired on", type:"date", half:true },
    { k:"hourly_rate", label:"Rate / hr", type:"number", step:"0.01", half:true },
    { k:"hours_ytd", label:"Hours this period", type:"number", step:"0.5", def:0, half:true },
    { k:"active", label:"Currently employed", type:"switch" },
  ],
  expenses: [
    { k:"date", label:"Date", type:"date", required:true, half:true },
    { k:"type", label:"Direction", type:"select", half:true,
      options:[{v:"out",t:"Money out"},{v:"in",t:"Money in"}] },
    { k:"description", label:"Description", required:true, placeholder:"Southern Glazer's — restock" },
    { k:"amount", label:"Amount", type:"number", step:"0.01", required:true, half:true },
    { k:"source", label:"Source", type:"select", half:true,
      options:[{v:"bank",t:"Bank"},{v:"card",t:"Credit card"},{v:"pos",t:"Clover POS"},{v:"mail",t:"Email invoice"}] },
    { k:"channel", label:"Channel note", placeholder:"Bank ACH" },
  ],
  utilities: [
    { k:"name", label:"Service", required:true, placeholder:"Electricity" },
    { k:"provider", label:"Provider", required:true, placeholder:"TXU Energy" },
    { k:"category", label:"Type", type:"select", half:true,
      options:["electricity","water","gas","internet","phone","waste","alarm","pos","insurance","rent","software","other"] },
    { k:"location", label:"Location", type:"select", half:true, options:["Palm Valley","Pflugerville","Both"] },
    { k:"account_number", label:"Account number", mono:true, half:true },
    { k:"amount", label:"Amount", type:"number", step:"0.01", half:true },
    { k:"billing_cycle", label:"Billing cycle", type:"select", half:true,
      options:["monthly","quarterly","annual","one-time"] },
    { k:"next_due_on", label:"Next due", type:"date", half:true },
    { k:"status", label:"Status", type:"select", half:true, options:["active","pending","overdue","cancelled"] },
    { k:"autopay", label:"On autopay", type:"switch", def:false, half:true },
    { k:"notes", label:"Notes", type:"textarea" },
  ],
  ads: [
    { k:"title", label:"Offer name", required:true, placeholder:"July 4th whiskey sale",
      hint:"Internal name + banner headline" },
    { k:"label", label:"Label on the site", required:true, placeholder:"15% OFF",
      hint:"The badge shown on the storefront — keep it short" },
    { k:"message", label:"Banner message", type:"textarea",
      placeholder:"15% off all American whiskey, in-store and online, through July 6." },
    { k:"tone", label:"Colour", type:"select", half:true,
      options:[{v:"gold",t:"Gold"},{v:"wine",t:"Wine"},{v:"success",t:"Green"},{v:"info",t:"Blue"},
               {v:"warning",t:"Amber"},{v:"danger",t:"Red"},{v:"neutral",t:"Neutral"}] },
    { k:"placement", label:"Where it shows", type:"select", half:true,
      options:[{v:"banner",t:"Top banner only"},{v:"product",t:"Product labels only"},{v:"both",t:"Banner + product labels"}] },
    { k:"target_category", label:"Applies to category", type:"select", half:true,
      options:[{v:"",t:"All categories"}].concat(CATEGORIES.map((c)=>({v:c,t:c}))) },
    { k:"target_product", label:"…or one product", half:true, placeholder:"Buffalo Trace",
      hint:"Exact product name; leave blank for the whole category" },
    { k:"cta_label", label:"Button text", half:true, placeholder:"Shop the sale" },
    { k:"cta_route", label:"Button goes to", type:"select", half:true,
      options:[{v:"shop",t:"Shop"},{v:"home",t:"Home"},{v:"contact",t:"Visit & Contact"},{v:"faq",t:"Policies & FAQ"}] },
    { k:"starts_at", label:"Starts", type:"datetime", half:true, hint:"Blank = immediately" },
    { k:"ends_at", label:"Ends", type:"datetime", half:true, hint:"Blank = runs until switched off" },
    { k:"priority", label:"Priority", type:"number", def:0, half:true, hint:"Higher wins the banner slot" },
    { k:"is_active", label:"Live on the storefront", type:"switch", half:true },
  ],
};

/* ---------- sales mini-chart (POS actuals + forecast) ---------- */
const SALES_14 = [3120,2890,3340,4010,3760,5210,6040,3480,3110,3920,4280,0,0,0];
const PRED_14  = [0,0,0,0,0,0,0,0,0,0,4280,4520,4980,6300];
const DAYS_14  = ["Jun 17","18","19","20","21","22","23","24","25","26","27","28","29","30"];

function SalesChart() {
  const all = SALES_14.map((v,i)=> v || PRED_14[i]);
  const max = Math.max(...all);
  const W = 720, H = 240, pad = 28, n = all.length;
  const bw = (W - pad*2) / n;
  return (
    <svg viewBox={`0 0 ${W} ${H}`} className="achart" preserveAspectRatio="none">
      {[0.25,0.5,0.75,1].map((g,i)=>(
        <line key={i} x1={pad} x2={W-pad} y1={pad + (H-pad*2)*(1-g)} y2={pad + (H-pad*2)*(1-g)} stroke="var(--border-subtle)" strokeWidth="1" strokeDasharray="2 4"/>
      ))}
      {all.map((v,i)=>{
        const h = (v/max)*(H-pad*2);
        const x = pad + i*bw + bw*0.18;
        const w = bw*0.64;
        const y = H-pad-h;
        const pred = !SALES_14[i];
        return <rect key={i} x={x} y={y} width={w} height={h} rx="3"
          fill={pred ? "transparent" : "var(--gold-500)"} stroke={pred ? "var(--gold-500)" : "none"}
          strokeDasharray={pred ? "3 3" : "0"} strokeWidth={pred?1.4:0} opacity={pred?0.85:1}/>;
      })}
      {DAYS_14.map((d,i)=> i%2===0 ? (
        <text key={i} x={pad + i*bw + bw/2} y={H-8} textAnchor="middle" fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-subtle)">{d}</text>
      ) : null)}
    </svg>
  );
}

window.PCAdmin = {
  Icons, api, useCollection, FIELDS, CATEGORIES,
  money, money0, shortDate, dateInput, dtInput, bytes,
  SALES_14, PRED_14, DAYS_14, SalesChart,
};
