/**
 * AI one-click bookmark organizer.
 * Uses SuperMark hosted OpenRouter AI (quota by plan) or a BYOK OpenAI-compatible API.
 */

import { callHostedAiChat, canUseHostedAi, getCachedMembershipStatus, isFeatureEntitled, PRO_FEATURES } from './membership.js';

export const AI_ORGANIZE_STORAGE_KEY = 'aiOrganizeSettings';
export const AI_ORGANIZE_SNAPSHOT_KEY = 'aiOrganizeSnapshots';
const AI_ORGANIZE_SNAPSHOT_LIMIT = 20;

export const AI_PROVIDER_PRESETS = [
  {
    id: 'hosted',
    labelKey: 'aiProviderHosted',
    baseUrl: '',
    model: ''
  },
  {
    id: 'openai',
    labelKey: 'aiProviderOpenAI',
    baseUrl: 'https://api.openai.com/v1',
    model: 'gpt-4o-mini'
  },
  {
    id: 'openrouter',
    labelKey: 'aiProviderOpenRouter',
    baseUrl: 'https://openrouter.ai/api/v1',
    model: 'openai/gpt-4o-mini'
  },
  {
    id: 'google',
    labelKey: 'aiProviderGoogle',
    baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
    model: 'gemini-2.0-flash'
  },
  {
    id: 'groq',
    labelKey: 'aiProviderGroq',
    baseUrl: 'https://api.groq.com/openai/v1',
    model: 'llama-3.3-70b-versatile'
  },
  {
    id: 'mistral',
    labelKey: 'aiProviderMistral',
    baseUrl: 'https://api.mistral.ai/v1',
    model: 'mistral-small-latest'
  },
  {
    id: 'custom',
    labelKey: 'aiProviderCustom',
    baseUrl: '',
    model: ''
  }
];

/**
 * OpenRouter models for SuperMark Hosted.
 * Curated from Artificial Analysis providers leaderboard (quality × price).
 * Curated from Artificial Analysis providers leaderboard
 * (speed × quality × price; prefer Flash/nano/mini).
 * Keep in sync with saas-membership/src/ai.js ALLOWED_AI_MODELS.
 *
 * @see https://artificialanalysis.ai/leaderboards/providers
 */
export const HOSTED_AI_MODELS = [
  { id: 'openai/gpt-5.4-nano', label: 'GPT-5.4 nano (fast + value)' },
  { id: 'openai/gpt-5.6-luna', label: 'GPT-5.6 Luna (fast)' },
  { id: 'openai/gpt-5.4-mini', label: 'GPT-5.4 mini' },
  { id: 'google/gemini-3.1-flash-lite', label: 'Gemini 3.1 Flash Lite' },
  { id: 'google/gemini-3.5-flash', label: 'Gemini 3.5 Flash' },
  { id: 'z-ai/glm-5.2', label: 'GLM 5.2' },
  { id: 'stepfun/step-3.7-flash', label: 'Step 3.7 Flash' },
  { id: 'qwen/qwen3.6-flash', label: 'Qwen3.6 Flash' },
  { id: 'deepseek/deepseek-v4-flash', label: 'DeepSeek V4 Flash (budget)' }
];

export const DEFAULT_HOSTED_MODEL = HOSTED_AI_MODELS[0].id;

const DEFAULT_SETTINGS = {
  provider: 'hosted',
  apiKey: '',
  baseUrl: 'https://api.openai.com/v1',
  model: 'gpt-4o-mini',
  hostedModel: DEFAULT_HOSTED_MODEL
};

const LEGACY_CHINA_PROVIDERS = new Set(['deepseek', 'moonshot', 'siliconflow']);
const MIN_BOOKMARKS = 3;
const MAX_BOOKMARKS = 120;

function i18n(key, fallback, substitutions) {
  const msg = chrome.i18n.getMessage(key, substitutions);
  return msg || fallback || key;
}

export function getDefaultAiOrganizeSettings() {
  return { ...DEFAULT_SETTINGS };
}

export async function loadAiOrganizeSettings() {
  const result = await chrome.storage.local.get(AI_ORGANIZE_STORAGE_KEY);
  const settings = { ...DEFAULT_SETTINGS, ...(result[AI_ORGANIZE_STORAGE_KEY] || {}) };
  const knownIds = new Set(AI_PROVIDER_PRESETS.map((preset) => preset.id));
  const hostedIds = new Set(HOSTED_AI_MODELS.map((m) => m.id));

  // Removed China-based presets: keep credentials but switch UI to Custom.
  if (!knownIds.has(settings.provider) || LEGACY_CHINA_PROVIDERS.has(settings.provider)) {
    settings.provider = 'custom';
  }

  if (!hostedIds.has(settings.hostedModel)) {
    // Migrate older hosted installs that stored Workers AI id in `model`
    if (hostedIds.has(settings.model)) {
      settings.hostedModel = settings.model;
    } else {
      settings.hostedModel = DEFAULT_HOSTED_MODEL;
    }
  }

  return settings;
}

