import { pb } from './pb';
import { storage } from './storage';

// Upsert helper: update existing record if found, otherwise create.
async function pbUpsert(collection, filter, updateData, createData) {
  try {
    const r = await pb.collection(collection).getFirstListItem(filter);
    return pb.collection(collection).update(r.id, updateData);
  } catch {
    return pb.collection(collection).create(createData);
  }
}

// ── Topics ──────────────────────────────────────────────────────────────────

export async function getTopics() {
  return pb.collection('topics').getFullList();
}

export async function saveTopics(topics) {
  const existing = await pb.collection('topics').getFullList({ fields: 'id' });
  for (const r of existing) {
    await pb.collection('topics').delete(r.id, { requestKey: null });
  }
  for (const t of topics) {
    await pb.collection('topics').create({
      id: t.id,
      label: t.label,
      type: t.type,
      description: t.description,
      learning_relevance: t.learning_relevance || 'standard',
      relevance_locked: t.relevance_locked === true,
      theme: t.theme || '',
      complexity_weight: t.complexity_weight || 3,
      difficulty: t.difficulty || 'intermediate',
    }, { requestKey: null });
  }
}

export async function upsertTopic(topic) {
  try {
    await pb.collection('topics').getOne(topic.id);
    return await pb.collection('topics').update(topic.id, topic);
  } catch {
    return await pb.collection('topics').create({
      id: topic.id,
      learning_relevance: 'standard',
      ...topic
    });
  }
}

// ── Relations ────────────────────────────────────────────────────────────────

export async function getRelations() {
  const records = await pb.collection('relations').getFullList();
  // Always return plain string IDs for source/target.
  // PocketBase can return populated objects when relation fields are expanded;
  // normalising here means callers never need the `r.source?.id || r.source` dance.
  return records.map(r => ({
    ...r,
    source: typeof r.source === 'object' && r.source !== null ? r.source.id : r.source,
    target: typeof r.target === 'object' && r.target !== null ? r.target.id : r.target,
  }));
}

export async function saveRelations(relations) {
  const existing = await pb.collection('relations').getFullList({ fields: 'id' });
  for (const r of existing) {
    await pb.collection('relations').delete(r.id, { requestKey: null });
  }
  for (const r of relations) {
    await pb.collection('relations').create(r, { requestKey: null });
  }
}

export async function addRelation(relation) {
  return pb.collection('relations').create(relation);
}

export async function removeRelation(source, target, type) {
  const records = await pb.collection('relations').getFullList({
    filter: `source="${source}" && target="${target}" && type="${type}"`,
  });
  return Promise.all(records.map(r => pb.collection('relations').delete(r.id, { requestKey: null })));
}

// ── Content ──────────────────────────────────────────────────────────────────

export async function getContent(topicId) {
  try {
    const r = await pb.collection('content').getFirstListItem(`topic_id="${topicId}"`);
    return r.data;
  } catch { return null; }
}

export function setContent(topicId, data) {
  return pbUpsert('content', `topic_id="${topicId}"`, { data }, { topic_id: topicId, data });
}

export async function deleteContent(topicId) {
  try {
    const r = await pb.collection('content').getFirstListItem(`topic_id="${topicId}"`);
    return pb.collection('content').delete(r.id);
  } catch { /* no record, nothing to do */ }
}

// ── Question Bank (shared, admin-curated, used by on-demand tests) ──────────
// One PB record per topic, with `questions: json[]`. The legacy aliases
// (getQuizBank etc.) are kept so existing admin UI code keeps working.

export async function getQuestionBank(topicId) {
  try {
    const r = await pb.collection('question_bank').getFirstListItem(`topic_id="${topicId}"`);
    return Array.isArray(r.questions) ? r.questions : [];
  } catch { return []; }
}

export async function setQuestionBank(topicId, questions) {
  return pbUpsert(
    'question_bank',
    `topic_id="${topicId}"`,
    { questions },
    { topic_id: topicId, questions },
  );
}

export async function deleteQuestionFromBank(topicId, questionId) {
  try {
    const r = await pb.collection('question_bank').getFirstListItem(`topic_id="${topicId}"`);
    const next = (r.questions || []).filter(q => q.id !== questionId);
    return pb.collection('question_bank').update(r.id, { questions: next });
  } catch { return null; }
}

// Legacy aliases — the admin TestManager UI and forceGenerateTopicQuestions
// call these. Routing them at the new collection un-breaks the panel.
export const getQuizBank = getQuestionBank;
export const setQuizBank = setQuestionBank;

