import { useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot, $isTextNode, $isElementNode, TextNode } from 'lexical';
import { getSettings, HighlighterRule } from '../lib/db';

const CONJUNCTIONS = ['and', 'or', 'but', 'yet', 'so', 'in addition', 'therefore'];

function setNodeBackgroundColor(node: TextNode, bg: string | undefined): boolean {
  let style = node.getStyle() || '';
  style = style.replace(/background(?:-color)?:\s*[^;]+;?/g, '').trim();
  if (bg) {
    const prop = bg.includes('gradient') ? 'background' : 'background-color';
    style = style + (style && !style.endsWith(';') ? ';' : '') + `${prop}: ${bg};`;
  }
  if (node.getStyle() !== style) {
    node.setStyle(style);
    return true;
  }
  return false;
}

function calculateBgColor(rule: HighlighterRule, depth: number): string | undefined {
    if (depth <= 0) return undefined;
    const hex = rule.color.replace('#', '');
    const r = parseInt(hex.substring(0, 2), 16) || 255;
    const g = parseInt(hex.substring(2, 4), 16) || 255;
    const b = parseInt(hex.substring(4, 6), 16) || 255;
    
    let opacity = rule.baseOpacity || 0.6;
    if (rule.stackMode === 'larger-lighter') {
        // larger means outer. depth=1 is outer. Outer is lighter. So as depth increases, opacity increases (darker).
        opacity = Math.min(0.95, opacity + ((depth - 1) * 0.15));
    } else {
        // smaller means inner. Outer is darker. As depth increases, opacity decreases (lighter).
        opacity = Math.max(0.05, opacity - ((depth - 1) * 0.15));
    }
    return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}

