/* ============================================================
   منصة مسابقات خبرة — الطبقة المشتركة
   نموذج البيانات، الترحيل، الاستعلامات، وأدوات العرض.
   ============================================================ */

window.EXP = (function(){

const STORE_KEY = "exp_platform_data";
const LEGACY_KEY = "exp_ambassadors_data";

/* ---------- حالات المسابقة ---------- */
const STATUS = {
  draft:    { id:"draft",    label:"مسودة",        short:"مسودة",   color:"#737373", icon:"✏️", public:false },
  voting:   { id:"voting",   label:"التصويت مفتوح", short:"تصويت",   color:"#00AAA4", icon:"🗳️", public:true  },
  results:  { id:"results",  label:"النتائج معلنة", short:"معلنة",   color:"#57BC59", icon:"🏆", public:true  },
  archived: { id:"archived", label:"مؤرشفة",       short:"أرشيف",   color:"#1A6D92", icon:"📦", public:true  }
};
const STATUS_ORDER = ["voting","results","draft","archived"];

/* ---------- قوالب فارغة ---------- */
function blankCompetition(i){
  return {
    id: "comp-" + (i || 1),
    title: "مسابقة جديدة",
    slogan: "",
    label: "",
    intro: "",
    status: "draft",
    accent: "#00AAA4",
    icon: "🏆",
    topN: 3,
    displaySeconds: 12,
    sectors: {},
    sectorInfo: {},
    categories: [],
    winners: [],
    scoreMode: "manual",
    revealAt: "",          /* موعد إعلان النتائج — قبله تُخفى وتظهر شاشة ترقّب */
    teaser: "",            /* نص التشويق المعروض أثناء العد التنازلي */
    cover: "",             /* صورة الشريحة التعريفية في شاشة العرض */
    coverText: "",         /* نص الشريحة التعريفية — يقع تحت العنوان */
    coverSeconds: 8,       /* مدة بقاء الشريحة التعريفية قبل الانتقال للمرشحين */
    coverFit: "cover",     /* cover = ملء الإطار · contain = إظهارها كاملة */
    coverSize: "m",        /* s · m · l · full (ملء الشاشة والنص فوقها) */
    prize: { note:"", perks:[] },
    vote: { enabled:false, system:"platform", mode:"single", url:"", label:"صوّت له",
            note:"", deadline:"", requireId:true, showCounts:true, allowList:[], domain:"" },
    ui: JSON.parse(JSON.stringify(UI_DEFAULTS))
  };
}

/* ============================================================
   تخصيص الواجهة — كل نص وكل قسم يتحكم فيه المسؤول
   ============================================================ */
const UI_DEFAULTS = {
  /* إظهار/إخفاء أقسام الواجهة */
  showVote:    true,
  showFilter:  true,
  showStats:   true,
  showSectors: true,
  showWeights: true,
  showPrize:   true,
  showChampion:true,

  /* نصوص الواجهة */
  labels: {
    navResults:  "النتائج",
    navSectors:  "القطاعات",
    navWeights:  "المؤشرات",
    navPrize:    "الجائزة",
    ctaResults:  "استعراض النتائج",
    ctaWeights:  "آلية التقييم",
    backAll:     "كل المسابقات",
    filterLabel: "تصفية حسب القطاع",
    filterAll:   "الكل",
    sectorsTitle:"تقسيم المنافسة",
    sectorsDesc: "المنافسة مقسّمة حسب القطاع حتى تكون عادلة، لأن طبيعة المحافظ والمستهدفات متقاربة داخل كل قطاع.",
    weightsTitle:"المؤشرات والأوزان",
    weightsDesc: "آلية احتساب النتيجة النهائية لكل فئة.",
    prizeTitle:  "الجائزة الكبرى",
    championText:"أعلى رقم بين الجميع",
    scoreLabel:  "النتيجة الإجمالية",
    candidateWord:"مرشّح",
    winnerWord:  "فائز",
    winnersWord: "فائزين",
    liveText:    "النتائج تُحدَّث مباشرة",
    lastUpdate:  "آخر تحديث",
    refreshNow:  "تحديث الآن",
    voteOpen:    "التصويت مفتوح",
    voteClosed:  "انتهى التصويت",
    voteMissing: "رابط التصويت غير متاح",
    noResults:   "لا توجد نتائج في هذا القطاع لهذه الفئة."
  }
};

function normalizeUI(ui){
  const d = JSON.parse(JSON.stringify(UI_DEFAULTS));
  if(!ui || typeof ui !== "object") return d;
  for(const k of Object.keys(d)){
    if(k === "labels") continue;
    if(typeof ui[k] === "boolean") d[k] = ui[k];
  }
  if(ui.labels && typeof ui.labels === "object"){
    for(const k of Object.keys(d.labels)){
      if(typeof ui.labels[k] === "string" && ui.labels[k].trim() !== "") d.labels[k] = ui.labels[k];
    }
  }
  return d;
}

/* بصمة نصية بسيطة — لتوليد معرّف ثابت للمرشح */
function hashStr(s){
  let h = 0;
  s = String(s||"");
  for(let i=0;i<s.length;i++) h = (h*31 + s.charCodeAt(i))|0;
  return h;
}

/* ---------- الترحيل والتطبيع ---------- */
function normalizeCompetition(c, i){
  const b = blankCompetition(i+1);
  c = Object.assign({}, b, c || {});
  c.sectors    = c.sectors    || {};
  c.sectorInfo = c.sectorInfo || {};
  c.categories = Array.isArray(c.categories) ? c.categories : [];
  c.winners    = Array.isArray(c.winners)    ? c.winners    : [];
  c.prize      = Object.assign({note:"",perks:[]}, c.prize || {});
  c.prize.perks= Array.isArray(c.prize.perks) ? c.prize.perks : [];
  c.vote       = Object.assign(b.vote, c.vote || {});
  c.vote.allowList = Array.isArray(c.vote.allowList) ? c.vote.allowList : [];
  c.ui         = normalizeUI(c.ui);
  c.scoreMode  = (c.scoreMode === "computed") ? "computed" : "manual";
  c.revealAt   = typeof c.revealAt === "string" ? c.revealAt : "";
  c.teaser     = typeof c.teaser   === "string" ? c.teaser   : "";
  c.cover      = typeof c.cover     === "string" ? c.cover     : "";
  c.coverText  = typeof c.coverText === "string" ? c.coverText : "";
  c.coverSeconds = Number(c.coverSeconds) > 0 ? Number(c.coverSeconds) : 8;
  c.coverFit  = c.coverFit === "contain" ? "contain" : "cover";
  c.coverSize = ["s","m","l","full"].indexOf(c.coverSize) > -1 ? c.coverSize : "m";
  /* معرّف ثابت لكل مرشح — الأصوات مرتبطة به */
  c.winners.forEach((w,i)=>{
    if(!w.uid) w.uid = "c" + Math.abs(hashStr((w.name||"") + "|" + (w.cat||"") + "|" + i)).toString(36)
                       + i.toString(36);
    if(w.votes == null) w.votes = 0;
  });
  c.topN       = Number(c.topN) > 0 ? Number(c.topN) : 3;
  c.displaySeconds = Number(c.displaySeconds) > 0 ? Number(c.displaySeconds) : 12;
  if(!STATUS[c.status]) c.status = "draft";
  c.categories.forEach(cat=>{ cat.metrics = Array.isArray(cat.metrics) ? cat.metrics : []; });
  return c;
}

/* ============================================================
   احتساب النتيجة — قاعدة واحدة، بلا مفتاح يدوي/تلقائي
   ------------------------------------------------------------
   • النوع له مؤشرات بأوزان  →  النتيجة = مجموع (وزن × قيمة)
     ومؤشر التصويت يأخذ قيمته من أصوات الموظفين الفعلية.
   • النوع بلا مؤشرات        →  النتيجة هي النسبة التي يكتبها المسؤول.
   ============================================================ */

const VOTE_RE = /تصويت|تصويتات|أصوات|vote|voting/i;
const isVoteMetric = k => VOTE_RE.test(String(k||""));

/* نصيب المرشح من التصويت داخل فئته: 1 لصاحب أعلى أصوات */
function voteShare(comp, w){
  const list = (comp.winners||[]).filter(x => x.cat === w.cat);
  const max = list.reduce((m,x)=> Math.max(m, Number(x.votes)||0), 0);
  if(!max) return 0;
  return (Number(w.votes)||0) / max;
}

/* عدد أصوات فئة كاملة */
function catVotes(comp, catId){
  return (comp.winners||[])
    .filter(w => w.cat === catId)
    .reduce((a,w)=> a + (Number(w.votes)||0), 0);
}

/* قيمة مؤشر معيّن لمرشح */
function metricValue(comp, w, metric){
  if(isVoteMetric(metric.k)) return voteShare(comp, w);
  const d = (w.details||[]).find(x => String(x.k||"").trim() === String(metric.k||"").trim());
  return d ? (Number(d.v)||0) : 0;
}

/* نقاط المؤشر كما يفهمها المسؤول: ما ناله من وزن المؤشر.
   مؤشر وزنه 80 ونسبة إنجازه 100% ← 80 نقطة، والمجموع يصل إلى 100. */
function points(b){
  const w = Number(b && b.w) || 0;
  const v = Number(b && b.v) || 0;
  if(w <= 0) return pct(v);                       // بلا وزن: نعرضها نسبة
  const max = Math.round(w * 100);
  const got = Math.round(v * max * 10) / 10;
  return got + " / " + max;
}

/* هل يُحتسب هذا النوع من مؤشراته؟ — نعم متى وُجد مؤشر واحد له وزن */
function isWeighted(comp, catId){
  const cat = (comp.categories||[]).find(c => c.id === catId);
  return !!(cat && (cat.metrics||[]).some(m => (Number(m.w)||0) > 0));
}

/* النتيجة النهائية للمرشح */
function score(comp, w){
  if(!comp || !w) return 0;
  const cat = (comp.categories||[]).find(c => c.id === w.cat);
  if(!cat || !isWeighted(comp, w.cat)) return Number(w.score)||0;

  let total = 0;
  cat.metrics.forEach(m => { total += (Number(m.w)||0) * metricValue(comp, w, m); });
  return Math.max(0, Math.min(1, total));
}

/* تفصيل المؤشرات المعروض على البطاقة */
function breakdown(comp, w){
  const cat = (comp.categories||[]).find(c => c.id === w.cat);
  if(isWeighted(comp, w.cat) && cat){
    return cat.metrics.map(m => ({
      k: m.k,
      v: metricValue(comp, w, m),
      vote: isVoteMetric(m.k),
      w: Number(m.w)||0
    }));
  }
  return (Array.isArray(w.details) ? w.details : []).filter(d => d && d.k)
    .map(d => ({k:d.k, v:Number(d.v)||0, vote:isVoteMetric(d.k), w:0}));
}

/* هل يوجد مؤشر تصويت ضمن الأوزان؟ يُستخدم للتنبيه في لوحة التحكم */
function voteWeight(comp){
  let total = 0, found = false;
  (comp.categories||[]).forEach(cat=>{
    (cat.metrics||[]).forEach(m=>{
      if(/تصويت|vote/i.test(String(m.k||""))){ found = true; total += Number(m.w)||0; }
    });
  });
  return {found, avg: found && comp.categories.length ? total / comp.categories.length : 0};
}

/* ============================================================
   موعد إعلان النتائج
   قبل الموعد تُخفى النتائج ويظهر عدّ تنازلي على شاشة العرض
   ============================================================ */
function revealTime(comp){
  if(!comp || !comp.revealAt) return null;
  const t = new Date(comp.revealAt);
  return isNaN(t.getTime()) ? null : t;
}
/* شاشة الترقّب تخصّ إعلان النتائج وحده.
   أثناء «التصويت مفتوح» يجب أن يرى الموظفون المرشحين ليصوّتوا لهم،
   فلا يُخفيهم العدّ التنازلي مهما كان موعد الإعلان. */
function beforeReveal(comp){
  if(!comp || comp.status !== "results") return false;
  const t = revealTime(comp);
  return !!t && Date.now() < t.getTime();
}
function untilReveal(comp){
  const t = revealTime(comp);
  if(!t) return 0;
  return Math.max(0, Math.floor((t.getTime() - Date.now())/1000));
}

/* التصويت يظهر فقط إذا فعّله المسؤول من الواجهة ومن إعدادات التصويت */
/* خيار التصويت مفعّل في الإعدادات — تستخدمه لوحة التحكم */
function voteVisible(comp){
  return !!(comp.ui && comp.ui.showVote) && !!(comp.vote && comp.vote.enabled);
}
/* مرحلة التصويت جارية — عندها تُعرض أدوات التصويت في الواجهة */
function voteStage(comp){
  return voteVisible(comp) && comp.status === "voting";
}
/* يُقبل الصوت الآن فعلًا — نفس شروط الخادم في api/vote.php */
function voteLive(comp){
  return voteStage(comp) && !voteClosed(comp);
}
function L(comp, key){
  return (comp && comp.ui && comp.ui.labels && comp.ui.labels[key]) || UI_DEFAULTS.labels[key] || "";
}

/* بيانات النسخة الأولى كانت مسابقة واحدة مسطّحة — نلفّها في منصة */
function migrate(raw){
  if(!raw || typeof raw !== "object") raw = {};

  if(!Array.isArray(raw.competitions)){
    const one = normalizeCompetition({
      id: "comp-1",
      title: (raw.period && raw.period.title) || "المسابقة الأولى",
      slogan:(raw.period && raw.period.slogan) || "",
      label: (raw.period && raw.period.label)  || "",
      intro: (raw.period && raw.period.intro)  || "",
      status: raw.stage === "voting" ? "voting" : "results",
      topN: raw.topN,
      displaySeconds: raw.displaySeconds,
      sectors: raw.sectors, sectorInfo: raw.sectorInfo,
      categories: raw.categories, winners: raw.winners,
      prize: raw.prize, vote: raw.vote
    }, 0);
    raw = {
      version: 2,
      org: { name:"خبرة الإعلانية", platform:"منصة خبرة",
             en:"KHIBRAH ADVERTISING",
             tagline:"نُبرز الكفاءات، ونصنع المنافسة العادلة." },
      live: { enabled:true, intervalSeconds:20, showDeltas:true },
      activeId: one.id,
      competitions: [one]
    };
  }

  raw.version = 2;
  raw.org  = Object.assign({name:"خبرة الإعلانية", platform:"منصة خبرة",
                            en:"KHIBRAH ADVERTISING",
                            tagline:"نُبرز الكفاءات، ونصنع المنافسة العادلة."}, raw.org || {});
  raw.live = Object.assign({enabled:true, intervalSeconds:20, showDeltas:true}, raw.live || {});
  raw.competitions = raw.competitions.map(normalizeCompetition);
  if(!raw.competitions.some(c=>c.id === raw.activeId))
    raw.activeId = (raw.competitions[0]||{}).id || "";
  return raw;
}

/* ============================================================
   مصادر البيانات
   ① قاعدة البيانات عبر api/data.php  ← المصدر الرسمي
   ② ملف data.js                      ← احتياطي إذا تعذّر الاتصال
   ============================================================ */

const API = "api/";
let ONLINE = null;        // null = لم يُعرف بعد · true/false بعد أول محاولة
let LAST_ERROR = "";

function isOnline(){ return ONLINE === true; }
function lastError(){ return LAST_ERROR; }

/* التحميل المحلي الفوري — يُستخدم للرسم الأولي قبل وصول الشبكة */
function loadLocal(){
  let raw = null;
  try{
    const s = localStorage.getItem(STORE_KEY);
    if(s) raw = JSON.parse(s);
    if(!raw){
      const old = localStorage.getItem(LEGACY_KEY);
      if(old) raw = JSON.parse(old);
    }
  }catch(e){}
  if(!raw) raw = window.EXP_DATA;
  return migrate(raw ? JSON.parse(JSON.stringify(raw)) : {});
}
const load = loadLocal;      /* توافق مع الاستدعاءات القائمة */

/* التحميل من قاعدة البيانات */
function fetchRemote(){
  return fetch(API + "data.php", {
    credentials: "same-origin",
    headers: {"Accept": "application/json"},
    cache: "no-store"
  })
  .then(r => {
    if(!r.ok) throw new Error("HTTP " + r.status);
    return r.json();
  })
  .then(j => {
    if(j && j.ok === false) throw new Error(j.error || "خطأ");
    ONLINE = true; LAST_ERROR = "";
    const d = migrate(j);
    try{ localStorage.setItem(STORE_KEY, JSON.stringify(d)); }catch(e){}   /* نسخة للعمل دون اتصال */
    return d;
  })
  .catch(e => {
    ONLINE = false;
    LAST_ERROR = e && e.message ? e.message : "تعذّر الاتصال";
    return null;
  });
}

/* التحميل الموصى به للصفحات: قاعدة البيانات ثم الاحتياطي */
function loadAsync(){
  return fetchRemote().then(d => d || loadLocal());
}

/* الحفظ في قاعدة البيانات */
function saveRemote(d, csrf){
  return fetch(API + "save.php", {
    method: "POST",
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": csrf || ""
    },
    body: JSON.stringify(d)
  })
  .then(r => r.json().catch(()=>({ok:false, error:"رد غير مفهوم من الخادم"})))
  .then(j => {
    if(!j || j.ok !== true) throw new Error((j && j.error) || "تعذّر الحفظ");
    ONLINE = true;
    return j;
  });
}

/* الحفظ المحلي — مسوّدة احتياطية داخل المتصفح */
function save(d){
  try{ localStorage.setItem(STORE_KEY, JSON.stringify(d)); return true; }
  catch(e){ return false; }
}
function clearDraft(){
  try{ localStorage.removeItem(STORE_KEY); localStorage.removeItem(LEGACY_KEY); }catch(e){}
}

/* ---------- الاستعلامات ---------- */
function byId(d, id){ return (d.competitions||[]).find(c=>c.id === id) || null; }

/* المسابقات الظاهرة للجمهور، مرتبة: تصويت ثم معلنة ثم أرشيف */
function publicList(d){
  return (d.competitions||[])
    .filter(c => STATUS[c.status] && STATUS[c.status].public)
    .sort((a,b)=> STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status));
}

