// Mock data + shared atoms for Zocker-Abstimmung

const ME_ID = 'me';

const FRIENDS = [
  { id: 'me', name: 'Zocker', gamertag: '', hue: 145, online: true, platform: 'steam' },
];

const FRIENDS_BY_ID = Object.fromEntries(FRIENDS.map(f => [f.id, f]));

// Groups — populated from Firestore on login
const GROUPS = [];

const GROUPS_BY_ID = Object.fromEntries(GROUPS.map(g => [g.id, g]));

// Mood tags
const MOODS = {
  chill:   { de: 'chill',   en: 'chill',   hue: 175, glyph: '◐' },
  sweaty:  { de: 'sweaty',  en: 'sweaty',  hue: 18,  glyph: '▲' },
  story:   { de: 'story',   en: 'story',   hue: 280, glyph: '✦' },
  party:   { de: 'party',   en: 'party',   hue: 330, glyph: '✺' },
};

// Games — populated from Steam import / manual add
const GAMES = [];

const GAMES_BY_ID = Object.fromEntries(GAMES.map(g => [g.id, g]));

// Game picks per friend — loaded from Firestore
const PICKS = {
  me: [],
};

// Full game library per friend (id → game objects) — loaded from Firestore.
// The local user's library lives in window.MY_LIBRARY.
const FRIEND_LIBRARY = {};

// Recurring weekly availability per friend.
// Days: 0=Mo … 6=So.  Hours stored as [from, to] in 24h decimal (>24 = past midnight).
const WEEKLY = {
  me: { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [] },
};

// Recurring scheduled sessions (raid nights, etc.) — loaded from Firestore
const RECURRING_SESSIONS = [];

// Initial game→group assignments (mutable in-app state)
const GROUP_GAMES_INITIAL = {};

const SPECIAL_FREE = {};

const DAY_LABELS_SHORT_DE = ['Mo','Di','Mi','Do','Fr','Sa','So'];
const DAY_LABELS_SHORT_EN = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
const DAY_LABELS_LONG_DE  = ['Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag','Sonntag'];
const DAY_LABELS_LONG_EN  = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];

// Today is derived from the real system clock so every calendar view always
// reflects the current day automatically (Mon=0 … Sun=6 to match the grids).
const _MONTHS_DE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const _MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const _NOW = new Date();
const TODAY_DOW = (_NOW.getDay() + 6) % 7; // Mon-first weekday index
const TODAY_DATE = _NOW.getDate();
const TODAY_MONTH_DE = _MONTHS_DE[_NOW.getMonth()];
const TODAY_MONTH_EN = _MONTHS_EN[_NOW.getMonth()];

// ── Real-calendar helpers ────────────────────────────────────────────────────
// Monday (00:00) of the week containing today.
function mondayOfCurrentWeek() {
  const n = new Date();
  n.setHours(0, 0, 0, 0);
  n.setDate(n.getDate() - ((n.getDay() + 6) % 7));
  return n;
}

// The 7 Date objects (Mon…Sun) for the week at `weekOffset` from this week.
function weekDatesFor(weekOffset = 0) {
  const mon = mondayOfCurrentWeek();
  mon.setDate(mon.getDate() + weekOffset * 7);
  return Array.from({ length: 7 }, (_, i) => {
    const d = new Date(mon);
    d.setDate(mon.getDate() + i);
    return d;
  });
}

// ISO-8601 week number for a given Date.
function isoWeek(date) {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = (d.getUTCDay() + 6) % 7;
  d.setUTCDate(d.getUTCDate() - dayNum + 3);
  const firstThursday = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
  const firstDayNum = (firstThursday.getUTCDay() + 6) % 7;
  firstThursday.setUTCDate(firstThursday.getUTCDate() - firstDayNum + 3);
  return 1 + Math.round((d - firstThursday) / (7 * 24 * 3600 * 1000));
}

// ISO week number for the week at `weekOffset` from this week.
function weekKW(weekOffset = 0) {
  return isoWeek(weekDatesFor(weekOffset)[0]);
}

// helpers
function fmtHour(h) {
  const real = ((h % 24) + 24) % 24;
  const hh = Math.floor(real);
  const mm = Math.round((real - hh) * 60);
  return `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`;
}

function rangeStr(from, to) {
  return `${fmtHour(from)} – ${fmtHour(to)}`;
}