// ── On-demand attempts ──────────────────────────────────────────────────────

function isoWeekKey(date = new Date()) {
  // Returns "YYYY-Www" for the ISO week containing `date`. Used for the
  // per-(user, topic) weekly point cap aggregation.
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const day = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - day);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
  return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
}

export { isoWeekKey };

export async function saveOnDemandAttempt(attempt) {
  return pb.collection('on_demand_attempts').create({
    user_id:        attempt.userId,
    topic_id:       attempt.topicId,
    topic_label:    attempt.topicLabel || '',
    question_ids:   attempt.questionIds || [],
    score:          attempt.score,
    total:          attempt.total,
    percentage:     attempt.percentage,
    time_used:      attempt.timeUsed,
    unseen_correct: attempt.unseenCorrect,
    points_earned:  attempt.pointsEarned,
    capped:         !!attempt.capped,
    iso_week:       attempt.isoWeek,
    completed_at:   attempt.completedAt,
    breakdown:      attempt.breakdown || [],
  });
}

export async function getOnDemandPointsThisWeek(userId, topicId) {
  try {
    const week = isoWeekKey();
    const rows = await pb.collection('on_demand_attempts').getFullList({
      filter: `user_id="${userId}" && topic_id="${topicId}" && iso_week="${week}"`,
    });
    return rows.reduce((sum, r) => sum + (r.points_earned || 0), 0);
  } catch { return 0; }
}

export async function getUserSeenQuestionIds(userId, topicId) {
  // Returns the Set of question_bank IDs this user has already been served
  // for `topicId`. Used by the draw routine to prefer unseen questions and
  // by finishOnDemandTest to zero out points for repeats.
  try {
    const rows = await pb.collection('on_demand_attempts').getFullList({
      filter: `user_id="${userId}" && topic_id="${topicId}"`,
      fields: 'question_ids',
    });
    const seen = new Set();
    for (const r of rows) {
      for (const id of (r.question_ids || [])) seen.add(id);
    }
    return seen;
  } catch { return new Set(); }
}

export async function getAllOnDemandAttempts() {
  try {
    return await pb.collection('on_demand_attempts').getFullList({ sort: '-created' });
  } catch { return []; }
}

// ── Sources ──────────────────────────────────────────────────────────────────

export async function getSources() {
  return pb.collection('sources').getFullList();
}

export async function addSource(source) {
  return pb.collection('sources').create(source);
}

export async function updateSourceStatus(id, status, error = '') {
  return pb.collection('sources').update(id, { status, error });
}

export async function updateSourceProgress(id, progress) {
  return pb.collection('sources').update(id, { progress });
}

/**
 * Find sources stuck in 'processing' status — likely orphaned by a tab close.
 * Sources older than 5 minutes in 'processing' state are considered stale.
 */
export async function getOrphanedSources() {
  try {
    const all = await pb.collection('sources').getFullList({
      filter: 'status="processing"',
    });
    const fiveMinAgo = Date.now() - 5 * 60 * 1000;
    return all.filter(s => new Date(s.updated || s.created).getTime() < fiveMinAgo);
  } catch { return []; }
}

export async function deleteSource(id) {
  return pb.collection('sources').delete(id);
}

// ── Team Members ─────────────────────────────────────────────────────────────

export async function getTeamMembers() {
  return pb.collection('team_members').getFullList();
}

export async function addTeamMember(member) {
  return pb.collection('team_members').create(member);
}

export async function updateTeamMember(id, data) {
  return pb.collection('team_members').update(id, data);
}

export async function deleteTeamMember(id) {
  return pb.collection('team_members').delete(id);
}

// ── Quiz Results ──────────────────────────────────────────────────────────────
// Persists one record per user per week in the `test_results` PocketBase
// collection (recreated in migration 1780900001_created_test_results.js).

export async function saveQuizResult(userId, weekNumber, result) {
  const data = {
    user_id:      userId,
    week_number:  weekNumber,
    score:        result.score,
    total:        result.total,
    percentage:   result.percentage,
    time_used:    result.timeUsed,
    completed_at: result.completedAt,
    breakdown:    result.breakdown,
    points_earned: result.score * 2,
  };
  return pbUpsert(
    'test_results',
    `user_id="${userId}" && week_number=${weekNumber}`,
    data,
    data,
  );
}

