import * as db from './db';
import { callLLM, cachedSystem } from './llm';
import { buildThemeTopicMap } from './curriculumService';
import { EMIT_ONBOARDING_OVERVIEW_TOOL } from './llmTools';

// The onboarding track introduces a new employee to every KB theme in breadth,
// spread over a guideline of 5 "days". A theme is the unit we track completion
// on; "days" are only a presentation grouping computed at read time, so
// re-chunking (e.g. when the theme set changes) never loses a user's progress.
export const ONBOARDING_DAY_COUNT = 5;

// ── Pure helpers (unit-tested) ───────────────────────────────────────────────

/**
 * Order theme names by their first appearance in the active curriculum schedule
 * (a pedagogical order), then append any KB themes the schedule never names,
 * alphabetically. Schedule labels that aren't real KB themes (e.g. merged-week
 * labels) are ignored. De-duplicated.
 *
 * @param {string[]} themeNames — canonical theme names present in the KB
 * @param {Array<{theme?: string}>|null} schedule — active curriculum schedule
 * @returns {string[]}
 */
export function orderThemes(themeNames, schedule) {
  const known = new Set(themeNames);
  const ordered = [];
  const seen = new Set();

  if (Array.isArray(schedule)) {
    for (const week of schedule) {
      const theme = week && week.theme;
      if (theme && known.has(theme) && !seen.has(theme)) {
        seen.add(theme);
        ordered.push(theme);
      }
    }
  }

  const remaining = themeNames
    .filter((t) => !seen.has(t))
    .sort((a, b) => a.localeCompare(b));

  return [...ordered, ...remaining];
}

/**
 * Split an ordered theme list into balanced, order-preserving day buckets.
 * Uses at most `dayCount` days; with fewer themes than days it uses `N` days.
 * The first `remainder` days get one extra theme.
 *
 * @param {string[]} orderedThemes
 * @param {number} [dayCount=5]
 * @returns {Array<{ day: number, themes: string[] }>} non-empty days only
 */
export function distributeThemesIntoDays(orderedThemes, dayCount = ONBOARDING_DAY_COUNT) {
  const themes = Array.isArray(orderedThemes) ? orderedThemes : [];
  const n = themes.length;
  if (n === 0) return [];

  const effectiveDays = Math.min(dayCount, n);
  const base = Math.floor(n / effectiveDays);
  const remainder = n % effectiveDays;

  const days = [];
  let idx = 0;
  for (let d = 0; d < effectiveDays; d++) {
    const size = base + (d < remainder ? 1 : 0);
    days.push({ day: d + 1, themes: themes.slice(idx, idx + size) });
    idx += size;
  }
  return days;
}

/**
 * Stable, order-independent fingerprint over a theme's topic ids. Changes when
 * a topic is added to or removed from the theme, so cached overviews can be
 * detected as stale and regenerated.
 *
 * @param {Array<{id?: string}>} topics
 * @returns {string}
 */
export function computeTopicsFingerprint(topics) {
  const ids = (Array.isArray(topics) ? topics : [])
    .map((t) => t && t.id)
    .filter(Boolean)
    .sort();
  const joined = ids.join(',');
  let h = 5381;
  for (let i = 0; i < joined.length; i++) {
    h = ((h << 5) + h + joined.charCodeAt(i)) >>> 0; // djb2, unsigned
  }
  return `${ids.length}:${h.toString(16)}`;
}

/**
 * Derive progress from the plan's days and the set of completed themes.
 *
 * @param {Array<{ day: number, themes: string[] }>} days
 * @param {Set<string>|string[]} completedThemeSet
 * @returns {{ themesTotal:number, themesDone:number, dayCount:number, daysCompleted:number, percentage:number, allDone:boolean }}
 */
export function computeOnboardingProgress(days, completedThemeSet) {
  const set = completedThemeSet instanceof Set
    ? completedThemeSet
    : new Set(completedThemeSet || []);
  const allThemes = (days || []).flatMap((d) => d.themes);
  const themesTotal = allThemes.length;
  const themesDone = allThemes.filter((t) => set.has(t)).length;
  const dayCount = (days || []).length;
  const daysCompleted = (days || []).filter(
    (d) => d.themes.length > 0 && d.themes.every((t) => set.has(t)),
  ).length;
  const percentage = themesTotal === 0 ? 0 : Math.round((themesDone / themesTotal) * 100);
  const allDone = themesTotal > 0 && themesDone === themesTotal;
  return { themesTotal, themesDone, dayCount, daysCompleted, percentage, allDone };
}