export async function saveAiOrganizeSettings(settings) {
  const provider = settings.provider || DEFAULT_SETTINGS.provider;
  const current = (await chrome.storage.local.get(AI_ORGANIZE_STORAGE_KEY))[AI_ORGANIZE_STORAGE_KEY] || {};
  const hostedIds = new Set(HOSTED_AI_MODELS.map((m) => m.id));

  if (provider === 'hosted') {
    const requestedHosted = String(settings.hostedModel || settings.model || '').trim();
    const hostedModel = hostedIds.has(requestedHosted) ? requestedHosted : DEFAULT_HOSTED_MODEL;
    const next = {
      provider: 'hosted',
      apiKey: String(current.apiKey || settings.apiKey || '').trim(),
      baseUrl: String(current.baseUrl || settings.baseUrl || DEFAULT_SETTINGS.baseUrl).trim().replace(/\/+$/, ''),
      model: String(current.model || settings.model || DEFAULT_SETTINGS.model).trim(),
      hostedModel
    };
    // Keep BYOK model separate from hosted model id
    if (hostedIds.has(next.model)) {
      next.model = DEFAULT_SETTINGS.model;
    }
    await chrome.storage.local.set({ [AI_ORGANIZE_STORAGE_KEY]: next });
    return next;
  }

  const next = {
    provider,
    apiKey: String(settings.apiKey || '').trim(),
    baseUrl: String(settings.baseUrl || '').trim().replace(/\/+$/, ''),
    model: String(settings.model || '').trim(),
    hostedModel: String(current.hostedModel || settings.hostedModel || DEFAULT_HOSTED_MODEL).trim()
  };
  await chrome.storage.local.set({ [AI_ORGANIZE_STORAGE_KEY]: next });
  return next;
}

export function resolveHostedModel(settings) {
  const hostedIds = new Set(HOSTED_AI_MODELS.map((m) => m.id));
  const id = String(settings?.hostedModel || '').trim();
  return hostedIds.has(id) ? id : DEFAULT_HOSTED_MODEL;
}

export function hasAiApiKey(settings) {
  return Boolean(settings?.apiKey?.trim() && settings?.baseUrl?.trim() && settings?.model?.trim());
}

export function isHostedAiProvider(settings) {
  return settings?.provider === 'hosted';
}

export async function canRunAiOrganize(settings, { aiFeature = 'ai_organize' } = {}) {
  const cfg = settings || await loadAiOrganizeSettings();
  const featureId = String(aiFeature || 'ai_organize');
  if (isHostedAiProvider(cfg)) {
    const status = await getCachedMembershipStatus();
    if (!status?.signedIn) return { ok: false, reason: 'NOT_SIGNED_IN' };
    if (await canUseHostedAi(featureId)) return { ok: true, mode: 'hosted' };
    return { ok: false, reason: 'AI_QUOTA_EXCEEDED' };
  }
  // BYOK only for Premium customize_model_providers
  if (hasAiApiKey(cfg)) {
    if (await isFeatureEntitled(PRO_FEATURES.CUSTOMIZE_MODEL_PROVIDERS)) {
      return { ok: true, mode: 'byok' };
    }
    // Fall back to hosted if entitled + quota remains
    if (await canUseHostedAi(featureId)) return { ok: true, mode: 'hosted' };
    return { ok: false, reason: 'FEATURE_REQUIRED' };
  }
  if (await canUseHostedAi(featureId)) return { ok: true, mode: 'hosted' };
  return { ok: false, reason: 'AI_NOT_CONFIGURED' };
}

function normalizeBaseUrl(baseUrl) {
  return String(baseUrl || '').trim().replace(/\/+$/, '');
}

