/**
 * Chrome-compatible Netscape Bookmark File Format (HTML) import/export.
 * Format matches chrome://bookmarks → Export / Import.
 */

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

function escapeAttr(text) {
  return escapeHtml(text).replace(/'/g, '&#39;');
}

function toUnixSeconds(ms) {
  if (ms == null || Number.isNaN(Number(ms))) {
    return Math.floor(Date.now() / 1000);
  }
  const value = Number(ms);
  // Chrome bookmark dates are ms; Netscape ADD_DATE is seconds
  return value > 1e12 ? Math.floor(value / 1000) : Math.floor(value);
}

function sanitizeFilename(name) {
  const cleaned = String(name || 'bookmarks')
    .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, 80);
  return cleaned || 'bookmarks';
}

function serializeNode(node, indent) {
  const pad = '    '.repeat(indent);
  const lines = [];

  if (node.url) {
    const addDate = toUnixSeconds(node.dateAdded);
    lines.push(
      `${pad}<DT><A HREF="${escapeAttr(node.url)}" ADD_DATE="${addDate}">${escapeHtml(node.title || node.url)}</A>`
    );
    return lines;
  }

  const addDate = toUnixSeconds(node.dateAdded);
  const lastModified = toUnixSeconds(node.dateGroupModified || node.dateAdded);
  lines.push(
    `${pad}<DT><H3 ADD_DATE="${addDate}" LAST_MODIFIED="${lastModified}">${escapeHtml(node.title || 'Folder')}</H3>`
  );
  lines.push(`${pad}<DL><p>`);
  for (const child of node.children || []) {
    lines.push(...serializeNode(child, indent + 1));
  }
  lines.push(`${pad}</DL><p>`);
  return lines;
}

/**
 * Build a Netscape Bookmark HTML document from a Chrome BookmarkTreeNode (folder).
 */
export function serializeFolderToHtml(folderNode) {
  const body = serializeNode(folderNode, 1).join('\n');
  return [
    '<!DOCTYPE NETSCAPE-Bookmark-file-1>',
    '<!-- This is an automatically generated file.',
    '     It will be read and written by Internet Explorer and Netscape.',
    '     DO NOT EDIT! -->',
    '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">',
    '<TITLE>Bookmarks</TITLE>',
    '<H1>Bookmarks</H1>',
    '<DL><p>',
    body,
    '</DL><p>',
    ''
  ].join('\n');
}

function decodeHtmlEntities(text) {
  const textarea = document.createElement('textarea');
  textarea.innerHTML = text;
  return textarea.value;
}

/**
 * Parse Netscape / Chrome bookmark HTML into a tree of { title, url?, children? }.
 */
export function parseBookmarkHtml(html) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');

  function parseDl(dl) {
    const items = [];
    if (!dl) return items;

    let child = dl.firstElementChild;
    while (child) {
      if (child.tagName === 'DT') {
        const h3 = child.querySelector(':scope > H3');
        const a = child.querySelector(':scope > A');

        if (h3) {
          let next = child.nextElementSibling;
          // Some exporters put <DL> inside <DT>; Chrome puts it after <DT>
          let nestedDl = child.querySelector(':scope > DL');
          if (!nestedDl && next?.tagName === 'DL') {
            nestedDl = next;
            child = next;
          }
          items.push({
            title: decodeHtmlEntities(h3.textContent || '').trim() || 'Folder',
            children: parseDl(nestedDl)
          });
        } else if (a) {
          const href = a.getAttribute('href') || '';
          if (href && !/^javascript:/i.test(href)) {
            items.push({
              title: decodeHtmlEntities(a.textContent || '').trim() || href,
              url: href
            });
          }
        }
      } else if (child.tagName === 'DL') {
        items.push(...parseDl(child));
      }
      child = child.nextElementSibling;
    }
    return items;
  }

  // Prefer the first top-level DL under body (Chrome export structure)
  const rootDl = doc.body?.querySelector(':scope > DL') || doc.querySelector('DL');
  return parseDl(rootDl);
}

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);
    });
  });
}

/**
 * Recursively create parsed bookmark nodes under parentId.
 * @returns {{ folders: number, bookmarks: number }}
 */
export async function importNodesIntoFolder(parentId, nodes) {
  const stats = { folders: 0, bookmarks: 0 };

  async function importList(targetParentId, list) {
    for (const node of list) {
      if (node.url) {
        await createBookmarkNode({
          parentId: targetParentId,
          title: node.title || node.url,
          url: node.url
        });
        stats.bookmarks += 1;
      } else {
        const folder = await createBookmarkNode({
          parentId: targetParentId,
          title: node.title || 'Folder'
        });
        stats.folders += 1;
        if (node.children?.length) {
          await importList(folder.id, node.children);
        }
      }
    }
  }

  await importList(parentId, nodes);
  return stats;
}

/**
 * Export a Chrome bookmark folder subtree as a downloadable HTML file.
 */
export async function exportFolderAsHtml(folderId, folderTitle) {
  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 folderNode = nodes?.[0];
  if (!folderNode) {
    throw new Error('Folder not found');
  }

  const html = serializeFolderToHtml(folderNode);
  const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  const name = sanitizeFilename(folderTitle || folderNode.title);
  anchor.href = url;
  anchor.download = `${name}.html`;
  document.body.appendChild(anchor);
  anchor.click();
  anchor.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

/**
 * Open a file picker and import a Chrome/Netscape bookmark HTML into parentId.
 */
export function pickAndImportBookmarkHtml(parentId) {
  return new Promise((resolve, reject) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.html,.htm,text/html';
    input.style.display = 'none';
    document.body.appendChild(input);

    let settled = false;

    const cleanup = () => {
      input.remove();
    };

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

    input.addEventListener('change', async () => {
      const file = input.files?.[0];
      if (!file) {
        finish(resolve, null);
        return;
      }
      try {
        const text = await file.text();
        const nodes = parseBookmarkHtml(text);
        if (!nodes.length) {
          finish(reject, new Error('EMPTY_BOOKMARK_FILE'));
          return;
        }
        const stats = await importNodesIntoFolder(parentId, nodes);
        finish(resolve, stats);
      } catch (error) {
        finish(reject, error);
      }
    }, { once: true });

    // Cancel: dialog closed without a file. focus returns to the page.
    window.addEventListener('focus', () => {
      setTimeout(() => {
        if (!settled && document.body.contains(input) && !input.files?.length) {
          finish(resolve, null);
        }
      }, 400);
    }, { once: true });

    input.click();
  });
}
