import type { DocsGraphicAttrs } from "@/lib/drive/docs-graphic-types"
import type { PreparedGraphicMeta } from "@/lib/drive/docs-export-prepare"

type DocxArchive = Record<string, Uint8Array>

const EMU_PER_PX = 914400 / 96

function pxToEmu(px: number): number {
  return Math.round(px * EMU_PER_PX)
}

function decodeXml(bytes: Uint8Array | undefined): string {
  if (!bytes) return ""
  return new TextDecoder().decode(bytes)
}

function encodeXml(text: string): Uint8Array {
  return new TextEncoder().encode(text)
}

function nextRelId(relsXml: string): string {
  const ids = [...relsXml.matchAll(/Id="rId(\d+)"/gi)].map((m) =>
    Number.parseInt(m[1] ?? "0", 10)
  )
  const max = ids.length > 0 ? Math.max(...ids) : 0
  return `rId${max + 1}`
}

function nextImageName(archive: DocxArchive): string {
  const existing = Object.keys(archive).filter((k) => /^word\/media\/image\d+\./i.test(k))
  const nums = existing.map((k) => Number.parseInt(k.match(/image(\d+)/i)?.[1] ?? "0", 10))
  const next = nums.length > 0 ? Math.max(...nums) + 1 : 1
  return `image${next}.png`
}

function dataUrlToBytes(dataUrl: string): Uint8Array | null {
  const match = dataUrl.match(/^data:[^;]+;base64,(.+)$/)
  if (!match?.[1]) return null
  const binary = atob(match[1])
  const bytes = new Uint8Array(binary.length)
  for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i)
  return bytes
}

function wrapTypeForGraphic(wrap: DocsGraphicAttrs["wrap"]): string {
  switch (wrap) {
    case "tight":
      return "wrapTight"
    case "through":
      return "wrapThrough"
    case "top-bottom":
      return "wrapTopAndBottom"
    case "behind":
    case "in-front":
      return "wrapNone"
    default:
      return "wrapSquare"
  }
}

function buildAnchorDrawingXml(
  relId: string,
  attrs: DocsGraphicAttrs,
  imageName: string
): string {
  const cx = pxToEmu(attrs.width)
  const cy = pxToEmu(attrs.height)
  const x = pxToEmu(attrs.pageX || attrs.x)
  const y = pxToEmu(attrs.pageY || attrs.y)
  const rot = attrs.rotationDeg * 60000
  const behind = attrs.wrap === "behind" ? ' behindDoc="1"' : ""
  const wrapTag = wrapTypeForGraphic(attrs.wrap)

  return `<w:p><w:r><w:drawing>
    <wp:anchor distT="0" distB="0" distL="114300" distR="114300" simplePos="0" relativeHeight="${251658240 + attrs.zIndex}"${behind} locked="0" layoutInCell="1" allowOverlap="1">
      <wp:simplePos x="0" y="0"/>
      <wp:positionH relativeFrom="page"><wp:posOffset>${x}</wp:posOffset></wp:positionH>
      <wp:positionV relativeFrom="page"><wp:posOffset>${y}</wp:posOffset></wp:positionV>
      <wp:extent cx="${cx}" cy="${cy}"/>
      <wp:effectExtent l="0" t="0" r="0" b="0"/>
      <wp:${wrapTag} wrapText="bothSides"/>
      <wp:docPr id="${Math.floor(Math.random() * 100000)}" name="${imageName}"/>
      <wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>
      <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
          <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
            <pic:nvPicPr>
              <pic:cNvPr id="0" name="${imageName}"/>
              <pic:cNvPicPr/>
            </pic:nvPicPr>
            <pic:blipFill>
              <a:blip r:embed="${relId}"/>
              <a:stretch><a:fillRect/></a:stretch>
            </pic:blipFill>
            <pic:spPr>
              <a:xfrm rot="${rot}"><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm>
              <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
            </pic:spPr>
          </pic:pic>
        </a:graphicData>
      </a:graphic>
    </wp:anchor>
  </w:drawing></w:r></w:p>`
}

/** Inject page-layer anchored graphics into document.xml. */
export async function patchDocxAnchoredGraphics(
  buffer: Uint8Array,
  graphics: PreparedGraphicMeta[]
): Promise<Uint8Array> {
  if (graphics.length === 0) return buffer

  const { unzipSync, zipSync } = await import("fflate")
  const archive = { ...(unzipSync(buffer) as DocxArchive) }

  let relsXml =
    decodeXml(archive["word/_rels/document.xml.rels"]) ||
    `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`

  let documentXml = decodeXml(archive["word/document.xml"])
  if (!documentXml) return buffer

  const drawingBlocks: string[] = []

  for (const { attrs } of graphics) {
    if (!attrs.src) continue
    const bytes = attrs.src.startsWith("data:") ? dataUrlToBytes(attrs.src) : null
    if (!bytes) continue

    const imageName = nextImageName(archive)
    const mediaPath = `word/media/${imageName}`
    archive[mediaPath] = bytes

    const relId = nextRelId(relsXml)
    relsXml = relsXml.replace(
      "</Relationships>",
      `<Relationship Id="${relId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${imageName}"/></Relationships>`
    )

    drawingBlocks.push(buildAnchorDrawingXml(relId, attrs, imageName))
  }

  if (drawingBlocks.length === 0) return buffer

  archive["word/_rels/document.xml.rels"] = encodeXml(relsXml)
  documentXml = documentXml.replace(
    /<\/w:body>/i,
    `${drawingBlocks.join("")}</w:body>`
  )
  archive["word/document.xml"] = encodeXml(documentXml)

  return zipSync(archive)
}