function buildAiRequestHeaders(settings) {
  const headers = {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${String(settings.apiKey || '').trim()}`
  };

  const baseUrl = normalizeBaseUrl(settings.baseUrl);
  if (settings.provider === 'openrouter' || baseUrl.includes('openrouter.ai')) {
    headers['HTTP-Referer'] = chrome.runtime.getURL('src/index.html');
    headers['X-Title'] = 'SuperMark';
  }

  return headers;
}

function originPatternFromBaseUrl(baseUrl) {
  const normalized = normalizeBaseUrl(baseUrl);
  let url;
  try {
    url = new URL(normalized);
  } catch {
    throw new Error('INVALID_API_BASE_URL');
  }
  if (url.protocol !== 'https:' && url.protocol !== 'http:') {
    throw new Error('INVALID_API_BASE_URL');
  }
  return `${url.origin}/*`;
}

function permissionsApiCall(method, options) {
  return new Promise((resolve, reject) => {
    const api = chrome.permissions?.[method];
    if (typeof api !== 'function') {
      resolve(true);
      return;
    }

    let settled = false;
    const finish = (fn, value) => {
      if (settled) return;
      settled = true;
      fn(value);
    };

    try {
      // Prefer promise-style (MV3). Passing a callback can dual-fire on some builds.
      const result = api.call(chrome.permissions, options);
      if (result && typeof result.then === 'function') {
        result
          .then((value) => finish(resolve, Boolean(value)))
          .catch((error) => finish(reject, error));
        return;
      }

      // Callback-only fallback
      api.call(chrome.permissions, options, (value) => {
        if (chrome.runtime.lastError) {
          finish(reject, new Error(chrome.runtime.lastError.message));
          return;
        }
        finish(resolve, Boolean(value));
      });
    } catch (error) {
      finish(reject, error);
    }
  });
}

export async function ensureAiHostPermission(baseUrl) {
  if (!chrome.permissions?.request) {
    return true;
  }
  const origin = originPatternFromBaseUrl(baseUrl);
  try {
    const already = await permissionsApiCall('contains', { origins: [origin] });
    if (already) return true;
    return await permissionsApiCall('request', { origins: [origin] });
  } catch (error) {
    console.error('Failed to request AI host permission:', error);
    throw error;
  }
}

function extractHostname(url) {
  try {
    return new URL(url).hostname.replace(/^www\./, '');
  } catch {
    return '';
  }
}

/**
 * Recursively collect all bookmarks (with URL) under a folder, including nested subfolders.
 * Also returns descendant folder ids for post-organize cleanup.
 */
export async function collectFolderBookmarks(folderId) {
  const nodes = await new Promise((resolve, reject) => {
    chrome.bookmarks.getSubTree(folderId, (result) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      resolve(result || []);
    });
  });

  const root = nodes[0];
  const bookmarks = [];
  const subfolderIds = [];

  const walk = (node, depth) => {
    if (!node) return;

    if (node.url) {
      bookmarks.push({
        id: node.id,
        title: (node.title || node.url || '').slice(0, 120),
        url: node.url,
        host: extractHostname(node.url),
        fromSubfolder: depth > 1
      });
      return;
    }

    // depth 0 = target folder itself; depth >= 1 = descendant folders
    if (depth >= 1 && node.id) {
      subfolderIds.push(node.id);
    }

    for (const child of node.children || []) {
      walk(child, depth + 1);
    }
  };

  if (root) {
    walk(root, 0);
  }

  return { bookmarks, subfolderIds };
}

function buildPrompt(bookmarks, localeHint) {
  const list = bookmarks.map((b, index) => (
    `${index + 1}. id=${b.id} | title=${JSON.stringify(b.title)} | host=${b.host}`
  )).join('\n');

  return [
    'You organize browser bookmarks into a small set of clear topical folders.',
    'The list includes bookmarks from the selected folder AND all nested subfolders (flattened).',
    'Rules:',
    '- Return ONLY valid JSON, no markdown.',
    '- Schema: {"categories":[{"name":"FolderName","ids":["id1","id2"]}]}',
    '- Use every bookmark id exactly once.',
    '- Prefer 3–10 categories. Merge tiny topics.',
    '- Category names should be short (2–12 chars / words).',
    `- Write category names in the user language hint: ${localeHint}.`,
    '- Do not invent ids. Only use ids from the list.',
    '- Ignore previous folder structure; regroup purely by topic.',
    '',
    'Bookmarks:',
    list
  ].join('\n');
}

function extractJsonObject(text) {
  // Defense if a proxy accidentally stringifies objects with String(obj)
  if (text && typeof text === 'object') {
    return text;
  }
  const trimmed = String(text || '').trim();
  if (!trimmed || trimmed === '[object Object]') {
    throw new Error('EMPTY_AI_RESPONSE');
  }

  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
  const candidate = fenced ? fenced[1].trim() : trimmed;
  const start = candidate.indexOf('{');
  const end = candidate.lastIndexOf('}');
  if (start === -1 || end === -1 || end <= start) {
    throw new Error('INVALID_AI_JSON');
  }
  return JSON.parse(candidate.slice(start, end + 1));
}

function validatePlan(rawPlan, bookmarks) {
  const idSet = new Set(bookmarks.map((b) => b.id));
  const byId = new Map(bookmarks.map((b) => [b.id, b]));
  const used = new Set();
  const categories = [];

  const rawCategories = Array.isArray(rawPlan?.categories) ? rawPlan.categories : [];
  for (const cat of rawCategories) {
    const name = String(cat?.name || '').trim().slice(0, 40);
    const ids = Array.isArray(cat?.ids) ? cat.ids.map(String) : [];
    if (!name || !ids.length) continue;

    const uniqueIds = [];
    for (const id of ids) {
      if (!idSet.has(id) || used.has(id)) continue;
      used.add(id);
      uniqueIds.push(id);
    }
    if (uniqueIds.length) {
      categories.push({
        name,
        ids: uniqueIds,
        bookmarks: uniqueIds.map((id) => byId.get(id)).filter(Boolean)
      });
    }
  }

  const leftover = bookmarks.filter((b) => !used.has(b.id));
  if (leftover.length) {
    const otherName = i18n('aiOrganizeOtherCategory', 'Other');
    let other = categories.find((c) => c.name === otherName);
    if (!other) {
      other = { name: otherName, ids: [], bookmarks: [] };
      categories.push(other);
    }
    for (const item of leftover) {
      other.ids.push(item.id);
      other.bookmarks.push(item);
    }
  }

  // Drop single-category plans that are just "everything in one folder"
  if (categories.length < 2) {
    throw new Error('PLAN_TOO_SIMPLE');
  }

  return categories;
}

async function callHostedChatCompletions(bookmarks, settings) {
  const localeHint = (chrome.i18n.getUILanguage() || 'en').replace('_', '-');
  // Always re-read so the latest saved hostedModel is used
  const latest = settings || await loadAiOrganizeSettings();
  const model = resolveHostedModel(latest);
  const data = await callHostedAiChat({
    temperature: 0.2,
    maxTokens: 2048,
    model,
    feature: PRO_FEATURES.AI_ORGANIZE,
    messages: [
      {
        role: 'system',
        content: 'You are a bookmark organization assistant. Respond with JSON only.'
      },
      {
        role: 'user',
        content: buildPrompt(bookmarks, localeHint)
      }
    ]
  });
  if (data?.model && data.model !== model) {
    console.warn('Hosted AI model mismatch:', { requested: model, used: data.model });
  }
  const content = data?.choices?.[0]?.message?.content;
  return extractJsonObject(content);
}

async function callChatCompletions(settings, bookmarks) {
  const localeHint = (chrome.i18n.getUILanguage() || 'en').replace('_', '-');
  const endpoint = `${normalizeBaseUrl(settings.baseUrl)}/chat/completions`;

  const response = await fetch(endpoint, {
    method: 'POST',
    headers: buildAiRequestHeaders(settings),
    body: JSON.stringify({
      model: settings.model.trim(),
      temperature: 0.2,
      response_format: { type: 'json_object' },
      messages: [
        {
          role: 'system',
          content: 'You are a bookmark organization assistant. Respond with JSON only.'
        },
        {
          role: 'user',
          content: buildPrompt(bookmarks, localeHint)
        }
      ]
    })
  });

  if (!response.ok) {
    let detail = '';
    try {
      const errBody = await response.json();
      detail = errBody?.error?.message || errBody?.message || '';
    } catch {
      detail = await response.text().catch(() => '');
    }
    const error = new Error(detail || `HTTP_${response.status}`);
    error.status = response.status;
    throw error;
  }

  const data = await response.json();
  const content = data?.choices?.[0]?.message?.content;
  return extractJsonObject(content);
}

/**
 * Ask AI for an organization plan. Does not mutate bookmarks.
 */
export async function proposeAiOrganizePlan(folderId) {
  const settings = await loadAiOrganizeSettings();
  const readiness = await canRunAiOrganize(settings);
  if (!readiness.ok) {
    throw new Error(readiness.reason || 'AI_NOT_CONFIGURED');
  }

  const { bookmarks, subfolderIds } = await collectFolderBookmarks(folderId);
  if (bookmarks.length < MIN_BOOKMARKS) {
    const error = new Error('TOO_FEW_BOOKMARKS');
    error.count = bookmarks.length;
    error.min = MIN_BOOKMARKS;
    throw error;
  }

  const limited = bookmarks.slice(0, MAX_BOOKMARKS);
  const useHosted = readiness.mode === 'hosted';

  let rawPlan;
  if (useHosted) {
    try {
      // Reload so UI selection saved to storage is always applied
      const hostedSettings = await loadAiOrganizeSettings();
      rawPlan = await callHostedChatCompletions(limited, hostedSettings);
    } catch (error) {
      const code = error.code || error.message;
      if (
        (code === 'quota_exceeded' || code === 'quota_none' || error.status === 429 || error.status === 403)
        && hasAiApiKey(settings)
        && !isHostedAiProvider(settings)
      ) {
        // Fall through to BYOK
      } else if (code === 'quota_exceeded' || code === 'quota_none') {
        throw new Error(code === 'quota_none' ? 'AI_QUOTA_NONE' : 'AI_QUOTA_EXCEEDED');
      } else if (code === 'NOT_SIGNED_IN') {
        throw new Error('NOT_SIGNED_IN');
      } else if (code === 'invalid_model' || code === 'MODEL_REQUIRED') {
        throw new Error('AI_NOT_CONFIGURED');
      } else {
        throw error;
      }
    }
  }

  if (!rawPlan) {
    if (!hasAiApiKey(settings)) {
      throw new Error('AI_NOT_CONFIGURED');
    }
    const hasPermission = await chrome.permissions?.contains?.({
      origins: [`${new URL(normalizeBaseUrl(settings.baseUrl)).origin}/*`]
    });
    if (hasPermission === false) {
      const granted = await ensureAiHostPermission(settings.baseUrl);
      if (!granted) {
        throw new Error('PERMISSION_DENIED');
      }
    }

    try {
      rawPlan = await callChatCompletions(settings, limited);
    } catch (error) {
      if (String(error.message || '').toLowerCase().includes('response_format')
        || error.status === 400) {
        rawPlan = await callChatCompletionsWithoutJsonMode(settings, limited);
      } else {
        throw error;
      }
    }
  }

  const categories = validatePlan(rawPlan, limited);
  const fromSubfolders = limited.filter((b) => b.fromSubfolder).length;
  return {
    folderId,
    total: limited.length,
    truncated: bookmarks.length > limited.length,
    fromSubfolders,
    subfolderIds,
    categories
  };
}

async function callChatCompletionsWithoutJsonMode(settings, bookmarks) {
  const localeHint = (chrome.i18n.getUILanguage() || 'en').replace('_', '-');
  const endpoint = `${normalizeBaseUrl(settings.baseUrl)}/chat/completions`;

  const response = await fetch(endpoint, {
    method: 'POST',
    headers: buildAiRequestHeaders(settings),
    body: JSON.stringify({
      model: settings.model.trim(),
      temperature: 0.2,
      messages: [
        {
          role: 'system',
          content: 'You are a bookmark organization assistant. Respond with JSON only.'
        },
        {
          role: 'user',
          content: buildPrompt(bookmarks, localeHint)
        }
      ]
    })
  });

  if (!response.ok) {
    let detail = '';
    try {
      const errBody = await response.json();
      detail = errBody?.error?.message || errBody?.message || '';
    } catch {
      detail = await response.text().catch(() => '');
    }
    const error = new Error(detail || `HTTP_${response.status}`);
    error.status = response.status;
    throw error;
  }

  const data = await response.json();
  return extractJsonObject(data?.choices?.[0]?.message?.content);
}

function createBookmarkNode(details) {
  return new Promise((resolve, reject) => {
    chrome.bookmarks.create(details, (result) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      resolve(result);
    });
  });
}

