/**
 * Data access abstraction layer.
 *
 * All Svelte components load BAS data through these functions instead of
 * calling fetch() directly. Each function merges server/bundled data with
 * any activities stored locally in IndexedDB (via localstore.ts), so the
 * app works the same whether it is connected to a cloud instance, running
 * offline, or somewhere in between.
 *
 * Design notes:
 * - Server fetch and IDB read run concurrently (Promise.allSettled).
 * - If the server is unreachable, local-only data is returned.
 * - If IDB is empty, pure server data is returned — zero overhead.
 * - Local activities override server ones with the same ID (local is authoritative
 *   for anything the user recorded or converted on this device).
 */

import type { ActivityDetail, ActivitySummary, BASIndex } from './types';
import { listLocalActivities } from './localstore';

// ── Helpers ───────────────────────────────────────────────────────────────────

async function fetchJSON<T>(url: string): Promise<T> {
  const r = await fetch(url);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json() as Promise<T>;
}

async function idbGetActivity(id: string): Promise<ActivityDetail | null> {
  // Inline IDB read — avoids importing openDB into every caller
  return new Promise(resolve => {
    try {
      const req = indexedDB.open('bincio', 1);
      req.onsuccess = e => {
        const db: IDBDatabase = (e.target as IDBOpenDBRequest).result;
        const tx = db.transaction('files', 'readonly');
        const get = tx.objectStore('files').get(`/data/activities/${id}.json`);
        get.onsuccess = ge => resolve((ge.target as IDBRequest).result?.data ?? null);
        get.onerror   = () => resolve(null);
      };
      req.onerror = () => resolve(null);
    } catch {
      resolve(null);
    }
  });
}

function emptyIndex(): BASIndex {
  return {
    bas_version: '1.0',
    owner: { handle: 'unknown', display_name: '' },
    generated_at: '',
    shards: [],
    activities: [],
  };
}

// ── Public API ────────────────────────────────────────────────────────────────

/**
 * Resolve shards from a BASIndex into a flat activity list.
 *
 * Handles two shard types transparently:
 *  - handle shards: multi-user manifest (url = "{handle}/index.json")
 *  - year shards:   per-user pagination (url = "index-2025.json")
 *
 * Shard URLs are resolved relative to the index URL that declared them.
 * All shard fetches run concurrently. Errors are silently skipped so a
 * single unavailable shard doesn't break the whole feed.
 */
async function resolveShards(
  index: BASIndex,
  indexUrl: string,
): Promise<ActivitySummary[]> {
  if (!index.shards?.length) return index.activities ?? [];

  const base = indexUrl.substring(0, indexUrl.lastIndexOf('/') + 1);

  const shardResults = await Promise.allSettled(
    index.shards.map(async shard => {
      const url = shard.url.startsWith('http') ? shard.url : `${base}${shard.url}`;
      // Base URL of this shard's directory (e.g. "http://…/data/dave/_merged/")
      const shardBase = url.substring(0, url.lastIndexOf('/') + 1);
      const sub = await fetchJSON<BASIndex>(url);
      // Recursively resolve nested shards (e.g. user shard that itself paginates)
      const activities = await resolveShards(sub, url);
      // Rewrite relative detail_url / track_url to be absolute so they can be
      // fetched correctly regardless of where the root index lives.
      return activities.map(a => ({
        ...a,
        ...(shard.handle ? { handle: shard.handle } : {}),
        detail_url: a.detail_url && !a.detail_url.startsWith('http')
          ? `${shardBase}${a.detail_url}`
          : a.detail_url,
        track_url: a.track_url && !a.track_url.startsWith('http')
          ? `${shardBase}${a.track_url}`
          : a.track_url,
      }));
    }),
  );

  const own = index.activities ?? [];
  const fromShards = shardResults.flatMap(r => r.status === 'fulfilled' ? r.value : []);
  return [...own, ...fromShards];
}

/**
 * Load the activity index, resolving any shards (multi-user or pagination),
 * then merging with locally-stored activities from IndexedDB.
 *
 * Single-user indexes with no shards work exactly as before — zero overhead.
 *
 * @param baseUrl   Site base URL (used for IDB local activities)
 * @param indexUrl  Full URL of the index to load (defaults to baseUrl + data/index.json)
 */
export async function loadIndex(baseUrl: string, indexUrl?: string): Promise<BASIndex> {
  indexUrl = indexUrl ?? `${baseUrl}data/index.json`;

  const [serverResult, localResult] = await Promise.allSettled([
    fetchJSON<BASIndex>(indexUrl),
    listLocalActivities(),
  ]);

  const server = serverResult.status === 'fulfilled' ? serverResult.value : null;
  const local  = localResult.status  === 'fulfilled' ? localResult.value  : [];

  const serverActivities = server
    ? await resolveShards(server, indexUrl)
    : [];

  if (local.length === 0 && !server) return emptyIndex();

  // Local overrides server for the same ID; new local entries are appended
  const merged = new Map<string, ActivitySummary>();
  for (const a of serverActivities)          merged.set(a.id, a);
  for (const a of local as ActivitySummary[]) merged.set(a.id, a);

  return {
    ...(server ?? emptyIndex()),
    activities: [...merged.values()].sort(
      (a, b) => (b.started_at ?? '').localeCompare(a.started_at ?? ''),
    ),
  };
}

/**
 * Load a single activity detail, checking IndexedDB first so locally-converted
 * activities are available offline.
 *
 * @param id        Activity ID (used for the IDB lookup)
 * @param detailUrl Relative path from the BAS index (e.g. "activities/id.json")
 * @param baseUrl   Site base URL
 */
export async function loadActivity(
  id:        string,
  detailUrl: string,
  baseUrl:   string,
): Promise<ActivityDetail | null> {
  // IDB first — instant and works offline
  const cached = await idbGetActivity(id);
  if (cached) return cached;

  try {
    const url = detailUrl.startsWith('http')
      ? detailUrl
      : `${baseUrl}data/${detailUrl}`;
    return await fetchJSON<ActivityDetail>(url);
  } catch {
    return null;
  }
}

/**
 * Load athlete profile. Athlete data is not stored locally yet, so this is
 * always a network fetch with a graceful null on failure.
 *
 * @param baseUrl    Site base URL (used to build the default path)
 * @param athleteUrl Explicit full URL — use for per-user pages in multi-user mode
 */
export async function loadAthlete(
  baseUrl: string,
  athleteUrl?: string,
): Promise<Record<string, unknown> | null> {
  try {
    return await fetchJSON(athleteUrl ?? `${baseUrl}data/athlete.json`);
  } catch {
    return null;
  }
}