/* المسابقة المطلوبة: من الرابط ?c=  ثم النشطة ثم أول مسابقة معروضة */
function pick(d, param){
  const id = param || new URLSearchParams(location.search).get("c");
  return byId(d, id) || byId(d, d.activeId) || publicList(d)[0] || (d.competitions||[])[0] || null;
}

/* ترتيب المرشحين داخل فئة */
function ranked(comp, catId, sector, limit){
  const n = limit != null ? limit : (Number(comp.topN) > 0 ? Number(comp.topN) : 3);
  return (comp.winners||[])
    .filter(w => w.cat === catId)
    .filter(w => !sector || sector === "all" || w.sector === sector)
    .sort((a,b)=> score(comp,b) - score(comp,a))
    .slice(0, n);
}

/* المركز داخل الفئة لكل مرشح — مفهرس بموضعه في المصفوفة */
function rankIndex(comp){
  const m = new Map();
  (comp.categories||[]).forEach(cat=>{
    (comp.winners||[]).map((w,i)=>({w,i})).filter(x=>x.w.cat===cat.id)
      .sort((a,b)=> score(comp,b.w) - score(comp,a.w))
      .forEach((x,r)=> m.set(x.i, r));
  });
  return m;
}

function stats(comp){
  const cats = (comp.categories||[]).length;
  const secs = Object.keys(comp.sectors||{}).length;
  return {
    categories: cats,
    sectors: secs,
    candidates: (comp.winners||[]).length,
    ambassadors: cats * Math.max(1, secs),
    shown: (comp.categories||[]).reduce((a,c)=> a + ranked(comp, c.id).length, 0),
    withPhoto: (comp.winners||[]).filter(w=>w.photo).length
  };
}