// Date key ("YYYY-M-D", month 0-indexed) for a real Date — matches the keys the
// Termine ("freie Tage") sheet writes into window.__MY_FREE_DATES.
function freeDateKey(date) {
  return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
}

// The local user's explicit free-day override for a specific date, if any.
// Returns undefined when there's no entry (→ fall back to the weekly schedule),
// { allDay: true } for a "ganzer Tag" entry, or { from, to } for a set window.
// An explicit free-day entry overrides the weekly schedule for that date, so a
// tighter (or wider) window the user picks for one day wins over their preset.
function myFreeDateOverride(dateKey) {
  const e = (window.__MY_FREE_DATES || {})[dateKey];
  if (!e) return undefined;
  if (e.time && e.time.length === 2) return { from: e.time[0], to: e.time[1] };
  return { allDay: true }; // time === null means the whole day is free
}

// Is a member free at (dow, hour)? For the local user, an explicit free-day
// entry for `dateKey` (when given) overrides their weekly schedule for that day.
function memberFreeAt(fid, dow, hour, dateKey) {
  if (fid === 'me' && dateKey) {
    const ov = myFreeDateOverride(dateKey);
    if (ov) return ov.allDay ? true : (hour >= ov.from && hour < ov.to);
  }
  const slot = WEEKLY[fid]?.[dow];
  return !!(slot && slot.length === 2 && hour >= slot[0] && hour < slot[1]);
}

// The crews the local user belongs to.
function myGroupIds() {
  return GROUPS.filter(g => g.memberIds.includes('me')).map(g => g.id);
}

// Does the local user actually offer their availability for this day to `groupId`?
// Availability is scoped per crew: a free-day entry carries its own crew
// selection (entry.groups); otherwise the weekly per-day selection applies
// (__MY_WEEKLY_GROUPS[dow]). Both default to ALL of my crews when unset, so a
// user only drops out of a crew's sessions once they deselect it for that day.
// Returns true for the unscoped ('all') view (groupId == null).
function myAvailabilityCoversGroup(dow, dateKey, groupId) {
  if (!groupId) return true;
  const all = myGroupIds();
  const entry = dateKey ? (window.__MY_FREE_DATES || {})[dateKey] : null;
  let scope;
  if (entry) {
    scope = Array.isArray(entry.groups) ? entry.groups : all;
  } else {
    const wg = window.__MY_WEEKLY_GROUPS || {};
    const v = wg[dow] ?? wg[String(dow)];
    scope = Array.isArray(v) ? v : all;
  }
  return scope.includes(groupId);
}

// Free-friend lookup, optionally scoped to a group. Pass `dateKey` so the local
// user's per-date "freie Tage" override is applied for that calendar day.
function whoIsFree(dow, hour, groupId = null, dateKey = null) {
  const memberSet = groupId
    ? new Set(GROUPS_BY_ID[groupId].memberIds)
    : null;
  const ids = [];
  for (const f of FRIENDS) {
    if (memberSet && !memberSet.has(f.id)) continue;
    if (!memberFreeAt(f.id, dow, hour, dateKey)) continue;
    // The local user only counts for crews they offered this day to.
    if (f.id === 'me' && !myAvailabilityCoversGroup(dow, dateKey, groupId)) continue;
    ids.push(f.id);
  }
  return ids;
}

// Intersection vote
function sharedGames(friendIds) {
  if (!friendIds.length) return [];
  const counts = {};
  for (const fid of friendIds) {
    for (const gid of (PICKS[fid] || [])) {
      counts[gid] = (counts[gid] || 0) + 1;
    }
  }
  return Object.entries(counts)
    .map(([gid, n]) => ({ game: GAMES_BY_ID[gid], votes: n, total: friendIds.length }))
    .sort((a,b) => b.votes - a.votes);
}

// ── Per-session RSVP + voting ────────────────────────────────────────────────
// A member's response for a session. The local user's responses live in
// __MY_RESPONSES; every other member's responses (loaded from their profile
// doc at login) live in __SESSION_RESPONSES[key][uid], so a member's "in /
// maybe / declined" status is visible to the whole crew.
function sessionResponseFor(session, fid) {
  const key = window.sessionKey?.(session);
  if (fid === 'me') return (window.__MY_RESPONSES || {})[key] || null;
  return ((window.__SESSION_RESPONSES || {})[key] || {})[fid] || null;
}