function moveBookmarkNode(id, destination) {
  return new Promise((resolve, reject) => {
    chrome.bookmarks.move(id, destination, (result) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      resolve(result);
    });
  });
}

function removeBookmarkNode(id) {
  return new Promise((resolve, reject) => {
    chrome.bookmarks.remove(id, () => {
      if (chrome.runtime.lastError) {
        // Folder may not be empty or already removed — treat as soft failure
        resolve(false);
        return;
      }
      resolve(true);
    });
  });
}

function removeBookmarkTree(id) {
  return new Promise((resolve) => {
    chrome.bookmarks.removeTree(id, () => {
      if (chrome.runtime.lastError) {
        resolve(false);
        return;
      }
      resolve(true);
    });
  });
}

function getBookmarkChildren(id) {
  return new Promise((resolve, reject) => {
    chrome.bookmarks.getChildren(id, (nodes) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      resolve(nodes || []);
    });
  });
}

function getBookmarkSubTree(folderId) {
  return new Promise((resolve, reject) => {
    chrome.bookmarks.getSubTree(folderId, (result) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      resolve(result || []);
    });
  });
}

function bookmarkNodeExists(id) {
  return new Promise((resolve) => {
    chrome.bookmarks.get(id, (nodes) => {
      if (chrome.runtime.lastError || !nodes?.length) {
        resolve(false);
        return;
      }
      resolve(true);
    });
  });
}