/* أعلى نتيجة في المسابقة كاملة — صاحب جائزة الرئيس التنفيذي */
function topOverall(comp){
  return (comp.winners||[]).slice()
    .sort((a,b)=> score(comp,b) - score(comp,a))[0] || null;
}

/* ---------- أدوات العرض ---------- */
const pct = n => Math.round((Number(n)||0)*100) + "%";
const pct1= n => (Math.round((Number(n)||0)*1000)/10).toFixed(1).replace(/\.0$/,"") + "%";
const esc = s => String(s==null?"":s)
  .replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));

const MEDAL = ["🥇","🥈","🥉"];
const PLACE = ["الأول","الثاني","الثالث","الرابع","الخامس"];
const RANK_LIGHT = [
  "--rk-t:#8A6B22;--rk-b:linear-gradient(135deg,rgba(243,211,145,.5),rgba(217,180,95,.22));--rk-l:rgba(217,180,95,.55)",
  "--rk-t:#5F6D70;--rk-b:rgba(185,196,198,.22);--rk-l:rgba(185,196,198,.55)",
  "--rk-t:#8A5A32;--rk-b:rgba(192,133,82,.16);--rk-l:rgba(192,133,82,.45)"
];
const RANK_DARK = [
  "--rk-t:#F7E3B4;--rk-b:linear-gradient(135deg,rgba(243,211,145,.28),rgba(217,180,95,.12));--rk-l:rgba(243,211,145,.5)",
  "--rk-t:#DCE4E5;--rk-b:rgba(185,196,198,.16);--rk-l:rgba(185,196,198,.4)",
  "--rk-t:#E8C3A0;--rk-b:rgba(192,133,82,.16);--rk-l:rgba(192,133,82,.4)"
];

