// JumpyBoard API client.
// Public dashboard reads need no auth. Staff operations use the shared
// Check-in PIN session and keep only its opaque token in sessionStorage.

const JUMPYBOARD_API_DEFAULT_ENDPOINT = "https://5k3din78rg.execute-api.eu-north-1.amazonaws.com";
const JUMPYBOARD_API_REGION = "eu-north-1";
const JUMPYBOARD_SAFETY_SYNC_MS = 60000;
const JUMPYBOARD_VISIBLE_JUMP_STEP_MS = 75;
const JUMPYBOARD_VISIBLE_JUMP_STEP_LIMIT = 12;
const JUMPYBOARD_STAFF_SESSION_KEY = "jumpyboard.staff.session.v1";
const JUMPYBOARD_API_BASE_KEY = "jumpyboard.apiBaseUrl";

const JUMPYBOARD_PALETTE = [
  "#E63329",
  "#FFD23F",
  "#7B61FF",
  "#00C2A8",
  "#FF6B9D",
  "#3DDC97",
  "#FF9F1C",
  "#4DA6FF",
  "#A364FF",
  "#111111",
];

function stripTrailingSlash(value) {
  return String(value || "").trim().replace(/\/+$/, "");
}

function normalizeJumpCount(value) {
  const numeric = Number(value);
  return Number.isFinite(numeric) ? Math.max(0, Math.round(numeric)) : 0;
}

function nextVisibleJumpCount(currentValue, targetValue, stepLimit = JUMPYBOARD_VISIBLE_JUMP_STEP_LIMIT) {
  const current = normalizeJumpCount(currentValue);
  const target = normalizeJumpCount(targetValue);
  if (target <= current) return target;

  const limit = Math.max(1, normalizeJumpCount(stepLimit));
  const remaining = target - current;
  return remaining > limit ? target - limit + 1 : current + 1;
}

function safeStorage(storage) {
  try {
    const testKey = "__jumpyboard_storage_test__";
    storage.setItem(testKey, "1");
    storage.removeItem(testKey);
    return storage;
  } catch {
    return null;
  }
}

function readJumpyBoardConfig() {
  const params = new URLSearchParams(window.location.search);
  const queryApi = params.get("api");
  const local = safeStorage(window.localStorage);
  const storedApi = local?.getItem(JUMPYBOARD_API_BASE_KEY);
  const apiBaseUrl = stripTrailingSlash(queryApi || storedApi || JUMPYBOARD_API_DEFAULT_ENDPOINT);
  return {
    apiBaseUrl,
    region: JUMPYBOARD_API_REGION,
    syncMs: JUMPYBOARD_SAFETY_SYNC_MS,
  };
}

function saveApiBaseUrl(value) {
  const apiBaseUrl = stripTrailingSlash(value || JUMPYBOARD_API_DEFAULT_ENDPOINT);
  const local = safeStorage(window.localStorage);
  local?.setItem(JUMPYBOARD_API_BASE_KEY, apiBaseUrl);
  return apiBaseUrl;
}

function initialsFor(name) {
  const parts = String(name || "?")
    .trim()
    .split(/\s+/)
    .filter(Boolean);
  if (parts.length === 0) return "?";
  return parts.slice(0, 2).map(part => part[0]).join("").toUpperCase();
}