export async function getQuizResult(userId, weekNumber) {
  try {
    // Use getList instead of getFirstListItem so that "no record yet" returns
    // an empty items array (HTTP 200) rather than throwing a ClientResponseError
    // that the SDK logs to the browser console as a 404.
    const res = await pb.collection('test_results').getList(1, 1, {
      filter: `user_id="${userId}" && week_number=${weekNumber}`,
      skipTotal: true,
      requestKey: null,
    });
    const r = res.items[0];
    if (!r) return null;
    // Map snake_case PB fields back to the camelCase shape Testen.jsx expects.
    return {
      score:        r.score,
      total:        r.total,
      percentage:   r.percentage,
      timeUsed:     r.time_used,
      completedAt:  r.completed_at,
      breakdown:    r.breakdown,
      pointsEarned: r.points_earned,
    };
  } catch {
    return null;
  }
}

// ── Quiz Cache (localStorage — ephemeral pre-generation) ─────────────────────
// Stores a pre-generated quiz object in localStorage so the Testen page can
// skip the LLM call when the user arrives after completing their learning
// session. The key includes weekNumber so stale entries are silently ignored
// when the week rolls over.

export function getCachedQuiz(userId, weekNumber) {
  const cached = storage.get(`quiz_cache:${userId}:${weekNumber}`);
  return Promise.resolve(cached || null);
}

export function setCachedQuiz(userId, weekNumber, quiz) {
  storage.set(`quiz_cache:${userId}:${weekNumber}`, quiz);
  return Promise.resolve(quiz);
}

// ── Learn Progress (DEPRECATED — collection dropped) ────────────────────────

/** @deprecated learn_progress collection has been dropped. */
export async function getLearnDone() { return false; }
/** @deprecated learn_progress collection has been dropped. */
export async function setLearnDone() { return null; }

// ── Theme Sessions ──────────────────────────────────────────────────────────

/**
 * Look up the cached theme session for a given (curriculum_version, week_number).
 * Returns null when no cache entry exists yet.
 */
export async function getThemeSession(curriculumVersionId, weekNumber) {
  try {
    const record = await pb.collection('theme_sessions').getFirstListItem(
      `curriculum_version = "${curriculumVersionId}" && week_number = ${weekNumber}`
    );
    return record;
  } catch {
    return null;
  }
}

/**
 * Upsert the theme session for a given (curriculum_version, week_number).
 */
export async function setThemeSession(curriculumVersionId, weekNumber, theme, content) {
  return pbUpsert(
    'theme_sessions',
    `curriculum_version = "${curriculumVersionId}" && week_number = ${weekNumber}`,
    { theme, content },
    { curriculum_version: curriculumVersionId, week_number: weekNumber, theme, content },
  );
}

/**
 * Return every cached theme session, newest first. Used by the admin
 * Content panel to surface what's been generated across all weeks.
 */
export async function getAllThemeSessions() {
  try {
    return await pb.collection('theme_sessions').getFullList({ sort: '-updated' });
  } catch { return []; }
}

/**
 * Delete a single theme session record by PocketBase id.
 */
export async function deleteThemeSession(id) {
  try { return await pb.collection('theme_sessions').delete(id); }
  catch { return null; }
}

/**
 * Has the user completed the theme session for a given personal week?
 * Returns the completion record, or null.
 */
export async function getThemeSessionCompletion(userId, sessionWeek) {
  try {
    return await pb.collection('theme_session_completions').getFirstListItem(
      `team_member_id = "${userId}" && session_week = ${sessionWeek}`
    );
  } catch {
    return null;
  }
}

/**
 * Mark a theme session as completed for the user/week. Idempotent.
 */
export async function setThemeSessionCompletion(userId, themeSessionId, sessionWeek) {
  return pbUpsert(
    'theme_session_completions',
    `team_member_id = "${userId}" && session_week = ${sessionWeek}`,
    { theme_session_id: themeSessionId },
    { team_member_id: userId, theme_session_id: themeSessionId, session_week: sessionWeek },
  );
}

// ── Onboarding track ─────────────────────────────────────────────────────────

/**
 * Cached onboarding overview for a theme, or null. `theme` is free text →
 * use pb.filter() bindings so quotes/specials can't break the filter.
 */
export async function getOnboardingOverview(theme) {
  try {
    return await pb.collection('onboarding_overviews').getFirstListItem(
      pb.filter('theme = {:theme}', { theme }),
    );
  } catch {
    return null;
  }
}

/** Upsert the cached overview for a theme (keyed by theme). */
export async function setOnboardingOverview(theme, content, fingerprint) {
  return pbUpsert(
    'onboarding_overviews',
    pb.filter('theme = {:theme}', { theme }),
    { content, topics_fingerprint: fingerprint },
    { theme, content, topics_fingerprint: fingerprint },
  );
}