const catName = t => String(t||"").replace(/^سفير خبرة\s*[—-]\s*/,"");
const initial = n => String(n||"?").trim().charAt(0) || "?";

/* ============================================================
   الأيقونات — تقبل إيموجي أو صورة مرفوعة من الجهاز
   ============================================================ */
function isImageIcon(v){
  const s = String(v||"").trim();
  return s.startsWith("data:image/") || /^uploads\/[\w.-]+$/.test(s);
}

/* الأيقونة: إيموجي أو صورة مرفوعة — بمقاس واحد في الحالتين.
   الصورة تُحصر في مربّع بحجم الإيموجي فلا تكبر ولا تشوّه الصف. */
function iconHTML(v, size){
  const s = String(v||"").trim();
  if(!s) return "";
  const box = size || "1.25em";
  if(isImageIcon(s)){
    return `<img src="${esc(s)}" alt="" class="ico-img"` +
           ` style="width:${box};height:${box}">`;
  }
  return `<span class="ico-em" style="width:${box};height:${box};font-size:${box}">${esc(s)}</span>`;
}

/* رابط تصويت المرشح حسب وضع المسابقة */
function voteUrl(comp, w){
  const v = comp.vote || {};
  if(!v.enabled) return "";
  return v.mode === "per" ? (w.voteUrl || "") : (v.url || "");
}
function voteClosed(comp){
  const v = comp.vote || {};
  return !!(v.deadline && new Date(v.deadline) < new Date());
}