export default function AutoHighlighterPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    let rules: HighlighterRule[] = [];
    getSettings().then(s => { rules = s.highlighters || []; });

    const removeUpdateListener = editor.registerUpdateListener(({ dirtyElements, dirtyLeaves }) => {
      if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;

      editor.update(() => {
        const root = $getRoot();
        const firstChild = root.getFirstChild();
        if (!firstChild) return;

        const blocks = root.getChildren();
        
        for (const block of blocks) {
          if (!$isElementNode(block)) continue;
          
          const text = block.getTextContent();
          if (!text) {
             // ensure no leftover background styles in empty blocks
             block.getChildren().forEach(c => {
                 if ($isTextNode(c)) setNodeBackgroundColor(c, undefined);
             });
             continue;
          }

          const sortedRules = [...rules].sort((a, b) => b.openSymbol.length - a.openSymbol.length);
          const tokens: any[] = [];

          // 1. Lexer
          for (let i = 0; i < text.length; i++) {
             let matchedConj = false;
             for (const c of CONJUNCTIONS) {
                 if (text.startsWith(c, i)) {
                     const before = i === 0 || !/[a-zA-Z]/.test(text[i-1]);
                     const after = i + c.length >= text.length || !/[a-zA-Z]/.test(text[i+c.length]);
                     if (before && after) {
                         tokens.push({ index: i, length: c.length, type: 'conj' });
                         i += c.length - 1;
                         matchedConj = true;
                         break;
                     }
                 }
             }
             if (matchedConj) continue;

             for (const rule of sortedRules) {
                 const isOpenSame = rule.openSymbol === rule.closeSymbol;
                 if (text.startsWith(rule.closeSymbol, i)) { // Always check close first
                     tokens.push({ index: i, length: rule.closeSymbol.length, type: isOpenSame ? 'same' : 'close', rule });
                     i += rule.closeSymbol.length - 1;
                     break;
                 } else if (text.startsWith(rule.openSymbol, i)) {
                     tokens.push({ index: i, length: rule.openSymbol.length, type: 'open', rule });
                     i += rule.openSymbol.length - 1;
                     break;
                 }
             }
          }

          // 2. Parser
          const pairs: { start: number, end: number, rule: HighlighterRule }[] = [];
          const stack: { token: any, rule: HighlighterRule }[] = [];

          for (const tk of tokens) {
              if (tk.type === 'conj') continue; // handled later

              if (tk.type === 'open') {
                  stack.push({ token: tk, rule: tk.rule });
              } else if (tk.type === 'close') {
                  let foundIdx = -1;
                  for (let j = stack.length - 1; j >= 0; j--) {
                      if (stack[j].rule.id === tk.rule.id && stack[j].token.type === 'open') {
                          foundIdx = j;
                          break;
                      }
                  }
                  if (foundIdx !== -1) {
                      const openTk = stack.splice(foundIdx, 1)[0];
                      pairs.push({ start: openTk.token.index, end: tk.index + tk.length, rule: openTk.rule });
                  }
              } else if (tk.type === 'same') {
                  let foundIdx = -1;
                  for (let j = stack.length - 1; j >= 0; j--) {
                      if (stack[j].rule.id === tk.rule.id && stack[j].token.type === 'same') {
                          foundIdx = j;
                          break;
                      }
                  }
                  if (foundIdx !== -1) {
                      const openTk = stack.splice(foundIdx, 1)[0];
                      pairs.push({ start: openTk.token.index, end: tk.index + tk.length, rule: openTk.rule });
                  } else {
                      stack.push({ token: tk, rule: tk.rule });
                  }
              }
          }

          const charColors = new Array(text.length).fill(undefined);

          for (let i = 0; i < text.length; i++) {
              let activePairs = [];
              for (const p of pairs) {
                  if (i >= p.start && i < p.end) {
                      activePairs.push(p);
                  }
              }
              if (activePairs.length > 0) {
                  // Outermost first (longest span)
                  activePairs.sort((a, b) => (b.end - b.start) - (a.end - a.start));
                  
                  if (activePairs.length === 1) {
                      charColors[i] = calculateBgColor(activePairs[0].rule, 1);
                  } else {
                      const gradients: string[] = [];
                      for (let d = activePairs.length - 1; d >= 0; d--) {
                          const base = calculateBgColor(activePairs[d].rule, 1);
                          gradients.push(`linear-gradient(${base}, ${base})`);
                      }
                      charColors[i] = gradients.join(', ');
                  }
              }
          }

          // Conjunction override
          for (let i = 0; i < text.length; i++) {
             for (const c of CONJUNCTIONS) {
                 if (text.startsWith(c, i)) {
                     const before = i === 0 || !/[a-zA-Z]/.test(text[i-1]);
                     const after = i + c.length >= text.length || !/[a-zA-Z]/.test(text[i+c.length]);
                     if (before && after) {
                         const color = 'rgba(234, 179, 8, 0.4)';
                         for(let j=0; j<c.length; j++) charColors[i+j] = color;
                         i += c.length - 1;
                         break;
                     }
                 }
             }
          }

          // 3. Modulator
          // Cleanly apply regions to Lexical DOM
          let currentOffset = 0;
          let child = block.getFirstChild();

          while (child !== null) {
              if (!$isTextNode(child)) {
                  currentOffset += child.getTextContent().length;
                  child = child.getNextSibling();
                  continue;
              }

              const textContent = child.getTextContent();
              const len = textContent.length;
              if (len === 0) {
                 child = child.getNextSibling();
                 continue;
              }

              let targetBg = charColors[currentOffset];
              let splitNeededOffset = -1;
              
              for (let c = 1; c < len; c++) {
                  if (charColors[currentOffset + c] !== targetBg) {
                      splitNeededOffset = c;
                      break;
                  }
              }

              let style = child.getStyle() || '';
              let currentBgMatch = style.match(/background(?:-color)?:\s*([^;]+)/);
              let currentBg = currentBgMatch ? currentBgMatch[1].trim() : undefined;
              let normalizedCurrent = currentBg ? currentBg.replace(/\s+/g, '') : undefined;
              let normalizedTarget = targetBg ? targetBg.replace(/\s+/g, '') : undefined;
              
              if (splitNeededOffset !== -1) {
                  const [, rightNode] = child.splitText(splitNeededOffset);
                  if (normalizedCurrent !== normalizedTarget) {
                      setNodeBackgroundColor(child, targetBg);
                  }
                  currentOffset += splitNeededOffset;
                  child = rightNode;
              } else {
                  if (normalizedCurrent !== normalizedTarget) {
                      setNodeBackgroundColor(child, targetBg);
                  }
                  currentOffset += len;
                  
                  // Optimisation: Merge adjacent nodes with the same formats & styles to prevent Lexical fragmentation
                  let next = child.getNextSibling();
                  if (next && $isTextNode(next) && next.getStyle() === child.getStyle() && next.getFormat() === child.getFormat()) {
                      child.setTextContent(child.getTextContent() + next.getTextContent());
                      next.remove();
                      // Don't advance offset, re-process this larger node on next pass if needed, or 
                      // actually we know targetBg covers `len`, but what about the rest? 
                      // It's safer to just let the overall update pass handle it or let Lexical normalize.
                      // Actually, Lexical internal text node normalization runs automatically if not prevented!
                  }
                  child = child.getNextSibling();
              }
          }
        }
      });
    });

    return removeUpdateListener;
  }, [editor]);

  return null;
}
