import { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
import {
  Hooks,
  type Token,
  type TokenizerExtensionFunction,
  type TokenizerStartFunction,
} from 'marked';
import {
  isExternalUrlSchemeAllowed,
  isPathSafeToOpen,
} from '../../../electron/shared-with-frontend/is-external-url-allowed';
import { escapeHtml } from '../util/escape-html';

/**
 * Escape a string for safe interpolation into a double-quoted HTML attribute.
 *
 * Defense-in-depth: note renders are sanitized as HTML before display, but the
 * raw renderer output still must not be attribute-injectable on its own.
 * Delegates to the shared {@link escapeHtml} (same escaping is correct for both
 * attribute and text contexts); kept as a named re-export for attribute-context
 * call sites.
 */
export const escapeHtmlAttr = escapeHtml;

/**
 * Parses image sizing syntax from title attribute.
 * The title format is expected to be "width|height" (e.g., "200|150", "200|", "|150")
 * This is generated by preprocessing.
 */
export const parseImageDimensionsFromTitle = (
  title: string | null,
): { width?: string; height?: string } => {
  if (!title) {
    return {};
  }

  // Check if title is in our custom format: "width|height"
  const match = title.match(/^(\d*)?\|(\d*)?$/);
  if (!match) {
    return {};
  }

  return {
    width: match[1] && match[1].length > 0 ? match[1] : undefined,
    height: match[2] && match[2].length > 0 ? match[2] : undefined,
  };
};

/**
 * Preprocess markdown to convert image sizing syntax into title attributes.
 * Converts: ![alt](url =WIDTHxHEIGHT) -> ![alt](url "WIDTH|HEIGHT")
 * This allows marked.js to parse it as a valid image with a title.
 */
export const preprocessMarkdown = (markdown: string): string => {
  // Match: ![alt](url =WIDTHxHEIGHT) or ![alt](url =WIDTHx) or ![alt](url =xHEIGHT)
  // Capture groups: 1=alt, 2=url, 3=width, 4=height
  return markdown.replace(
    /!\[([^\]]*)\]\(([^\s)]+)\s+=(\d*)x(\d*)\)/g,
    (match, alt, url, width, height) => {
      // Create title attribute with width|height format
      const dimensions = `${width || ''}|${height || ''}`;
      return `![${alt}](${url} "${dimensions}")`;
    },
  );
};

const WEBEX_TEAMS_URI_PREFIX = 'webexteams://';
const WEBEX_TEAMS_URI_RE = /^webexteams:\/\/\S{1,2000}/i;
const WEBEX_TRAILING_PUNCT_RE = /[.,;!?]+$/;

const stripWebexTeamsUriTrailing = (raw: string): string => {
  const uri = raw.replace(WEBEX_TRAILING_PUNCT_RE, '');
  let opens = 0;
  let closes = 0;
  for (let i = 0; i < uri.length; i++) {
    const c = uri.charCodeAt(i);
    if (c === 40) opens++;
    else if (c === 41) closes++;
  }
  let end = uri.length;
  while (end > 0 && uri.charCodeAt(end - 1) === 41 && closes > opens) {
    end--;
    closes--;
  }
  return end < uri.length ? uri.substring(0, end) : uri;
};

const startWebexTeamsAutoLink: TokenizerStartFunction = (src: string): number | void => {
  const index = src.toLowerCase().indexOf(WEBEX_TEAMS_URI_PREFIX);
  return index >= 0 ? index : undefined;
};

const tokenizeWebexTeamsAutoLink: TokenizerExtensionFunction = (src: string) => {
  const match = WEBEX_TEAMS_URI_RE.exec(src);
  if (!match) {
    return undefined;
  }

  const href = stripWebexTeamsUriTrailing(match[0]);
  return {
    type: 'link',
    raw: href,
    href,
    text: href,
    tokens: [{ type: 'text', raw: href, text: href }],
  };
};