/* حقن الشعار في كل عنصر .logo */
function mountLogos(){
  if(!window.KHIBRAH_LOGO) return;
  document.querySelectorAll(".logo").forEach(el=>{
    if(!el.querySelector("svg")) el.innerHTML = window.KHIBRAH_LOGO;
  });
}

/* ---------- جلسة المسؤول ---------- */
function session(){
  return fetch(API + "session.php", {credentials:"same-origin", cache:"no-store"})
    .then(r => r.json())
    .then(j => (j && j.ok) ? j : {authed:false})
    .catch(()=> ({authed:false, offline:true}));
}
function apiLogin(user, pass){
  return fetch(API + "login.php", {
    method:"POST", credentials:"same-origin",
    headers:{"Content-Type":"application/json"},
    body: JSON.stringify({user, pass})
  })
  .then(r => r.json()
    .then(j => {
      /* رسالة الخادم إن وُجدت، وإلا رسالة مناسبة لرمز الحالة */
      if(j && typeof j.ok === "boolean") return j;
      return {ok:false, error: httpMessage(r.status)};
    })
    .catch(()=> ({ok:false, error: httpMessage(r.status)}))
  )
  .catch(()=> ({ok:false, error:"تعذّر الوصول إلى الخادم — تأكد من الاتصال بالإنترنت"}));
}

