/**
 * Shared utilities for exporting dashboard/report charts via html2canvas.
 *
 * Problem: html2canvas serializes SVG elements to a data URL before drawing
 * them on canvas. Inside that data URL the parent document's CSS is not
 * available, so `fill="var(--primary)"` renders as nothing / black.
 * Additionally, html2canvas's internal CSS parser cannot handle oklch() color
 * values used by Tailwind v4.
 *
 * Solution:
 * 1. Patch oklch() in <style> tags and inject :root overrides so html2canvas's
 *    CSS parser sees rgb() values everywhere.
 * 2. Resolve CSS variable references in SVG presentation attributes
 *    (fill, stroke, stop-color) to their computed rgb() values before capture,
 *    then restore them afterward.
 */

// ── Oklch → RGB conversion ────────────────────────────────────────────────────

export function oklchToRgb(l: number, c: number, h: number): [number, number, number] {
  const hr = (h * Math.PI) / 180;
  const a = c * Math.cos(hr);
  const b = c * Math.sin(hr);
  const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
  const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
  const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
  const lc = l_ ** 3, mc = m_ ** 3, sc = s_ ** 3;
  const gam = (x: number) =>
    x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055;
  const clamp = (x: number) => Math.max(0, Math.min(255, Math.round(x * 255)));
  return [
    clamp(gam(+4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc)),
    clamp(gam(-1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc)),
    clamp(gam(-0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc)),
  ];
}

export const OKLCH_RE = /oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)[^)]*\)/g;

export function oklchMatchToRgb(_: string, l: string, c: string, h: string): string {
  const lv = l.endsWith("%") ? parseFloat(l) / 100 : parseFloat(l);
  const [r, g, b] = oklchToRgb(lv, parseFloat(c), parseFloat(h));
  return `rgb(${r},${g},${b})`;
}

// ── CSS oklch patching ────────────────────────────────────────────────────────

function collectOklchOverrides(
  rules: CSSRuleList,
  overrides: Map<string, string>,
): void {
  for (const rule of Array.from(rules)) {
    if (rule instanceof CSSStyleRule) {
      for (let i = 0; i < rule.style.length; i++) {
        const prop = rule.style[i];
        const val = rule.style.getPropertyValue(prop).trim();
        if (!val.includes("oklch") || overrides.has(prop)) continue;
        const m = val.match(/oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)/);
        if (m) {
          const lv = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
          const [r, g, b] = oklchToRgb(lv, parseFloat(m[2]), parseFloat(m[3]));
          overrides.set(prop, `rgb(${r},${g},${b})`);
        }
      }
    }
    if ("cssRules" in rule && (rule as CSSGroupingRule).cssRules) {
      collectOklchOverrides((rule as CSSGroupingRule).cssRules, overrides);
    }
  }
}

/**
 * Patches oklch() values in inline <style> tags and injects a :root override
 * block for external stylesheets so html2canvas's CSS parser sees only rgb().
 * Returns a `restore()` function that undoes all patches.
 */
export function patchDocumentOklch(): { restore: () => void } {
  const patched = new Map<HTMLStyleElement, string>();
  for (const el of Array.from(document.querySelectorAll("style"))) {
    const s = el as HTMLStyleElement;
    if (s.textContent?.includes("oklch")) {
      patched.set(s, s.textContent);
      s.textContent = s.textContent.replace(OKLCH_RE, oklchMatchToRgb);
    }
  }

  const overrideVars = new Map<string, string>();
  for (const sheet of Array.from(document.styleSheets)) {
    try { collectOklchOverrides(sheet.cssRules, overrideVars); } catch {}
  }

  let varOverride: HTMLStyleElement | null = null;
  if (overrideVars.size > 0) {
    varOverride = document.createElement("style");
    varOverride.textContent = `:root { ${
      Array.from(overrideVars.entries())
        .map(([p, v]) => `${p}: ${v}`)
        .join("; ")
    } }`;
    document.head.appendChild(varOverride);
  }

  return {
    restore() {
      for (const [s, orig] of patched.entries()) s.textContent = orig;
      varOverride?.remove();
    },
  };
}

// ── SVG presentation attribute inlining ──────────────────────────────────────

const SVG_PRES_ATTRS = ["fill", "stroke", "stop-color"] as const;

/**
 * Walks every SVG descendant of `root` and replaces any presentation attribute
 * that contains a CSS variable (`var(…)`) with its computed rgb() value.
 *
 * This must be called AFTER patchDocumentOklch() so that getComputedStyle
 * already returns rgb values rather than oklch.
 *
 * Returns a `restore()` function that reverts all attribute changes.
 * For off-screen clones that are discarded after capture, calling restore()
 * is optional.
 */
export function inlineSvgPresentationAttrs(
  root: Element,
): { restore: () => void } {
  const patches: Array<{ el: SVGElement; attr: string; orig: string }> = [];

  for (const node of Array.from(root.querySelectorAll("*"))) {
    if (!(node instanceof SVGElement)) continue;
    const cs = window.getComputedStyle(node);
    for (const attr of SVG_PRES_ATTRS) {
      const val = node.getAttribute(attr);
      if (!val || !val.includes("var(")) continue;
      const resolved = cs.getPropertyValue(attr).trim().replace(OKLCH_RE, oklchMatchToRgb);
      if (!resolved || resolved === val) continue;
      patches.push({ el: node, attr, orig: val });
      node.setAttribute(attr, resolved);
    }
  }

  return {
    restore() {
      for (const { el, attr, orig } of patches) el.setAttribute(attr, orig);
    },
  };
}

/**
 * Convenience wrapper that runs the full pre-capture patch sequence on a DOM
 * element:
 *   1. patchDocumentOklch  – fixes oklch in <style>/<link> CSS
 *   2. inlineSvgPresentationAttrs – resolves var() in SVG fill/stroke attrs
 *
 * Returns a single `restore()` that undoes both steps in reverse order.
 *
 * Usage:
 *   const patch = prepareForHtml2Canvas(element);
 *   try { await html2canvas(element, …); } finally { patch.restore(); }
 */
export function prepareForHtml2Canvas(root: Element): { restore: () => void } {
  const oklch = patchDocumentOklch();
  const svg = inlineSvgPresentationAttrs(root);
  return {
    restore() {
      svg.restore();
      oklch.restore();
    },
  };
}
