/**
 * MermaidDiagram - Renders Mermaid diagram definitions as interactive SVG
 * @countermeasure/web-components/visualization/mermaid-diagram
 *
 * Supports all Mermaid diagram types:
 * flowchart, sequenceDiagram, classDiagram, stateDiagram, erDiagram,
 * gantt, pie, journey, gitGraph, mindmap, timeline, sankey,
 * quadrantChart, xychart, block
 */

import type mermaid from 'mermaid'
import type { MermaidDiagramConfig, MermaidTheme } from './types'
import { resolveContainer, uniqueId } from '../../primitives/utils'

export type { MermaidDiagramConfig, MermaidDiagramType, MermaidTheme } from './types'

/** Cached mermaid library promise for lazy loading */
interface MermaidModule {
  default: typeof mermaid
}
let mermaidPromise: Promise<MermaidModule> | null = null

/** Counter for unique diagram IDs */
let diagramCounter = 0

/**
 * Lazy-load the mermaid library
 */
async function loadMermaid(): Promise<MermaidModule> {
  if (mermaidPromise !== null) {
    return mermaidPromise
  }

  mermaidPromise = import('mermaid').then(m => {
    // Initialize mermaid with secure defaults
    m.default.initialize({
      startOnLoad: false,
      securityLevel: 'strict',
      theme: 'base',
      fontFamily: 'inherit',
      logLevel: 'error',
    })
    return m
  })

  return mermaidPromise
}

/**
 * Get theme variables from CounterMeasure design tokens
 */