function serializeSnapshotNode(node) {
  if (node.url) {
    return {
      type: 'bookmark',
      id: node.id,
      title: node.title || '',
      url: node.url,
      index: typeof node.index === 'number' ? node.index : 0
    };
  }

  return {
    type: 'folder',
    id: node.id,
    title: node.title || '',
    index: typeof node.index === 'number' ? node.index : 0,
    children: (node.children || []).map(serializeSnapshotNode)
  };
}

function collectSnapshotBookmarkIds(nodes, out = []) {
  for (const node of nodes || []) {
    if (node.type === 'bookmark') {
      out.push(node.id);
    } else {
      collectSnapshotBookmarkIds(node.children, out);
    }
  }
  return out;
}

/**
 * Capture the full child tree under a folder before AI organize mutates it.
 */
export async function captureFolderSnapshot(folderId) {
  const nodes = await getBookmarkSubTree(folderId);
  const root = nodes[0];
  if (!root) {
    throw new Error('FOLDER_NOT_FOUND');
  }

  const tree = (root.children || []).map(serializeSnapshotNode);
  return {
    folderId,
    folderTitle: root.title || '',
    capturedAt: Date.now(),
    tree,
    bookmarkIds: collectSnapshotBookmarkIds(tree)
  };
}

export async function loadAiOrganizeSnapshot(folderId) {
  if (!folderId) return null;
  const result = await chrome.storage.local.get(AI_ORGANIZE_SNAPSHOT_KEY);
  const map = result[AI_ORGANIZE_SNAPSHOT_KEY] || {};
  return map[folderId] || null;
}

export async function hasAiOrganizeSnapshot(folderId) {
  return Boolean(await loadAiOrganizeSnapshot(folderId));
}