/** All onboarding-completion rows for a user (each has a `theme`). */
export async function getCompletedOnboardingThemes(userId) {
  return pb.collection('onboarding_completions').getFullList({
    filter: pb.filter('team_member_id = {:userId}', { userId }),
  });
}

/** Mark a theme complete for the user. Idempotent (unique on user+theme). */
export async function setOnboardingCompletion(userId, theme) {
  return pbUpsert(
    'onboarding_completions',
    pb.filter('team_member_id = {:userId} && theme = {:theme}', { userId, theme }),
    { team_member_id: userId, theme },
    { team_member_id: userId, theme },
  );
}

// ── Leaderboard ───────────────────────────────────────────────────────────────

export async function getLeaderboard() {
  const entries = await pb.collection('leaderboard').getFullList();
  return entries.sort((a, b) => (b.points || 0) - (a.points || 0));
}

/**
 * Fetch every test_results record across all users, newest first.
 * Used by the Contributions page to build per-user heatmaps, streaks,
 * perfect-score counts, and the recent-activity feed in a single query.
 */
export async function getAllTestResults() {
  try {
    return await pb.collection('test_results').getFullList({ sort: '-created' });
  } catch { return []; }
}

export async function upsertLeaderboardEntry(userId, name, pointsDelta, testsCompletedDelta = 0) {
  try {
    const r = await pb.collection('leaderboard').getFirstListItem(`user_id="${userId}"`);
    return pb.collection('leaderboard').update(r.id, {
      points: (r.points || 0) + pointsDelta,
      tests_completed: (r.tests_completed || 0) + testsCompletedDelta,
    });
  } catch {
    return pb.collection('leaderboard').create({
      user_id: userId,
      name,
      points: pointsDelta,
      tests_completed: testsCompletedDelta,
      learnings_completed: 0,
    });
  }
}

// ── Settings ──────────────────────────────────────────────────────────────────

export async function getSetting(key, defaultValue = null) {
  try {
    const r = await pb.collection('settings').getFirstListItem(`key="${key}"`);
    return r.value ?? defaultValue;
  } catch { return defaultValue; }
}

export function setSetting(key, value) {
  return pbUpsert('settings', `key="${key}"`, { value: String(value) }, { key, value: String(value) });
}

// ── Graph Snapshots ───────────────────────────────────────────────────────────
//
// Stored in localStorage, not in PocketBase.
//
// The settings collection's `value` field is a plain text field designed for
// small strings (model overrides, flags). A full graph snapshot can easily
// reach 40–100 KB of JSON, which causes a 400 Bad Request from PocketBase.
//
// localStorage is the right choice here: the admin who clicks "Analyze" and
// the admin who clicks "Restore" are always the same person, on the same
// machine, in the same browser. Cross-device persistence is not required.

const SNAPSHOT_LS_KEY = 'respellion:graph_snapshot';

/**
 * Persist the current graph state as a rollback point in localStorage.
 * Called by useGraphData.bulkSave() before overwriting the graph.
 *
 * Silently no-ops if localStorage is unavailable (private browsing quota
 * exceeded, etc.) — the snapshot is best-effort, not critical-path.
 *
 * @param {object[]} topics
 * @param {object[]} relations
 */
export function saveGraphSnapshot(topics, relations) {
  try {
    localStorage.setItem(SNAPSHOT_LS_KEY, JSON.stringify({
      ts: Date.now(),
      topicCount: topics.length,
      relationCount: relations.length,
      topics,
      relations,
    }));
  } catch {
    // Storage quota exceeded or unavailable — skip silently.
  }
}

/**
 * Read the latest snapshot from localStorage.
 * Returns null when nothing is stored or the value is malformed.
 *
 * @returns {{ ts: number, topicCount: number, relationCount: number, topics: object[], relations: object[] } | null}
 */
export function getGraphSnapshot() {
  try {
    const raw = localStorage.getItem(SNAPSHOT_LS_KEY);
    if (!raw) return null;
    return JSON.parse(raw);
  } catch {
    return null;
  }
}

/**
 * Remove the stored snapshot after a successful restore so the admin cannot
 * accidentally restore twice.
 */
export function clearGraphSnapshot() {
  try {
    localStorage.removeItem(SNAPSHOT_LS_KEY);
  } catch { /* non-fatal */ }
}

// ── Curriculum Versions (v2) ──────────────────────────────────────────────────

export async function getCurriculumVersions(status) {
  try {
    const opts = { sort: '-version_number' };
    if (status) opts.filter = `status="${status}"`;
    return await pb.collection('curriculum_versions').getFullList(opts);
  } catch { return []; }
}