// A member's RSVP status for a session. Default answer is "in" (matches the
// rest of the app), so members who haven't responded count as in.
function sessionMemberStatus(session, fid) {
  return sessionResponseFor(session, fid)?.rsvp || 'in';
}

// Members who declined a session (rsvp 'out'). Visible to everyone now that
// each member's response is loaded into __SESSION_RESPONSES.
function sessionDeclinedIds(session) {
  const declined = new Set();
  for (const fid of (session.friends || [])) {
    if (sessionMemberStatus(session, fid) === 'out') declined.add(fid);
  }
  return declined;
}

// Members still available for a session — everyone free minus those who declined.
function sessionAvailableIds(session) {
  const declined = sessionDeclinedIds(session);
  return (session.friends || []).filter(id => !declined.has(id));
}

// Everyone who counts toward a session's vote: members free for the slot, plus
// any crew member who opted in (rsvp in/maybe) or cast a vote even if they
// weren't in the free-overlap window — casting a vote opts you in. Declined
// members are removed. This is why your own vote starts counting the moment you
// tap a game, even if you hadn't marked yourself free.
function sessionVoterIds(session) {
  const ids = new Set(session.friends || []);
  const groupIds = session.groupId ? (GROUPS_BY_ID[session.groupId]?.memberIds || []) : [];
  for (const fid of new Set([...(session.friends || []), ...groupIds, 'me'])) {
    const r = sessionResponseFor(session, fid);
    if (r && (r.rsvp === 'in' || r.rsvp === 'maybe' || r.voteGameId)) ids.add(fid);
  }
  for (const fid of sessionDeclinedIds(session)) ids.delete(fid);
  return [...ids];
}

// Per-session game vote tally. Two kinds of signal count, and they stack:
//   • a member's shortlisted picks each count as a vote, and
//   • a member's manual session vote adds ONE more vote for that one game.
// So voting for a game you already picked bumps it again (your name then shows
// twice in the "picked by" list). The denominator is the number of members who
// have participated at all (any pick or a vote) — it grows by one the moment
// you cast your first vote, which is why others shift from 2/2 to 2/3 while the
// game you voted for goes to 3/3. Declined members are excluded entirely.
function sessionGameVotes(session) {
  const voters = sessionVoterIds(session);
  const counts = {};
  const participants = new Set();
  for (const fid of voters) {
    const picks = PICKS[fid] || [];
    const vote = sessionResponseFor(session, fid)?.voteGameId;
    if (picks.length || vote) participants.add(fid);
    for (const gid of picks) counts[gid] = (counts[gid] || 0) + 1;
    if (vote) counts[vote] = (counts[vote] || 0) + 1;
  }
  const total = participants.size;
  return Object.entries(counts)
    .map(([gid, n]) => ({ game: GAMES_BY_ID[gid], votes: n, total }))
    .filter(x => x.game)
    .sort((a, b) => b.votes - a.votes);
}

// Who is backing the given game in this session, as a flat list of contributions
// (so a member who both picked AND voted for it appears twice). kind is 'pick'
// or 'vote'. Declined members are excluded.
function sessionGameBackers(session, gameId) {
  if (!gameId) return [];
  const out = [];
  for (const fid of sessionVoterIds(session)) {
    if ((PICKS[fid] || []).includes(gameId)) out.push({ fid, kind: 'pick' });
    if (sessionResponseFor(session, fid)?.voteGameId === gameId) out.push({ fid, kind: 'vote' });
  }
  return out;
}

// ── Readiness check ("Seid ihr bereit?") ─────────────────────────────────────
// Any participant can fire a ready check for a session. It's stored on the
// requester's per-session response (responses[key].readyCheck = { at, by }) and
// every accepted member answers with responses[key].ready = { status, at }.
// Answers only count for the *current* check (ready.at >= check.at), so a fresh
// check resets everyone to "no reply" until they respond again.
const READY_CHECK_TTL_MS = 4 * 3600 * 1000; // a check stays live for 4h

// The latest ready check across everyone's responses for this session, or null.
function sessionReadyCheck(session) {
  const key = window.sessionKey?.(session);
  if (!key) return null;
  let latest = null;
  const consider = (rc) => { if (rc && rc.at && (!latest || rc.at > latest.at)) latest = rc; };
  consider(((window.__MY_RESPONSES || {})[key] || {}).readyCheck);
  const others = (window.__SESSION_RESPONSES || {})[key] || {};
  for (const uid in others) consider(others[uid]?.readyCheck);
  return latest;
}