export async function saveAiOrganizeSnapshot(snapshot) {
  if (!snapshot?.folderId) return null;

  const result = await chrome.storage.local.get(AI_ORGANIZE_SNAPSHOT_KEY);
  const map = { ...(result[AI_ORGANIZE_SNAPSHOT_KEY] || {}) };
  map[snapshot.folderId] = snapshot;

  const entries = Object.entries(map);
  if (entries.length > AI_ORGANIZE_SNAPSHOT_LIMIT) {
    entries.sort((a, b) => (a[1]?.capturedAt || 0) - (b[1]?.capturedAt || 0));
    const trimmed = Object.fromEntries(entries.slice(-AI_ORGANIZE_SNAPSHOT_LIMIT));
    await chrome.storage.local.set({ [AI_ORGANIZE_SNAPSHOT_KEY]: trimmed });
  } else {
    await chrome.storage.local.set({ [AI_ORGANIZE_SNAPSHOT_KEY]: map });
  }

  return snapshot;
}

export async function clearAiOrganizeSnapshot(folderId) {
  if (!folderId) return;
  const result = await chrome.storage.local.get(AI_ORGANIZE_SNAPSHOT_KEY);
  const map = { ...(result[AI_ORGANIZE_SNAPSHOT_KEY] || {}) };
  if (!(folderId in map)) return;
  delete map[folderId];
  await chrome.storage.local.set({ [AI_ORGANIZE_SNAPSHOT_KEY]: map });
}

/**
 * Restore a folder to the snapshot taken before the last AI organize.
 * Bookmark IDs are preserved via move; folders are recreated.
 */
export async function restoreAiOrganizeSnapshot(folderId) {
  const snapshot = await loadAiOrganizeSnapshot(folderId);
  if (!snapshot?.tree) {
    throw new Error('NO_SNAPSHOT');
  }

  if (!(await bookmarkNodeExists(folderId))) {
    throw new Error('FOLDER_NOT_FOUND');
  }

  const nodes = await getBookmarkSubTree(folderId);
  const root = nodes[0];
  if (!root) {
    throw new Error('FOLDER_NOT_FOUND');
  }

  const bookmarkNodes = [];
  const folderIds = [];
  const walkCurrent = (node, depth) => {
    if (!node) return;
    if (node.url) {
      bookmarkNodes.push(node);
      return;
    }
    if (depth >= 1 && node.id) {
      folderIds.push(node.id);
    }
    for (const child of node.children || []) {
      walkCurrent(child, depth + 1);
    }
  };
  walkCurrent(root, 0);

  // Move all bookmarks to the folder root so descendant folders can be removed safely.
  for (const bookmark of bookmarkNodes) {
    if (bookmark.parentId === folderId) continue;
    try {
      await moveBookmarkNode(bookmark.id, { parentId: folderId });
    } catch (error) {
      console.warn('Failed to flatten bookmark before restore', bookmark.id, error);
    }
  }

  for (const id of [...folderIds].reverse()) {
    await removeBookmarkTree(id);
  }

  let foldersCreated = 0;
  let bookmarksRestored = 0;
  let bookmarksRecreated = 0;
  let incomplete = false;

  async function rebuild(children, parentId) {
    for (let i = 0; i < (children || []).length; i += 1) {
      const node = children[i];
      if (node.type === 'folder') {
        const folder = await createBookmarkNode({
          parentId,
          title: node.title || i18n('aiOrganizeOtherCategory', 'Other'),
          index: i
        });
        foldersCreated += 1;
        await rebuild(node.children || [], folder.id);
        continue;
      }

      if (!node?.id) {
        incomplete = true;
        continue;
      }

      if (await bookmarkNodeExists(node.id)) {
        await moveBookmarkNode(node.id, { parentId, index: i });
        bookmarksRestored += 1;
      } else if (node.url) {
        await createBookmarkNode({
          parentId,
          title: node.title || node.url,
          url: node.url,
          index: i
        });
        bookmarksRecreated += 1;
        incomplete = true;
      } else {
        incomplete = true;
      }
    }
  }

  await rebuild(snapshot.tree, folderId);
  await clearAiOrganizeSnapshot(folderId);

  return {
    foldersCreated,
    bookmarksRestored,
    bookmarksRecreated,
    incomplete
  };
}

export function showAiOrganizeRestoreConfirm(folderTitle) {
  return new Promise((resolve) => {
    const dialog = document.getElementById('ai-organize-restore-dialog');
    const descEl = document.getElementById('ai-organize-restore-description');
    const confirmBtn = document.getElementById('ai-organize-restore-confirm-button');
    const cancelBtn = document.getElementById('ai-organize-restore-cancel-button');
    const closeBtn = document.getElementById('ai-organize-restore-close-button');

    if (!dialog || !confirmBtn || !cancelBtn) {
      resolve(window.confirm(
        i18n(
          'aiOrganizeRestoreDescription',
          'This will undo the last AI Organize for this folder and restore the previous layout.'
        )
      ));
      return;
    }

    if (descEl) {
      const base = i18n(
        'aiOrganizeRestoreDescription',
        'This will undo the last AI Organize for this folder and restore the previous layout.'
      );
      descEl.textContent = folderTitle
        ? `${base} (${folderTitle})`
        : base;
    }

    dialog.style.display = 'block';

    const cleanup = () => {
      confirmBtn.removeEventListener('click', onConfirm);
      cancelBtn.removeEventListener('click', onCancel);
      closeBtn?.removeEventListener('click', onCancel);
      dialog.style.display = 'none';
    };

    const onConfirm = () => {
      cleanup();
      resolve(true);
    };
    const onCancel = () => {
      cleanup();
      resolve(false);
    };

    confirmBtn.addEventListener('click', onConfirm);
    cancelBtn.addEventListener('click', onCancel);
    closeBtn?.addEventListener('click', onCancel);
  });
}

