import { defaultKeymap, indentWithTab } from "@codemirror/commands"
import { css } from "@codemirror/lang-css"
import { html } from "@codemirror/lang-html"
import { javascript } from "@codemirror/lang-javascript"
import { json } from "@codemirror/lang-json"
import { markdown as cmMarkdown } from "@codemirror/lang-markdown"
import { python } from "@codemirror/lang-python"
import { xml } from "@codemirror/lang-xml"
import { syntaxHighlighting } from "@codemirror/language"
import { Compartment, EditorState } from "@codemirror/state"
import { EditorView as CodeMirrorView, keymap } from "@codemirror/view"
import { classHighlighter } from "@lezer/highlight"
import type { Node as ProsemirrorNode } from "prosemirror-model"
import type { EditorView, NodeView } from "prosemirror-view"
import { createLogger } from "@/lib/logger"

/**
 * Delegate through which CodeBlockView signals navigation intent.
 *
 * The node view never mutates document structure directly — it emits
 * intent and lets the owning layer (Block / Editor) decide what to do.
 *
 * Methods receive `nodePos` — the ProseMirror position of the code_block
 * node at call time — so the delegate can resolve it to find neighbour
 * nodes without depending on PM's node view lifecycle.
 */
export interface CodeBlockNavigationDelegate {
  /** User pressed ArrowUp at first line or ArrowDown at last line. */
  requestNavigateOut(direction: "up" | "down", nodePos: number): void

  /** User pressed Shift+Enter on first or last line of a code block. */
  requestInsertParagraphAround(
    direction: "before" | "after",
    nodePos: number,
  ): void

  /** User pressed Backspace on an empty code block. */
  requestRemoveBlock(nodePos: number): void

  /** User selected a new language from the dropdown. */
  requestChangeLanguage(
    newLanguage: string,
    body: string,
    nodePos: number,
  ): void
}

function instanceLogger(componentFilter?: string) {
  return createLogger("CodeBlockView", componentFilter)
}

/**
 * Strip fence markers from a code block's text content.
 *
 * Input:  "```typescript\nconst x = 1\n```"
 * Output: "const x = 1"
 *
 * Handles edge cases:
 * - Empty block: "```\n```" (2 lines) → ""
 * - Block with language but no body: "```js\n```" (2 lines) → ""
 * - Single-line body: "```js\nconst x = 1\n```" (3 lines) → "const x = 1"
 */
export function stripFences(content: string): string {
  const lines = content.split("\n")
  return lines.slice(1, -1).join("\n")
}

/**
 * Re-add fence markers around code body for PM storage.
 * Normalizes trailing newlines to prevent blank-line drift
 * before the closing fence.
 */
export function addFences(content: string, language: string): string {
  const body = content.replace(/\n$/, "")
  return `\`\`\`${language}\n${body}\n\`\`\``
}

/**
 * Map language aliases to canonical short forms used in the dropdown.
 *
 * "typescript" → "ts", "javascript" → "js", "py" → "python", etc.
 * Unrecognized languages return "" to show "Plain" in the select.
 */
function normalizeLanguage(lang: string): string {
  switch (lang.toLowerCase()) {
    case "js":
    case "javascript":
    case "jsx":
    case "mjs":
    case "cjs":
      return "js"
    case "ts":
    case "typescript":
    case "tsx":
    case "mts":
    case "cts":
      return "ts"
    case "python":
    case "py":
      return "python"
    case "html":
    case "htm":
      return "html"
    case "css":
      return "css"
    case "json":
      return "json"
    case "xml":
    case "svg":
      return "xml"
    case "md":
    case "markdown":
      return "md"
    default:
      return ""
  }
}

function resolveLanguage(lang: string) {
  switch (lang.toLowerCase()) {
    case "js":
    case "javascript":
    case "jsx":
    case "mjs":
    case "cjs":
      return javascript()
    case "ts":
    case "typescript":
    case "tsx":
    case "mts":
    case "cts":
      return javascript({ typescript: true })
    case "python":
    case "py":
      return python()
    case "html":
    case "htm":
      return html()
    case "css":
      return css()
    case "json":
      return json()
    case "xml":
    case "svg":
      return xml()
    case "md":
    case "markdown":
      return cmMarkdown()
    default:
      return []
  }
}

const LANG_OPTIONS = [
  { value: "", label: "Plain" },
  { value: "js", label: "JavaScript" },
  { value: "ts", label: "TypeScript" },
  { value: "python", label: "Python" },
  { value: "html", label: "HTML" },
  { value: "css", label: "CSS" },
  { value: "json", label: "JSON" },
  { value: "xml", label: "XML" },
  { value: "svg", label: "SVG" },
  { value: "md", label: "Markdown" },
]

export class CodeBlockView implements NodeView {
  dom: HTMLElement
  cmView: CodeMirrorView
  private pmView: EditorView
  private getPos: () => number
  private delegate: CodeBlockNavigationDelegate
  private languageCompartment = new Compartment()
  private lastLanguage = ""
  private langSelect: HTMLSelectElement
  private log: ReturnType<typeof createLogger>