export const markedOptionsFactory = (): MarkedOptions => {
  const renderer = new MarkedRenderer();

  // Override checkbox to return empty string — the listitem renderer below handles
  // all checkbox rendering with Material Icons. Without this, marked's default checkbox
  // renderer produces an <input type="checkbox"> that duplicates our custom checkbox.
  renderer.checkbox = () => '';

  // In marked v17, list items can contain block-level tokens (e.g., paragraph) when the list
  // is "loose" (has blank lines between items). Use parse() instead of parseInline() to handle
  // both block and inline tokens correctly. See GitHub issue #6244.
  // Using a regular function to access 'this'
  renderer.listitem = function ({
    text,
    task,
    checked,
    tokens,
  }: {
    text: string;
    task: boolean;
    checked?: boolean;
    tokens: Token[];
  }) {
    // In marked v17, task list items need to manually prepend the checkbox
    // Use parse() to handle both block tokens (paragraph in loose lists) and inline tokens
    let renderedText = tokens ? this.parser.parse(tokens) : text;

    if (task) {
      // For task items, strip outer <p></p> wrapper to keep checkbox and text on same line.
      // Loose lists wrap content in <p> tags which pushes text to new line after checkbox.
      // Only strip if it's a simple wrapper (no nested block content).
      const strippedText = renderedText.replace(/^<p>([\s\S]*)<\/p>\n?$/, '$1');
      if (strippedText !== renderedText && !strippedText.includes('<p>')) {
        renderedText = strippedText;
      }

      const isChecked = checked === true;
      const checkboxHtml = `<span class="checkbox material-icons">${isChecked ? 'check_box' : 'check_box_outline_blank'}</span>`;
      // Wrap the text in its own element so only the checkbox and the label —
      // not the empty rest of the row — are clickable (see clickPreview).
      return `<li class="checkbox-wrapper ${isChecked ? 'done' : 'undone'}">${checkboxHtml} <span class="checkbox-label">${renderedText}</span></li>`;
    }
    return `<li>${renderedText}</li>`;
  };

  // In marked v17, link renderer receives tokens that need to be parsed for inline content
  renderer.link = function ({
    href,
    title,
    tokens,
  }: {
    href: string;
    title?: string | null;
    tokens: Token[];
  }) {
    const text = tokens ? this.parser.parseInline(tokens) : '';
    // Block unsafe URL schemes from rendering as clickable links. On click the
    // href is passed verbatim to shell.openExternal (Electron), which would let
    // note content silently invoke OS protocol handlers. See GHSA-hr87-735w-hfq3.
    if (!isExternalUrlSchemeAllowed(href)) {
      return `<span class="markdown-blocked-link" title="Link blocked: unsafe URL scheme">${text}</span>`;
    }
    return `<a target="_blank" rel="noopener noreferrer" href="${escapeHtmlAttr(href)}" title="${escapeHtmlAttr(title || '')}">${text}</a>`;
  };

  // Custom image renderer with support for sizing syntax
  // Note: indexeddb:// URLs are pre-resolved to blob: URLs before markdown rendering
  // The sizing dimensions are passed via the title attribute in "width|height" format
  renderer.image = ({
    href,
    title,
    text,
  }: {
    href: string;
    title: string | null;
    text: string;
  }) => {
    // Unlike links, an image src auto-loads on render (no click). A remote
    // `file://host/share` or UNC src would silently make the OS open an SMB
    // connection and leak the user's NTLM hash just by viewing the note, so
    // such srcs must never reach the `src` attribute. Remote web images
    // (http/https/data/blob) are unaffected. See GHSA-hr87-735w-hfq3.
    if (!isPathSafeToOpen(href)) {
      return `<span class="markdown-blocked-link" title="Image blocked: unsafe URL">${escapeHtmlAttr(
        text || '',
      )}</span>`;
    }

    const { width, height } = parseImageDimensionsFromTitle(title);

    // Build width and height attributes (not style, as Angular sanitizer strips inline styles)
    const widthAttr = width ? ` width="${width}"` : '';
    const heightAttr = height ? ` height="${height}"` : '';

    // Only include title if it's not our custom dimension format.
    // width/height are digit-only (parsed via regex above), so they need no escaping;
    // href/title/alt are attacker-controllable and must be escaped.
    const isCustomDimensionTitle = title && /^(\d*)?\|(\d*)?$/.test(title);
    const titleAttr =
      title && !isCustomDimensionTitle ? ` title="${escapeHtmlAttr(title)}"` : '';
    const srcAttr = ` src="${escapeHtmlAttr(href)}"`;

    return `<img alt="${escapeHtmlAttr(text)}"${srcAttr}${titleAttr}${widthAttr}${heightAttr} loading="lazy">`;
  };

  // In marked v17, paragraph renderer receives tokens that need to be parsed
  renderer.paragraph = function ({ tokens }: { tokens: Token[] }) {
    const text = tokens ? this.parser.parseInline(tokens) : '';
    const split = text.split('\n');
    return split.reduce((acc, p, i) => {
      const result = /h(\d)\./.exec(p);
      if (result !== null) {
        const h = `h${result[1]}`;
        return acc + `<${h}>${p.replace(result[0], '')}</${h}>`;
      }

      if (split.length === 1) {
        return `<p>` + p + `</p>`;
      }

      return acc ? (split.length - 1 === i ? acc + p + `</p>` : acc + p) : `<p>` + p;
    }, '');
  };

  // NOTE: We intentionally do NOT override renderer.text for URL auto-linking.
  // In marked v17 with gfm: true, URLs are automatically detected and converted to links
  // by the lexer before they reach the text renderer. Custom URL linkification here
  // would cause double-processing and broken HTML (links inside links).

  const options: MarkedOptions = {
    renderer,
    gfm: true,
    breaks: true,
    pedantic: false,
    extensions: {
      renderers: {},
      childTokens: {},
      inline: [tokenizeWebexTeamsAutoLink],
      startInline: [startWebexTeamsAutoLink],
    },
  };

  // Add preprocessing hook to handle image sizing syntax
  const hooks = new Hooks();
  hooks.preprocess = preprocessMarkdown;
  options.hooks = hooks;

  return options;
};