export async function getCurriculumVersion(id) {
  try {
    return await pb.collection('curriculum_versions').getOne(id);
  } catch { return null; }
}

export async function getActiveCurriculumVersion() {
  try {
    return await pb.collection('curriculum_versions').getFirstListItem('status="active"');
  } catch { return null; }
}

export async function getDraftCurriculumVersion() {
  try {
    return await pb.collection('curriculum_versions').getFirstListItem('status="draft"');
  } catch { return null; }
}

export async function createCurriculumVersion(data) {
  return pb.collection('curriculum_versions').create(data);
}

export async function updateCurriculumVersion(id, data) {
  return pb.collection('curriculum_versions').update(id, data);
}

export async function getNextVersionNumber() {
  try {
    const latest = await pb.collection('curriculum_versions').getFirstListItem('', {
      sort: '-version_number',
      fields: 'version_number',
    });
    return (latest?.version_number || 0) + 1;
  } catch { return 1; }
}

// ── Curriculum (legacy, v1 — deprecated) ──────────────────────────────────────

/** @deprecated Use curriculum_versions (v2) instead. */
export async function getCurriculum(year) {
  try {
    return await pb.collection('curriculum').getFullList({
      filter: `year=${year}`,
      sort: 'week_number',
    });
  } catch { return []; }
}

/** @deprecated Use curriculum_versions (v2) instead. */
export async function getCurriculumWeek(year, weekNumber) {
  try {
    return await pb.collection('curriculum').getFirstListItem(
      `year=${year} && week_number=${weekNumber}`
    );
  } catch { return null; }
}

/** @deprecated Use curriculum_versions (v2) instead. */
export function setCurriculumWeek(year, weekNumber, data) {
  return pbUpsert('curriculum', `year=${year} && week_number=${weekNumber}`,
    data, { year, week_number: weekNumber, ...data });
}

/** @deprecated Use curriculum_versions (v2) instead. */
export async function deleteCurriculumWeek(year, weekNumber) {
  try {
    const r = await pb.collection('curriculum').getFirstListItem(
      `year=${year} && week_number=${weekNumber}`
    );
    return pb.collection('curriculum').delete(r.id);
  } catch { /* nothing to delete */ }
}

/** @deprecated Use curriculum_versions (v2) instead. */
export async function bulkSetCurriculum(year, weeks) {
  // Delete all existing entries for this year first
  const existing = await getCurriculum(year);
  await Promise.all(
    existing.map(r => pb.collection('curriculum').delete(r.id, { requestKey: null }))
  );
  // Create all new entries
  return Promise.all(
    weeks.map(w => pb.collection('curriculum').create({
      year,
      week_number: w.week_number,
      topic_id: w.topic_id || '',
      theme: w.theme || '',
      quarter: w.quarter || Math.ceil(w.week_number / 13),
      is_review_week: w.is_review_week || false,
      sort_order: w.sort_order ?? w.week_number,
    }, { requestKey: null }))
  );
}

// ── Reset for Smoke Testing ──────────────────────────────────────────────────

/**
 * Truncate the collections that hold AI-generated and progress data.
 *
 * Order matters: rows that reference `topics.id` (relations, content,
 * quiz_banks) are deleted before topics themselves so we never leave
 * dangling references mid-wipe.
 *
 * `team_members`, `settings`, and `llm_calls` are intentionally preserved.
 *
 * @param {{ includeProgress?: boolean }} [opts]
 * @returns {Promise<Array<{collection: string, deleted: number, error?: string}>>}
 */
export async function resetForSmokeTest({
  includeProgress = false,
} = {}) {
  const collections = [
    'relations',
    'content',
    'curriculum',
    'sources',
    'topics',
    'micro_learnings',
    'micro_learning_completions',
    'theme_session_completions',
    'theme_sessions',
    'test_results',
  ];
  if (includeProgress) collections.push('leaderboard');

  const results = [];
  for (const name of collections) {
    try {
      const rows = await pb.collection(name).getFullList({ fields: 'id', requestKey: null });
      // Sequential deletes — bulk delete-many isn't in the JS SDK and parallel
      // deletes on the same collection sometimes 409 in PocketBase.
      let deleted = 0;
      for (const r of rows) {
        await pb.collection(name).delete(r.id, { requestKey: null });
        deleted++;
      }
      results.push({ collection: name, deleted });
    } catch (err) {
      results.push({ collection: name, deleted: 0, error: err?.message || String(err) });
    }
  }
  return results;
}