// The active (not-yet-expired) ready check for a session, or null.
function sessionReadyCheckActive(session) {
  const rc = sessionReadyCheck(session);
  return rc && (Date.now() - rc.at) < READY_CHECK_TTL_MS ? rc : null;
}

// A member's readiness answer for the *active* check: 'yes' | 'coming' | null.
function sessionReadyStatus(session, fid) {
  const rc = sessionReadyCheckActive(session);
  if (!rc) return null;
  const r = sessionResponseFor(session, fid);
  const ans = r && r.ready;
  if (ans && ans.status && (ans.at || 0) >= rc.at) return ans.status;
  return null;
}

// Members an active check applies to: everyone who accepted (in/maybe), i.e.
// not declined. These are the avatars whose readiness we display.
function sessionReadyTargets(session) {
  return (session.friends || []).filter(fid => window.sessionMemberStatus(session, fid) !== 'out');
}

// How many of the accepted members have answered (yes or coming).
function sessionReadyCount(session) {
  const targets = sessionReadyTargets(session);
  const ready = targets.filter(fid => sessionReadyStatus(session, fid)).length;
  return { ready, total: targets.length };
}

// Which groups does a free-friend set belong to?
// Returns groups where at least 2 members are in the set.
function groupsForSession(friendIds) {
  const set = new Set(friendIds);
  return GROUPS.filter(g => g.memberIds.filter(id => set.has(id)).length >= 2);
}

// Sessions for next 7 days; restricted to a group if groupId given.
// Threshold is "more than one person" (>1) per the home-screen spec.
function buildSessions(groupId = null, minOverlap = 2) {
  const sessions = [];
  const base = new Date(); base.setHours(0, 0, 0, 0);
  for (let off = 0; off < 7; off++) {
    const dow = (TODAY_DOW + off) % 7;
    const date = new Date(base); date.setDate(base.getDate() + off);
    const dateKey = freeDateKey(date);
    let cur = null;
    let curSet = '';
    for (let h = 14; h <= 27; h++) {
      const free = whoIsFree(dow, h, groupId, dateKey);
      const key = free.slice().sort().join(',');
      if (free.length >= minOverlap && key === curSet) {
        cur.to = h + 1;
      } else {
        if (cur && cur.friends.length >= minOverlap) sessions.push(cur);
        if (free.length >= minOverlap) {
          cur = { dayOffset: off, dow, from: h, to: h + 1, friends: free, groupId };
          curSet = key;
        } else {
          cur = null; curSet = '';
        }
      }
    }
    if (cur && cur.friends.length >= minOverlap) sessions.push(cur);
  }
  for (const s of sessions) {
    const sg = sharedGames(s.friends);
    s.topGame = sg[0]?.game || null;
    s.topVotes = sg[0]?.votes || 0;
    s.groups = groupsForSession(s.friends);
  }
  return sessions;
}

// Cache by groupId so the data is stable across renders
const SESSIONS_CACHE = {};
function getSessions(groupId = 'all') {
  if (!SESSIONS_CACHE[groupId]) {
    SESSIONS_CACHE[groupId] = buildSessions(groupId === 'all' ? null : groupId);
  }
  return SESSIONS_CACHE[groupId];
}

// ── Crew priority + scheduling-clash resolution ──────────────────────────────
// The user ranks their crews (window.__MY_CREW_PRIORITY = [groupId, …], highest
// first). When two crews could both play in overlapping time windows on the same
// day, the higher-ranked crew wins and the others are auto-declined (reversible).

// Full priority order: saved ranking first, then any crews not yet ranked.
function getCrewPriority() {
  const ids = GROUPS.map(g => g.id);
  const saved = (window.__MY_CREW_PRIORITY || []).filter(id => ids.includes(id));
  for (const id of ids) if (!saved.includes(id)) saved.push(id);
  return saved;
}

// Lower rank value = higher priority. Unknown crews sort last.
function crewRank(groupId) {
  const i = getCrewPriority().indexOf(groupId);
  return i === -1 ? 9999 : i;
}