/**
 * Build the onboarding plan from already-loaded topics + active curriculum version.
 *
 * @param {Array} topics — all topics (db.getTopics())
 * @param {object|null} activeVersion — active curriculum_versions row (for schedule order)
 * @returns {{ themes: string[], days: Array<{day:number,themes:string[]}>, themeTopicMap: Map<string, Array> }}
 */
export function buildOnboardingPlan(topics, activeVersion) {
  const themeTopicMap = buildThemeTopicMap(topics || []);

  let schedule = activeVersion && activeVersion.schedule ? activeVersion.schedule : null;
  if (typeof schedule === 'string') {
    try { schedule = JSON.parse(schedule); } catch { schedule = null; }
  }

  const themes = orderThemes([...themeTopicMap.keys()], schedule);
  const days = distributeThemesIntoDays(themes);
  return { themes, days, themeTopicMap };
}

// ── I/O + generation ─────────────────────────────────────────────────────────

const ONBOARDING_OVERVIEW_SYSTEM = `You are an onboarding guide for Respellion, an internal IT company.
You introduce a brand-new employee to ONE theme from the company knowledge base.
This is a breadth-first introduction, not a deep lesson: keep it short, plain and skimmable, and always connect the theme to how work actually happens day-to-day and week-to-week at Respellion.
Write in clear, professional English. Emit content only through the emit_onboarding_overview tool.`;

function buildOverviewPrompt(theme, topics) {
  const topicLines = topics
    .map((t, idx) => `${idx + 1}. id=${t.id} | label="${t.label}" | type=${t.type || '—'} | description=${t.description || '—'}`)
    .join('\n');

  return `Theme: ${theme}

Topics that make up this theme (reuse the exact topic_id for each entry in topics_covered):
${topicLines || '(no topics listed)'}

Write a short, breadth-first onboarding overview of this theme for a new employee's first week: what it is, why it matters for their daily and weekly work at Respellion, 3–5 key points, and the list of topics it covers.`;
}

/**
 * Load everything needed to render the track.
 * @returns {Promise<{themes:string[], days:Array, themeTopicMap:Map}>}
 */
export async function getOnboardingPlan() {
  const [topics, activeVersion] = await Promise.all([
    db.getTopics(),
    db.getActiveCurriculumVersion(),
  ]);
  return buildOnboardingPlan(topics, activeVersion);
}

/**
 * Get the cached onboarding overview for a theme, or generate + cache it.
 * Regenerates when the theme's topic set has changed (fingerprint mismatch).
 *
 * @param {string} theme
 * @param {Array} topicsForTheme — topics belonging to the theme
 * @param {object} [opts]
 * @param {boolean} [opts.force=false]
 */
export async function getOrGenerateOnboardingOverview(theme, topicsForTheme, { force = false } = {}) {
  if (!theme) throw new Error('Onboarding overview requires a theme.');
  const topics = Array.isArray(topicsForTheme) ? topicsForTheme : [];
  const fingerprint = computeTopicsFingerprint(topics);

  if (!force) {
    const cached = await db.getOnboardingOverview(theme);
    if (cached && cached.topics_fingerprint === fingerprint) return cached;
  }

  const result = await callLLM({
    task: 'onboarding.overview',
    tier: 'fast',
    system: cachedSystem(ONBOARDING_OVERVIEW_SYSTEM),
    user: buildOverviewPrompt(theme, topics),
    tools: [EMIT_ONBOARDING_OVERVIEW_TOOL],
    toolChoice: { type: 'tool', name: EMIT_ONBOARDING_OVERVIEW_TOOL.name },
    maxTokens: 1500,
    timeoutMs: 60_000,
  });

  const emitted = result.toolUses[0]?.input;
  if (!emitted) throw new Error('AI did not return an onboarding overview. Please try again.');

  return db.setOnboardingOverview(theme, emitted, fingerprint);
}

/**
 * Set of theme names the user has marked complete.
 * @param {string} userId
 * @returns {Promise<Set<string>>}
 */
export async function getCompletedThemes(userId) {
  const rows = await db.getCompletedOnboardingThemes(userId);
  return new Set(rows.map((r) => r.theme));
}

/**
 * Mark a theme complete for the user. Idempotent.
 */
export async function markThemeCompleted(userId, theme) {
  return db.setOnboardingCompletion(userId, theme);
}

/**
 * Progress summary for the Dashboard chip.
 * @param {string} userId
 */
export async function getOnboardingSummary(userId) {
  const [plan, completed] = await Promise.all([
    getOnboardingPlan(),
    getCompletedThemes(userId),
  ]);
  return computeOnboardingProgress(plan.days, completed);
}