function slugify(value) {
  return String(value || "")
    .toLowerCase()
    .replace(/\.png$/, "")
    .replace(/-mascot$/, "")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

function mascotChoices() {
  return (window.MASCOT_ASSETS || []).map((file, index) => ({
    id: file,
    file,
    src: `public/mascots/${file}`,
    label: file.replace(/-mascot\.png$/, "").replace(/-/g, " "),
    index,
  }));
}

function normalizeMascotId(value) {
  const assets = window.MASCOT_ASSETS || [];
  if (!value) return assets[0] || "";
  const raw = String(value).trim();
  const filename = raw.split(/[\\/]/).pop();
  if (assets.includes(raw)) return raw;
  if (assets.includes(filename)) return filename;

  const wanted = slugify(filename || raw);
  const match = assets.find((asset) => {
    const candidate = slugify(asset);
    return candidate === wanted || candidate.includes(wanted) || wanted.includes(candidate);
  });
  return match || filename || raw;
}

function mascotPath(value) {
  const id = normalizeMascotId(value);
  if (!id) return "";
  if (id.startsWith("public/")) return id;
  return `public/mascots/${id}`;
}

function hashString(value) {
  let hash = 0;
  const input = String(value || "");
  for (let i = 0; i < input.length; i += 1) {
    hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0;
  }
  return Math.abs(hash);
}

function toNumber(value, fallback = 0) {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
}

function formatRelativeTime(value) {
  const date = new Date(value || Date.now());
  const ms = Date.now() - date.getTime();
  if (!Number.isFinite(ms) || ms < 5000) return "just nu";
  const seconds = Math.floor(ms / 1000);
  if (seconds < 60) return `${seconds}s sedan`;
  const minutes = Math.floor(seconds / 60);
  if (minutes < 60) return `${minutes} min sedan`;
  return date.toLocaleTimeString("sv", { hour: "2-digit", minute: "2-digit" });
}

function sessionToDisplayEntry(session, index = 0) {
  const name = String(session?.display_name || `Hoppare ${index + 1}`).trim();
  const mascotId = normalizeMascotId(session?.mascot_id);
  const jumps = toNumber(session?.jump_count);
  const totalMeters = toNumber(session?.cumulative_height_mm) / 1000;
  const best = Math.round(toNumber(session?.latest_height_mm) / 10);
  const sessionId = String(session?.session_id || name || index);
  return {
    rank: index + 1,
    sessionId,
    deviceLabel: session?.assigned_device_label || session?.device_label || "",
    name,
    displayName: name,
    avatar: initialsFor(name),
    mascotId,
    mascot: mascotPath(mascotId),
    color: JUMPYBOARD_PALETTE[hashString(sessionId || name) % JUMPYBOARD_PALETTE.length],
    best,
    jumps,
    totalMeters,
    level: 0,
    xp: 0,
    trend: "same",
    state: session?.state || "active",
    latestJumpAt: session?.latest_jump_at,
    latestAirtimeMs: toNumber(session?.latest_airtime_ms),
    createdAt: session?.created_at,
    updatedAt: session?.updated_at,
  };
}

function eventToDisplayEntry(event, sessionsById, index = 0) {
  const session = sessionsById.get(event?.session_id) || {};
  const entry = sessionToDisplayEntry({ ...session, ...event }, index);
  const height = Math.round(toNumber(event?.height_mm) / 10);
  return {
    ...entry,
    height,
    jumps: toNumber(session?.jump_count, entry.jumps || 1),
    totalMeters: toNumber(session?.cumulative_height_mm, entry.totalMeters * 1000) / 1000,
    ts: formatRelativeTime(event?.created_at || event?.received_at),
    isPR: height > 0 && height >= toNumber(session?.latest_height_mm) / 10,
    pad: event?.device_label || session?.assigned_device_label || "",
  };
}

function formatDuration(ms) {
  const seconds = Math.max(0, Math.round(toNumber(ms) / 1000));
  if (seconds < 60) return `${seconds}s`;
  const minutes = Math.floor(seconds / 60);
  const rest = seconds % 60;
  return `${minutes}m ${rest}s`;
}

function buildYardStats(activeSessions, recentEvents) {
  const totalJumps = activeSessions.reduce((sum, session) => sum + toNumber(session.jump_count), 0);
  const assigned = activeSessions.filter(session => session.assigned_device_label).length;
  const highestFromSessions = activeSessions.map(session => toNumber(session.latest_height_mm));
  const highestFromEvents = recentEvents.map(event => toNumber(event.height_mm));
  const highestToday = Math.round(Math.max(0, ...highestFromSessions, ...highestFromEvents) / 10);
  const latestAirtime = activeSessions.reduce((sum, session) => sum + toNumber(session.latest_airtime_ms), 0);
  return {
    totalJumps,
    jumpersToday: activeSessions.length,
    jumpersNow: assigned,
    highestToday,
    avgHeight: 0,
    totalCalories: 0,
    totalAirtime: latestAirtime > 0 ? formatDuration(latestAirtime) : "0s",
  };
}

function estimateJumpsPerMinute(recentEvents) {
  const oneMinuteAgo = Date.now() - 60000;
  const recentCount = recentEvents.filter((event) => {
    const stamp = new Date(event?.created_at || event?.received_at || 0).getTime();
    return Number.isFinite(stamp) && stamp >= oneMinuteAgo;
  }).length;
  return recentCount;
}

function buildMountain(yardStats, recentEvents) {
  const base = window.MOUNTAIN || {};
  const currentJumps = yardStats.totalJumps;
  const goalJumps = Math.max(20000, Math.ceil((currentJumps + 1) / 5000) * 5000);
  return {
    ...base,
    goalName: base.goalName || "Mount Everest",
    goalJumps,
    currentJumps,
    contributorsToday: yardStats.jumpersToday,
    jumpsPerMinute: estimateJumpsPerMinute(recentEvents),
    milestones: base.milestones || [],
    reward: base.reward || "Hela ganget far gratis glass i cafet",
  };
}

function buildGroups(leaderboard) {
  return leaderboard.slice(0, 5).map((entry, index) => ({
    id: entry.deviceLabel || entry.sessionId || `group-${index}`,
    name: entry.deviceLabel ? `Band ${entry.deviceLabel}` : entry.name,
    color: entry.color,
    mascot: entry.mascot,
    members: 1,
    bestBumper: entry.name,
    bestHeight: entry.best,
    totalMeters: entry.totalMeters,
    totalJumps: entry.jumps,
    leader: index === 0,
  }));
}

function emptyLiveState(status = "loading", error = "") {
  const yardStats = buildYardStats([], []);
  return {
    leaderboard: [],
    activeSessions: [],
    recent: [],
    spotlight: null,
    groups: [],
    yardStats,
    mountain: buildMountain(yardStats, []),
    dataStatus: {
      mode: "live",
      status,
      error,
      lastUpdated: null,
    },
  };
}

function dashboardPayloadToState(payload) {
  const activeSessions = Array.isArray(payload?.active_sessions) ? payload.active_sessions : [];
  const leaderboardSource = Array.isArray(payload?.leaderboard) && payload.leaderboard.length > 0
    ? payload.leaderboard
    : activeSessions;
  const leaderboard = leaderboardSource
    .map(sessionToDisplayEntry)
    .sort((left, right) => {
      if (right.jumps !== left.jumps) return right.jumps - left.jumps;
      return right.totalMeters - left.totalMeters;
    })
    .map((entry, index) => ({ ...entry, rank: index + 1 }));

  const sessionsById = new Map(activeSessions.map(session => [session.session_id, session]));
  const recentEvents = Array.isArray(payload?.recent_events) ? payload.recent_events : [];
  const recent = recentEvents.map((event, index) => eventToDisplayEntry(event, sessionsById, index));
  const yardStats = buildYardStats(activeSessions, recentEvents);
  const spotlight = leaderboard[0] ? {
    ...leaderboard[0],
    previousBest: Math.max(0, (leaderboard[0].best || 0) - 12),
    badge: "LIVE",
    xpGained: 0,
  } : null;

  return {
    leaderboard,
    activeSessions: activeSessions.map(sessionToDisplayEntry),
    recent,
    spotlight,
    groups: buildGroups(leaderboard),
    yardStats,
    mountain: buildMountain(yardStats, recentEvents),
    dataStatus: {
      mode: "live",
      status: "ready",
      error: "",
      generatedAt: payload?.generated_at || null,
      lastUpdated: new Date().toISOString(),
    },
  };
}

function realtimeConfigFromPayload(payload) {
  const realtime = payload?.realtime;
  if (toNumber(realtime?.protocol_version) !== 1) return null;
  const websocketUrl = String(realtime?.websocket_url || "").trim();
  try {
    const parsed = new URL(websocketUrl);
    if (parsed.protocol !== "wss:" && parsed.protocol !== "ws:") return null;
    return { websocketUrl: parsed.toString(), protocolVersion: 1 };
  } catch {
    return null;
  }
}

function applyDashboardRealtimeEvent(payload, message) {
  if (message?.type !== "jump.recorded" || toNumber(message?.version) !== 1) {
    return { applied: false, reason: "unsupported" };
  }

  const event = message?.event;
  const session = message?.session;
  const eventId = String(event?.event_id || "").trim();
  const sessionId = String(event?.session_id || "").trim();
  if (!eventId || !sessionId || String(session?.session_id || "").trim() !== sessionId) {
    return { applied: false, reason: "malformed" };
  }

  const recentEvents = Array.isArray(payload?.recent_events) ? payload.recent_events : [];
  if (recentEvents.some(item => item?.event_id === eventId)) {
    return { applied: false, reason: "duplicate", eventId };
  }

  const activeSessions = Array.isArray(payload?.active_sessions) ? payload.active_sessions : [];
  const existingSession = activeSessions.find(item => item?.session_id === sessionId);
  const preserveExistingAggregate = session?.state === "active"
    && existingSession
    && toNumber(session?.jump_count, -1) <= toNumber(existingSession?.jump_count, -1);
  const nextSessions = preserveExistingAggregate
    ? activeSessions
    : session?.state === "active"
      ? [...activeSessions.filter(item => item?.session_id !== sessionId), session]
      : activeSessions.filter(item => item?.session_id !== sessionId);
  const nextRecentEvents = preserveExistingAggregate
    ? [...recentEvents, event].slice(0, 25)
    : [event, ...recentEvents].slice(0, 25);

  return {
    applied: true,
    eventId,
    aggregateApplied: !preserveExistingAggregate,
    payload: {
      ...payload,
      generated_at: message?.emitted_at || new Date().toISOString(),
      active_sessions: nextSessions,
      leaderboard: nextSessions,
      recent_events: nextRecentEvents,
    },
  };
}

async function parseJsonResponse(response) {
  const text = await response.text();
  let data = {};
  if (text) {
    try {
      data = JSON.parse(text);
    } catch {
      data = { message: text };
    }
  }
  if (!response.ok) {
    const message = data?.message || `HTTP ${response.status}`;
    const error = new Error(message);
    error.status = response.status;
    error.body = data;
    throw error;
  }
  return data;
}

async function fetchDashboardLive(signal) {
  const { apiBaseUrl } = readJumpyBoardConfig();
  const response = await fetch(`${apiBaseUrl}/dashboard/live`, {
    method: "GET",
    headers: { accept: "application/json" },
    cache: "no-store",
    signal,
  });
  return parseJsonResponse(response);
}

function loadStaffSession() {
  const session = safeStorage(window.sessionStorage);
  if (!session) return null;
  try {
    const parsed = JSON.parse(session.getItem(JUMPYBOARD_STAFF_SESSION_KEY) || "null");
    if (!isStaffSession(parsed) || isStaffSessionExpired(parsed)) {
      session.removeItem(JUMPYBOARD_STAFF_SESSION_KEY);
      return null;
    }
    return parsed;
  } catch {
    session.removeItem(JUMPYBOARD_STAFF_SESSION_KEY);
    return null;
  }
}

function saveStaffSession(staffSession) {
  if (!isStaffSession(staffSession)) throw new Error("Personalsessionen kunde inte sparas.");
  const session = safeStorage(window.sessionStorage);
  session?.setItem(JUMPYBOARD_STAFF_SESSION_KEY, JSON.stringify(staffSession));
  return staffSession;
}

function clearStaffSession() {
  safeStorage(window.sessionStorage)?.removeItem(JUMPYBOARD_STAFF_SESSION_KEY);
}

function isStaffSession(value) {
  return Boolean(
    value &&
    typeof value.auth?.token === "string" && value.auth.token &&
    typeof value.auth?.expiresAt === "string" &&
    typeof value.session?.sessionId === "string" && value.session.sessionId &&
    typeof value.staff?.displayName === "string" && value.staff.displayName,
  );
}

function isStaffSessionExpired(value, now = Date.now()) {
  const expiries = [value?.auth?.expiresAt, value?.session?.idleExpiresAt, value?.session?.absoluteExpiresAt]
    .map(item => Date.parse(item || ""))
    .filter(Number.isFinite);
  return expiries.length === 0 || Math.min(...expiries) <= now;
}

async function staffRequest(path, options = {}) {
  const config = readJumpyBoardConfig();
  const staffSession = options.staffSession || loadStaffSession();
  const token = staffSession?.auth?.token;
  if (!token) throw staffSessionError("Logga in med din personal-PIN.", 401);

  const response = await fetch(`${config.apiBaseUrl}${path}`, {
    method: options.method || "GET",
    headers: {
      accept: "application/json",
      authorization: `Bearer ${token}`,
      ...(options.body === undefined ? {} : { "content-type": "application/json" }),
    },
    body: options.body === undefined ? undefined : JSON.stringify(options.body),
    cache: "no-store",
  });
  if (response.status === 401) clearStaffSession();
  return parseJsonResponse(response);
}

function staffSessionError(message, status) {
  const error = new Error(message);
  error.status = status;
  return error;
}

async function loginStaff(pin) {
  const normalizedPin = String(pin || "").trim();
  if (!/^\d{6}$/.test(normalizedPin)) throw staffSessionError("Ange din sexsiffriga PIN-kod.", 400);
  const { apiBaseUrl } = readJumpyBoardConfig();
  const response = await fetch(`${apiBaseUrl}/staff/auth/login`, {
    method: "POST",
    headers: { accept: "application/json", "content-type": "application/json" },
    body: JSON.stringify({ pin: normalizedPin }),
    cache: "no-store",
  });
  const result = await parseJsonResponse(response);
  return saveStaffSession(result);
}

async function manageStaffSession(action, staffSession = loadStaffSession()) {
  const result = await staffRequest("/staff/auth/session", {
    method: "POST",
    body: { action },
    staffSession,
  });
  if (action === "logout") {
    clearStaffSession();
    return null;
  }
  const updated = {
    ...staffSession,
    session: result.session,
    staff: result.principal || staffSession.staff,
  };
  return saveStaffSession(updated);
}

function listDevices(staffSession) {
  return staffRequest("/staff/devices", { method: "GET", staffSession });
}

function upsertDevice(staffSession, body) {
  return staffRequest("/staff/devices", { method: "POST", body, staffSession });
}

function listSessions(staffSession) {
  return staffRequest("/staff/sessions", { method: "GET", staffSession });
}

function createSession(staffSession, body) {
  return staffRequest("/staff/sessions", { method: "POST", body, staffSession });
}

function assignDevice(staffSession, sessionId, body) {
  return staffRequest(`/staff/sessions/${encodeURIComponent(sessionId)}/assign`, { method: "POST", body, staffSession });
}

function unassignDevice(staffSession, sessionId) {
  return staffRequest(`/staff/sessions/${encodeURIComponent(sessionId)}/unassign`, { method: "POST", body: {}, staffSession });
}

function endSession(staffSession, sessionId) {
  return staffRequest(`/staff/sessions/${encodeURIComponent(sessionId)}/end`, { method: "POST", body: {}, staffSession });
}

function createStaffRequestId() {
  const uuid = window.crypto?.randomUUID?.();
  if (uuid) return uuid;
  return `request_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
}

function simulateJump(staffSession, sessionId, requestId = createStaffRequestId()) {
  return staffRequest(`/staff/sessions/${encodeURIComponent(sessionId)}/simulate-jump`, {
    method: "POST",
    body: { request_id: requestId },
    staffSession,
  });
}

Object.assign(window, {
  JUMPYBOARD_API_DEFAULT_ENDPOINT,
  JUMPYBOARD_API_REGION,
  JUMPYBOARD_SAFETY_SYNC_MS,
  JUMPYBOARD_VISIBLE_JUMP_STEP_MS,
  JUMPYBOARD_VISIBLE_JUMP_STEP_LIMIT,
  JumpyBoardApi: {
    readJumpyBoardConfig,
    saveApiBaseUrl,
    fetchDashboardLive,
    dashboardPayloadToState,
    realtimeConfigFromPayload,
    applyDashboardRealtimeEvent,
    nextVisibleJumpCount,
    emptyLiveState,
    mascotChoices,
    normalizeMascotId,
    mascotPath,
    initialsFor,
    sessionToDisplayEntry,
    loadStaffSession,
    saveStaffSession,
    clearStaffSession,
    loginStaff,
    manageStaffSession,
    listDevices,
    upsertDevice,
    listSessions,
    createSession,
    assignDevice,
    unassignDevice,
    endSession,
    createStaffRequestId,
    simulateJump,
  },
});