// Groups sorted by the user's saved crew priority (highest first). Used so the
// most important crew shows on top everywhere it's listed, not just while
// editing. Pass a subset (e.g. the user's own crews) or omit for all GROUPS.
function groupsByPriority(groups = GROUPS) {
  return [...groups].sort((a, b) => crewRank(a.id) - crewRank(b.id));
}

// Do two sessions occupy overlapping time on the same day?
function sessionsOverlap(a, b) {
  return a.dayOffset === b.dayOffset && a.from < b.to && b.from < a.to;
}

// Build one session list *per crew* (each session owned by a single group),
// so overlapping crews surface as distinct events that can clash.
function buildGroupSessions(filterGroupId = null, minOverlap = 2) {
  const groups = filterGroupId
    ? (GROUPS_BY_ID[filterGroupId] ? [GROUPS_BY_ID[filterGroupId]] : [])
    : GROUPS;
  const out = [];
  for (const g of groups) {
    for (const s of buildSessions(g.id, minOverlap)) {
      s.group = g;
      s.groups = [g];
      s.groupId = g.id;
      out.push(s);
    }
  }
  return out;
}

// Mark lower-priority sessions that overlap a higher-priority one as
// auto-declined. An explicit "in"/"maybe" RSVP from the user overrides the
// auto-decline (that's the one-tap "reactivate" path).
function resolveClashes(sessions) {
  const responses = window.__MY_RESPONSES || {};
  const userWantsIn = (s) => {
    const r = responses[window.sessionKey?.(s)];
    return r && (r.rsvp === 'in' || r.rsvp === 'maybe');
  };
  // Process highest priority (and earliest) first so winners are claimed first.
  const byPriority = [...sessions].sort((a, b) =>
    a.dayOffset - b.dayOffset ||
    crewRank(a.groupId) - crewRank(b.groupId) ||
    a.from - b.from
  );
  const claimed = [];
  for (const s of byPriority) {
    s.autoDeclined = false;
    s.declinedBy = null;
    const clash = claimed.find(c => c.groupId !== s.groupId && sessionsOverlap(c, s));
    if (clash && !userWantsIn(s)) {
      s.autoDeclined = true;
      s.declinedBy = clash.group;
    } else {
      claimed.push(s);
    }
  }
  // Return in natural display order (soonest first).
  return byPriority.sort((a, b) => a.dayOffset - b.dayOffset || a.from - b.from);
}

// Home-screen session feed: per-crew events the user is actually part of, with
// clashes resolved. We keep only sessions where the user is free for THAT crew
// (whoIsFree already scopes 'me' to the crews they offered each day to), so a
// crew the user didn't make themselves available for never shows up as theirs —
// and clash resolution only weighs the user's own overlapping sessions.
function getHomeSessions(groupId = 'all') {
  const mine = buildGroupSessions(null).filter(s => (s.friends || []).includes('me'));
  const resolved = resolveClashes(mine);
  if (groupId === 'all') return resolved;
  return resolved.filter(s => s.groupId === groupId);
}

Object.assign(window, {
  ME_ID, FRIENDS, FRIENDS_BY_ID, GROUPS, GROUPS_BY_ID,
  MOODS, GAMES, GAMES_BY_ID, PICKS, FRIEND_LIBRARY, WEEKLY, RECURRING_SESSIONS, SPECIAL_FREE,
  GROUP_GAMES_INITIAL,
  DAY_LABELS_SHORT_DE, DAY_LABELS_SHORT_EN,
  DAY_LABELS_LONG_DE,  DAY_LABELS_LONG_EN,
  TODAY_DOW, TODAY_DATE, TODAY_MONTH_DE, TODAY_MONTH_EN,
  mondayOfCurrentWeek, weekDatesFor, isoWeek, weekKW,
  fmtHour, rangeStr, whoIsFree, memberFreeAt, freeDateKey, myFreeDateOverride,
  myGroupIds, myAvailabilityCoversGroup, sharedGames, groupsForSession, getSessions,
  sessionResponseFor, sessionMemberStatus, sessionDeclinedIds, sessionAvailableIds, sessionVoterIds, sessionGameVotes, sessionGameBackers,
  sessionReadyCheck, sessionReadyCheckActive, sessionReadyStatus, sessionReadyTargets, sessionReadyCount,
  getCrewPriority, crewRank, groupsByPriority, sessionsOverlap, buildGroupSessions, resolveClashes, getHomeSessions,
});