/**
 * Remove empty descendant folders after bookmarks have been moved out.
 * Processes deepest folders first so parents can become empty afterward.
 */
async function pruneEmptySubfolders(subfolderIds) {
  let removed = 0;
  // subfolderIds are collected depth-first (parents before children); reverse → deepest first
  for (const id of [...subfolderIds].reverse()) {
    try {
      const children = await getBookmarkChildren(id);
      if (children.length === 0) {
        const ok = await removeBookmarkNode(id);
        if (ok) removed += 1;
      }
    } catch (error) {
      console.warn('Skip pruning folder', id, error);
    }
  }
  return removed;
}

/**
 * Apply an approved plan: create subfolders, move bookmarks (including from nested folders),
 * then remove emptied old subfolders.
 */
export async function applyAiOrganizePlan(folderId, categories, subfolderIds = []) {
  let foldersCreated = 0;
  let bookmarksMoved = 0;

  for (const category of categories) {
    if (!category.ids?.length) continue;
    const folder = await createBookmarkNode({
      parentId: folderId,
      title: category.name
    });
    foldersCreated += 1;

    for (const id of category.ids) {
      await moveBookmarkNode(id, { parentId: folder.id });
      bookmarksMoved += 1;
    }
  }

  const foldersRemoved = await pruneEmptySubfolders(subfolderIds);
  return { foldersCreated, bookmarksMoved, foldersRemoved };
}

export function getAiOrganizeErrorMessage(error) {
  const code = error?.message || '';
  switch (code) {
    case 'AI_NOT_CONFIGURED':
      return i18n('aiOrganizeNeedConfig', 'Please configure AI API in Settings → AI first');
    case 'NOT_SIGNED_IN':
      return i18n('aiOrganizeNeedSignIn', 'Sign in to use SuperMark hosted AI');
    case 'AI_QUOTA_EXCEEDED':
      return i18n(
        'aiOrganizeQuotaExceeded',
        'Monthly hosted AI quota used up. Upgrade your plan or use your own API key.'
      );
    case 'AI_QUOTA_NONE':
      return i18n(
        'aiOrganizeQuotaNone',
        'Your plan has no hosted AI quota. Upgrade or configure your own API key.'
      );
    case 'TOO_FEW_BOOKMARKS':
      return i18n(
        'aiOrganizeTooFew',
        'Need at least $1 bookmarks in this folder',
        [String(error.min || MIN_BOOKMARKS)]
      );
    case 'PERMISSION_DENIED':
      return i18n('aiOrganizePermissionDenied', 'Network permission denied for the AI API');
    case 'PLAN_TOO_SIMPLE':
      return i18n('aiOrganizePlanTooSimple', 'AI could not find meaningful categories');
    case 'EMPTY_AI_RESPONSE':
    case 'INVALID_AI_JSON':
      return i18n('aiOrganizeInvalidResponse', 'AI returned an invalid response');
    case 'NO_SNAPSHOT':
      return i18n('aiOrganizeRestoreNone', 'No previous AI Organize snapshot for this folder');
    case 'FOLDER_NOT_FOUND':
      return i18n('aiOrganizeNoFolder', 'No folder selected');
    default:
      return i18n('aiOrganizeError', 'AI organize failed: $1', [code.slice(0, 120)]);
  }
}

/**
 * Show preview modal and resolve when user confirms or cancels.
 */