function getThemeVariables(theme: MermaidTheme): Record<string, string> {
  const isDark =
    theme === 'dark' || (theme === 'auto' && document.documentElement.classList.contains('dark'))

  // Only read CSS variables in 'auto' mode where page theme matches mermaid theme.
  // For explicit 'light'/'dark', use standalone color values to avoid mismatches
  // when the mermaid theme differs from the page theme.
  const useVars = theme === 'auto'

  const root = useVars ? getComputedStyle(document.documentElement) : null

  // Helper to get HSL color from CSS variable (only when in auto mode)
  const getColor = (varName: string, fallback: string): string => {
    if (root !== null) {
      const value = root.getPropertyValue(varName).trim()
      if (value !== '') {
        return `hsl(${value})`
      }
    }
    return fallback
  }

  if (isDark) {
    return {
      primaryColor: getColor('--primary', '#0ea5e9'),
      primaryTextColor: getColor('--primary-foreground', '#ffffff'),
      primaryBorderColor: getColor('--border', '#374151'),
      secondaryColor: getColor('--secondary', '#1f2937'),
      secondaryTextColor: getColor('--secondary-foreground', '#f3f4f6'),
      secondaryBorderColor: getColor('--border', '#374151'),
      tertiaryColor: getColor('--muted', '#374151'),
      tertiaryTextColor: getColor('--muted-foreground', '#9ca3af'),
      tertiaryBorderColor: getColor('--border', '#374151'),
      lineColor: getColor('--border', '#374151'),
      textColor: getColor('--foreground', '#f9fafb'),
      mainBkg: getColor('--background', '#111827'),
      nodeBkg: getColor('--card', '#1f2937'),
      nodeBorder: getColor('--border', '#374151'),
      clusterBkg: getColor('--muted', '#1f2937'),
      clusterBorder: getColor('--border', '#374151'),
      defaultLinkColor: getColor('--primary', '#0ea5e9'),
      titleColor: getColor('--foreground', '#f9fafb'),
      edgeLabelBackground: getColor('--background', '#111827'),
      actorBkg: getColor('--card', '#1f2937'),
      actorBorder: getColor('--border', '#374151'),
      actorTextColor: getColor('--foreground', '#f9fafb'),
      actorLineColor: getColor('--border', '#374151'),
      signalColor: getColor('--foreground', '#f9fafb'),
      signalTextColor: getColor('--foreground', '#f9fafb'),
      labelBoxBkgColor: getColor('--card', '#1f2937'),
      labelBoxBorderColor: getColor('--border', '#374151'),
      labelTextColor: getColor('--foreground', '#f9fafb'),
      loopTextColor: getColor('--foreground', '#f9fafb'),
      noteBorderColor: getColor('--border', '#374151'),
      noteBkgColor: getColor('--muted', '#374151'),
      noteTextColor: getColor('--muted-foreground', '#9ca3af'),
      activationBorderColor: getColor('--primary', '#0ea5e9'),
      activationBkgColor: getColor('--primary', '#0ea5e9'),
      sequenceNumberColor: getColor('--primary-foreground', '#ffffff'),
      sectionBkgColor: getColor('--muted', '#1f2937'),
      altSectionBkgColor: getColor('--card', '#1f2937'),
      sectionBkgColor2: getColor('--muted', '#374151'),
      taskBorderColor: getColor('--primary', '#0ea5e9'),
      taskBkgColor: getColor('--primary', '#0ea5e9'),
      taskTextColor: getColor('--primary-foreground', '#ffffff'),
      taskTextLightColor: getColor('--primary-foreground', '#ffffff'),
      taskTextOutsideColor: getColor('--foreground', '#f9fafb'),
      taskTextClickableColor: getColor('--primary', '#0ea5e9'),
      activeTaskBorderColor: getColor('--primary', '#0ea5e9'),
      activeTaskBkgColor: getColor('--primary', '#0ea5e9'),
      gridColor: getColor('--border', '#374151'),
      doneTaskBkgColor: getColor('--muted', '#374151'),
      doneTaskBorderColor: getColor('--border', '#374151'),
      critBorderColor: getColor('--destructive', '#ef4444'),
      critBkgColor: getColor('--destructive', '#ef4444'),
      todayLineColor: getColor('--primary', '#0ea5e9'),
      personBorder: getColor('--border', '#374151'),
      personBkg: getColor('--card', '#1f2937'),
    }
  }

  // Light theme — primaryTextColor must be dark since mermaid lightens
  // primaryColor for node fills, making white text invisible
  return {
    primaryColor: getColor('--primary', '#0ea5e9'),
    primaryTextColor: getColor('--foreground', '#1e293b'),
    primaryBorderColor: getColor('--border', '#cbd5e1'),
    secondaryColor: getColor('--secondary', '#f1f5f9'),
    secondaryTextColor: getColor('--secondary-foreground', '#1e293b'),
    secondaryBorderColor: getColor('--border', '#cbd5e1'),
    tertiaryColor: getColor('--muted', '#f8fafc'),
    tertiaryTextColor: getColor('--muted-foreground', '#64748b'),
    tertiaryBorderColor: getColor('--border', '#cbd5e1'),
    lineColor: getColor('--border', '#94a3b8'),
    textColor: getColor('--foreground', '#1e293b'),
    mainBkg: getColor('--background', '#ffffff'),
    nodeBkg: getColor('--card', '#f8fafc'),
    nodeBorder: getColor('--border', '#cbd5e1'),
    clusterBkg: getColor('--muted', '#f9fafb'),
    clusterBorder: getColor('--border', '#e5e7eb'),
    defaultLinkColor: getColor('--primary', '#0ea5e9'),
    titleColor: getColor('--foreground', '#111827'),
    edgeLabelBackground: getColor('--background', '#ffffff'),
    actorBkg: getColor('--card', '#ffffff'),
    actorBorder: getColor('--border', '#e5e7eb'),
    actorTextColor: getColor('--foreground', '#111827'),
    actorLineColor: getColor('--border', '#e5e7eb'),
    signalColor: getColor('--foreground', '#111827'),
    signalTextColor: getColor('--foreground', '#111827'),
    labelBoxBkgColor: getColor('--card', '#ffffff'),
    labelBoxBorderColor: getColor('--border', '#e5e7eb'),
    labelTextColor: getColor('--foreground', '#111827'),
    loopTextColor: getColor('--foreground', '#111827'),
    noteBorderColor: getColor('--border', '#e5e7eb'),
    noteBkgColor: getColor('--muted', '#f9fafb'),
    noteTextColor: getColor('--muted-foreground', '#6b7280'),
    activationBorderColor: getColor('--primary', '#0ea5e9'),
    activationBkgColor: getColor('--primary', '#0ea5e9'),
    sequenceNumberColor: getColor('--primary-foreground', '#ffffff'),
    sectionBkgColor: getColor('--muted', '#f9fafb'),
    altSectionBkgColor: getColor('--card', '#ffffff'),
    sectionBkgColor2: getColor('--muted', '#f3f4f6'),
    taskBorderColor: getColor('--primary', '#0ea5e9'),
    taskBkgColor: getColor('--primary', '#0ea5e9'),
    taskTextColor: getColor('--primary-foreground', '#ffffff'),
    taskTextLightColor: getColor('--primary-foreground', '#ffffff'),
    taskTextOutsideColor: getColor('--foreground', '#111827'),
    taskTextClickableColor: getColor('--primary', '#0ea5e9'),
    activeTaskBorderColor: getColor('--primary', '#0ea5e9'),
    activeTaskBkgColor: getColor('--primary', '#0ea5e9'),
    gridColor: getColor('--border', '#e5e7eb'),
    doneTaskBkgColor: getColor('--muted', '#f3f4f6'),
    doneTaskBorderColor: getColor('--border', '#e5e7eb'),
    critBorderColor: getColor('--destructive', '#ef4444'),
    critBkgColor: getColor('--destructive', '#ef4444'),
    todayLineColor: getColor('--primary', '#0ea5e9'),
    personBorder: getColor('--border', '#e5e7eb'),
    personBkg: getColor('--card', '#ffffff'),
  }
}