  constructor(
    node: ProsemirrorNode,
    view: EditorView,
    getPos: () => number,
    delegate: CodeBlockNavigationDelegate,
    componentFilter?: string,
  ) {
    this.pmView = view
    this.getPos = getPos
    this.delegate = delegate
    this.log = instanceLogger(componentFilter)

    this.dom = document.createElement("div")
    this.dom.className = "cm-code-block"
    this.dom.setAttribute("data-codeblock-editor", "")

    const header = document.createElement("div")
    header.className = "cm-code-block-header"

    this.langSelect = document.createElement("select")
    this.langSelect.className = "cm-code-block-lang-select"
    this.langSelect.addEventListener("mousedown", (e) => e.stopPropagation())

    const language = (node.attrs.language as string) || ""
    for (const opt of LANG_OPTIONS) {
      const option = document.createElement("option")
      option.value = opt.value
      option.textContent = opt.label
      this.langSelect.appendChild(option)
    }
    this.langSelect.value = normalizeLanguage(language)

    this.langSelect.addEventListener("change", () => {
      const body = this.cmView.state.doc.toString()
      this.delegate.requestChangeLanguage(
        this.langSelect.value,
        body,
        this.getPos(),
      )
    })

    const deleteBtn = document.createElement("button")
    deleteBtn.className = "cm-code-block-delete-btn"
    deleteBtn.textContent = "×"
    deleteBtn.title = "Delete code block"
    deleteBtn.addEventListener("mousedown", (e) => e.stopPropagation())
    deleteBtn.addEventListener("click", () => {
      this.delegate.requestRemoveBlock(this.getPos())
    })

    header.appendChild(this.langSelect)
    header.appendChild(deleteBtn)
    this.dom.appendChild(header)

    this.lastLanguage = language
    const langExt = resolveLanguage(language)
    this.log.info("constructor", {
      language,
      langExt: langExt?.constructor?.name ?? typeof langExt,
    })

    const exitKeymap = keymap.of([
      {
        key: "ArrowUp",
        run: (cm6View) => {
          const sel = cm6View.state.selection.main
          if (sel.from === 0 && sel.empty) {
            this.delegate.requestNavigateOut("up", this.getPos())
            return true
          }
          return false
        },
      },
      {
        key: "ArrowDown",
        run: (cm6View) => {
          const sel = cm6View.state.selection.main
          if (sel.to === cm6View.state.doc.length && sel.empty) {
            this.delegate.requestNavigateOut("down", this.getPos())
            return true
          }
          return false
        },
      },
      {
        key: "Shift-Enter",
        run: (cm6View) => {
          const sel = cm6View.state.selection.main
          if (sel.from === 0 && sel.empty) {
            this.delegate.requestInsertParagraphAround("before", this.getPos())
            return true
          }
          const docLen = cm6View.state.doc.length
          if (sel.from === docLen && sel.empty) {
            this.delegate.requestInsertParagraphAround("after", this.getPos())
            return true
          }
          return false
        },
      },
      {
        key: "Backspace",
        run: (cm6View) => {
          if (cm6View.state.doc.length === 0) {
            this.delegate.requestRemoveBlock(this.getPos())
            return true
          }
          return false
        },
      },
    ])

    const cmState = EditorState.create({
      doc: stripFences(node.textContent),
      extensions: [
        this.languageCompartment.of(langExt),
        exitKeymap,
        keymap.of([indentWithTab, ...defaultKeymap]),
        CodeMirrorView.lineWrapping,
        syntaxHighlighting(classHighlighter),
      ],
    })

    this.cmView = new CodeMirrorView({
      state: cmState,
      parent: this.dom,
      dispatch: (tr) => {
        this.cmView.update([tr])
        if (!tr.docChanged) return
        this.syncToPM()
      },
    })

    // Only auto-focus empty code blocks (user-created via auto-close).
    // Non-empty blocks loaded from content shouldn't steal the cursor.
    if (stripFences(node.textContent).length === 0) {
      requestAnimationFrame(() => this.cmView.focus())
    }
  }

  private syncToPM() {
    const content = this.cmView.state.doc.toString()
    const pos = this.getPos()
    const node = this.pmView.state.doc.nodeAt(pos)
    if (!node) return

    const language = node.attrs.language as string
    const fencedContent = addFences(content, language)
    if (node.textContent === fencedContent) return

    this.log.debug("syncToPM content sync", {
      pmLen: node.textContent.length,
      cmLen: content.length,
    })

    const textNode = this.pmView.state.schema.text(fencedContent)
    const tr = this.pmView.state.tr.replaceWith(
      pos + 1,
      pos + node.nodeSize - 1,
      textNode,
    )
    this.pmView.dispatch(tr)
  }

  update(node: ProsemirrorNode) {
    if (node.type.name !== "code_block") return false

    const language = (node.attrs.language as string) || ""
    const langExt = resolveLanguage(language)
    this.log.debug("update", {
      language,
      langExt: langExt?.constructor?.name ?? typeof langExt,
      contentMatch:
        this.cmView.state.doc.toString() === stripFences(node.textContent),
    })

    if (language !== this.lastLanguage) {
      this.lastLanguage = language
      this.langSelect.value = normalizeLanguage(language)
      this.cmView.dispatch({
        effects: this.languageCompartment.reconfigure(langExt),
      })
    }

    const body = stripFences(node.textContent)
    if (this.cmView.state.doc.toString() === body) return true
    this.cmView.dispatch({
      changes: { from: 0, to: this.cmView.state.doc.length, insert: body },
    })
    return true
  }

  destroy() {
    this.cmView.destroy()
  }

  stopEvent(event: Event) {
    if (
      event.type === "copy" ||
      event.type === "cut" ||
      event.type === "paste" ||
      event.type === "compositionstart" ||
      event.type === "compositionupdate" ||
      event.type === "compositionend"
    ) {
      return false
    }
    return true
  }
}