/* ترجمة رموز الحالة إلى رسائل واضحة */
function httpMessage(status){
  if(status === 401 || status === 403) return "اسم المستخدم أو كلمة المرور غير صحيحة";
  if(status === 429) return "محاولات كثيرة — انتظر قليلًا ثم أعد المحاولة";
  if(status === 404) return "خدمة الدخول غير موجودة — تأكد من رفع مجلد api";
  if(status >= 500)  return "خطأ في الخادم — راجع إعدادات قاعدة البيانات";
  return "تعذّر تسجيل الدخول — تأكد من أن الاستضافة تدعم PHP";
}
function apiLogout(){
  return fetch(API + "logout.php", {method:"POST", credentials:"same-origin"})
    .then(r => r.json()).catch(()=>({ok:true}));
}

/* ---------- التصويت داخل المنصة ---------- */
function castVote(comp, cat, uid, voter){
  return fetch(API + "vote.php", {
    method:"POST", credentials:"same-origin",
    headers:{"Content-Type":"application/json"},
    body: JSON.stringify({comp, cat, uid, voter})
  })
  .then(r => r.json().catch(()=>({ok:false, error:"رد غير مفهوم من الخادم"})))
  .catch(()=>({ok:false, error:"تعذّر الاتصال بالخادم"}));
}