export class MermaidDiagram {
  public readonly el: HTMLDivElement

  private readonly diagramId: string
  private readonly theme: MermaidTheme
  private readonly onRenderStart: (() => void) | undefined
  private readonly onRenderComplete: ((svg: SVGElement) => void) | undefined
  private readonly onRenderError: ((error: Error) => void) | undefined
  private content: string
  private svgElement: SVGElement | null = null
  private destroyed = false

  constructor(config: MermaidDiagramConfig) {
    diagramCounter += 1
    this.diagramId = uniqueId(`cmm-mermaid-${String(diagramCounter)}`)
    this.content = config.content
    this.theme = config.theme ?? 'auto'
    this.onRenderStart = config.onRenderStart
    this.onRenderComplete = config.onRenderComplete
    this.onRenderError = config.onRenderError

    // Create container element
    this.el = document.createElement('div')
    this.el.id = this.diagramId
    this.el.className = this.buildClassName(config.className)

    // Show loading state
    this.showLoading()

    // Mount to container
    const parent = resolveContainer(config.container)
    parent.appendChild(this.el)

    // Start rendering
    void this.render()
  }

  /**
   * Update the diagram content and re-render
   */
  async update(content: string): Promise<void> {
    if (this.isDestroyed()) return

    this.content = content
    this.showLoading()
    await this.render()
  }

  /**
   * Remove the diagram from DOM and clean up
   */
  destroy(): void {
    this.destroyed = true
    this.svgElement = null
    this.el.remove()
  }

  /**
   * Get the current SVG element (if rendered)
   */
  getSvg(): SVGElement | null {
    return this.svgElement
  }

  /**
   * Export the diagram as SVG string
   */
  toSvgString(): string | null {
    if (this.svgElement === null) return null
    return this.svgElement.outerHTML
  }

  /**
   * Export the diagram as a data URL (for download)
   */
  toDataUrl(): string | null {
    const svgString = this.toSvgString()
    if (svgString === null) return null

    const encoded = encodeURIComponent(svgString)
    return `data:image/svg+xml;charset=utf-8,${encoded}`
  }