export function showAiOrganizePreview(plan) {
  return new Promise((resolve) => {
    const dialog = document.getElementById('ai-organize-dialog');
    const listEl = document.getElementById('ai-organize-preview-list');
    const summaryEl = document.getElementById('ai-organize-preview-summary');
    const confirmBtn = document.getElementById('ai-organize-confirm-button');
    const cancelBtn = document.getElementById('ai-organize-cancel-button');
    const closeBtn = document.getElementById('ai-organize-close-button');

    if (!dialog || !listEl || !confirmBtn || !cancelBtn) {
      resolve(false);
      return;
    }

    summaryEl.textContent = i18n(
      'aiOrganizePreviewSummary',
      'Will create $1 folders and move $2 bookmarks (including subfolders)',
      [String(plan.categories.length), String(plan.total)]
    );
    if (plan.fromSubfolders > 0) {
      summaryEl.textContent += ' '
        + i18n(
          'aiOrganizePreviewSubfoldersHint',
          '($1 from nested folders)',
          [String(plan.fromSubfolders)]
        );
    }

    listEl.innerHTML = '';
    for (const category of plan.categories) {
      const item = document.createElement('div');
      item.className = 'ai-organize-category';

      const header = document.createElement('div');
      header.className = 'ai-organize-category-header';
      header.innerHTML = `<strong>${escapeHtml(category.name)}</strong><span>${category.ids.length}</span>`;

      const titles = document.createElement('ul');
      titles.className = 'ai-organize-category-titles';
      const previewBookmarks = category.bookmarks.slice(0, 12);
      for (const bookmark of previewBookmarks) {
        const li = document.createElement('li');
        li.className = 'ai-organize-category-title-item';
        li.textContent = bookmark.title;
        if (bookmark.host) {
          li.title = bookmark.host;
        }
        titles.appendChild(li);
      }
      if (category.bookmarks.length > previewBookmarks.length) {
        const more = document.createElement('li');
        more.className = 'ai-organize-category-title-more';
        more.textContent = i18n(
          'aiOrganizePreviewMore',
          '+$1 more',
          [String(category.bookmarks.length - previewBookmarks.length)]
        );
        titles.appendChild(more);
      }

      item.appendChild(header);
      item.appendChild(titles);
      listEl.appendChild(item);
    }

    dialog.style.display = 'block';

    const cleanup = () => {
      confirmBtn.removeEventListener('click', onConfirm);
      cancelBtn.removeEventListener('click', onCancel);
      closeBtn?.removeEventListener('click', onCancel);
      dialog.style.display = 'none';
    };

    const onConfirm = () => {
      cleanup();
      resolve(true);
    };
    const onCancel = () => {
      cleanup();
      resolve(false);
    };

    confirmBtn.addEventListener('click', onConfirm);
    cancelBtn.addEventListener('click', onCancel);
    closeBtn?.addEventListener('click', onCancel);
  });
}

function escapeHtml(text) {
  return String(text ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

const THINKING_STATUS_KEYS = [
  ['aiOrganizeThinking', 'AI is organizing bookmarks…'],
  ['aiOrganizeLoadingScanning', 'Scanning bookmarks…'],
  ['aiOrganizeLoadingGrouping', 'Grouping by topic…']
];

function getAiOrganizeProgressElements() {
  return {
    dialog: document.getElementById('ai-organize-progress-dialog'),
    titleEl: document.getElementById('ai-organize-progress-title'),
    statusEl: document.getElementById('ai-organize-progress-status')
  };
}

/**
 * Waiting animation while AI Organize is thinking or applying.
 * Hide during the preview dialog so the user can review the plan.
 */
export function createAiOrganizeProgress() {
  const els = getAiOrganizeProgressElements();
  let tickerId = null;
  let tickerIndex = 0;

  const clearTicker = () => {
    if (tickerId != null) {
      clearInterval(tickerId);
      tickerId = null;
    }
  };

  const setStatus = (text) => {
    if (els.statusEl) els.statusEl.textContent = text;
  };

  const setPhase = (phase) => {
    clearTicker();
    tickerIndex = 0;
    if (phase === 'applying') {
      setStatus(i18n('aiOrganizeApplying', 'Applying organization…'));
      return;
    }
    const [firstKey, firstFallback] = THINKING_STATUS_KEYS[0];
    setStatus(i18n(firstKey, firstFallback));
    tickerId = setInterval(() => {
      tickerIndex = (tickerIndex + 1) % THINKING_STATUS_KEYS.length;
      const [key, fallback] = THINKING_STATUS_KEYS[tickerIndex];
      setStatus(i18n(key, fallback));
    }, 2800);
  };

  const show = (phase = 'thinking') => {
    if (!els.dialog) return;
    if (els.titleEl) {
      els.titleEl.textContent = i18n('aiOrganizeProgressTitle', 'AI Organize');
    }
    els.dialog.style.display = 'block';
    els.dialog.setAttribute('aria-busy', 'true');
    setPhase(phase);
  };

  const hide = () => {
    clearTicker();
    if (!els.dialog) return;
    els.dialog.style.display = 'none';
    els.dialog.setAttribute('aria-busy', 'false');
  };

  return { show, hide, setStatus, setPhase };
}

/**
 * Full flow: propose → preview → apply.
 */
export async function runAiOrganizeFolder(folderId, { onStatus } = {}) {
  const progress = createAiOrganizeProgress();
  const report = (key, fallback) => {
    const message = i18n(key, fallback);
    progress.setStatus(message);
    if (typeof onStatus === 'function') {
      onStatus(message);
    }
  };

  progress.show('thinking');
  try {
    report('aiOrganizeThinking', 'AI is organizing bookmarks…');
    const plan = await proposeAiOrganizePlan(folderId);
    progress.hide();

    const confirmed = await showAiOrganizePreview(plan);
    if (!confirmed) {
      return null;
    }

    progress.show('applying');
    report('aiOrganizeApplying', 'Applying organization…');
    try {
      const snapshot = await captureFolderSnapshot(folderId);
      await saveAiOrganizeSnapshot(snapshot);
    } catch (error) {
      console.warn('Failed to capture AI organize snapshot:', error);
    }
    return await applyAiOrganizePlan(folderId, plan.categories, plan.subfolderIds || []);
  } finally {
    progress.hide();
  }
}