/* سجل تصويت المتصفح — يمنع تكرار المحاولة ويُظهر ما اختاره */
const VOTED_KEY = "exp_voted";
function myVotes(){
  try{ return JSON.parse(localStorage.getItem(VOTED_KEY) || "{}") || {}; }catch(e){ return {}; }
}
function markVoted(comp, cat, uid){
  const v = myVotes();
  v[comp + "|" + cat] = uid;
  try{ localStorage.setItem(VOTED_KEY, JSON.stringify(v)); }catch(e){}
}
function votedFor(comp, cat){ return myVotes()[comp + "|" + cat] || null; }

return {
  STORE_KEY, LEGACY_KEY, STATUS, STATUS_ORDER, API, UI_DEFAULTS,
  blankCompetition, normalizeCompetition, normalizeUI, migrate,
  voteWeight, voteVisible, voteStage, voteLive, L,
  revealTime, beforeReveal, untilReveal,
  load, loadLocal, loadAsync, fetchRemote, saveRemote, save, clearDraft,
  isOnline, lastError, session, apiLogin, apiLogout,
  castVote, myVotes, markVoted, votedFor, hashStr,
  byId, publicList, pick, ranked, rankIndex, stats, topOverall,
  score, breakdown, voteShare, catVotes, isVoteMetric, metricValue, isWeighted,
  pct, pct1, points, esc, MEDAL, PLACE, RANK_LIGHT, RANK_DARK,
  catName, initial, voteUrl, voteClosed, mountLogos,
  isImageIcon, iconHTML
};
})();