  private async render(): Promise<void> {
    if (this.isDestroyed()) return

    this.onRenderStart?.()

    // Hoisted so the catch block can reference it for orphan cleanup.
    const renderId = `${this.diagramId}-svg`

    try {
      const mermaid = await loadMermaid()

      if (this.isDestroyed()) return

      // Configure theme for this render
      const themeVariables = getThemeVariables(this.theme)
      mermaid.default.initialize({
        startOnLoad: false,
        securityLevel: 'strict',
        theme: 'base',
        themeVariables,
        fontFamily: 'inherit',
        logLevel: 'error',
      })

      // Render the diagram
      const { svg } = await mermaid.default.render(renderId, this.content)

      if (this.isDestroyed()) return

      // Clear loading state and insert SVG
      this.el.innerHTML = ''
      this.el.innerHTML = svg

      // Get the SVG element
      const svgEl = this.el.querySelector('svg')
      if (svgEl !== null) {
        this.svgElement = svgEl
        svgEl.classList.add('cmm-mermaid-diagram__svg')

        // Make responsive
        svgEl.removeAttribute('height')
        svgEl.style.maxWidth = '100%'
        svgEl.style.height = 'auto'

        this.onRenderComplete?.(svgEl)
      }
    } catch (error) {
      if (this.isDestroyed()) return

      // Mermaid v11 appends a fallback error <svg> wrapped in a div with id
      // matching `d${renderId}` (and the renderId itself) to document.body
      // when parse/render throws. Remove those orphans so they don't leak
      // into the host page below our component. The wrapper handles error
      // display via showError() / onRenderError below.
      cleanupMermaidOrphans(renderId)

      const err = error instanceof Error ? error : new Error(String(error))
      this.showError(err)
      this.onRenderError?.(err)
    }
  }

  private showLoading(): void {
    this.el.innerHTML = `
      <div class="cmm-mermaid-diagram__loading">
        <div class="cmm-mermaid-diagram__spinner"></div>
        <span class="cmm-mermaid-diagram__loading-text">Rendering diagram...</span>
      </div>
    `
  }

  private showError(error: Error): void {
    const escapedMessage = this.escapeHtml(error.message)
    this.el.innerHTML = `
      <div class="cmm-mermaid-diagram__error">
        <div class="cmm-mermaid-diagram__error-icon">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <circle cx="12" cy="12" r="10"/>
            <line x1="12" y1="8" x2="12" y2="12"/>
            <line x1="12" y1="16" x2="12.01" y2="16"/>
          </svg>
        </div>
        <div class="cmm-mermaid-diagram__error-title">Failed to render diagram</div>
        <div class="cmm-mermaid-diagram__error-message">${escapedMessage}</div>
        <pre class="cmm-mermaid-diagram__error-source">${this.escapeHtml(this.content)}</pre>
      </div>
    `
  }

  private escapeHtml(text: string): string {
    return text
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;')
  }

  private buildClassName(extra: string | undefined): string {
    const classes = ['cmm-mermaid-diagram']
    if (extra !== undefined) {
      classes.push(extra)
    }
    return classes.join(' ')
  }

  private isDestroyed(): boolean {
    return this.destroyed
  }
}

/**
 * Render Mermaid content and return the SVG string (utility function)
 */
export async function renderMermaidToString(
  content: string,
  theme: MermaidTheme = 'auto'
): Promise<string> {
  const mermaid = await loadMermaid()

  const themeVariables = getThemeVariables(theme)
  mermaid.default.initialize({
    startOnLoad: false,
    securityLevel: 'strict',
    theme: 'base',
    themeVariables,
    fontFamily: 'inherit',
    logLevel: 'error',
  })

  diagramCounter += 1
  const renderId = `cmm-mermaid-render-${String(diagramCounter)}`
  try {
    const { svg } = await mermaid.default.render(renderId, content)
    return svg
  } catch (error) {
    cleanupMermaidOrphans(renderId)
    throw error
  }
}

/**
 * Mermaid v11 appends fallback SVG into `document.body` when render/parse
 * throws — typically as `#${renderId}`, `#d${renderId}`, or a stray svg
 * with class `error-icon` etc. Remove orphan nodes the library leaks so
 * the host page stays clean. Safe to call multiple times.
 */
function cleanupMermaidOrphans(renderId: string): void {
  const ids = [renderId, `d${renderId}`, `${renderId}-d`]
  for (const id of ids) {
    const orphan = document.getElementById(id)
    if (orphan && orphan.parentElement === document.body) {
      orphan.remove()
    }
  }
}
