import { visit } from 'unist-util-visit'
import fs from 'fs/promises'
import path from 'path'
import { Node } from 'unist'

interface ImageNode extends Node {
  type: 'image'
  url: string
  alt?: string
  title?: string
}

interface Options {
  contentDir: string
  publicDir: string
  currentSlug: string
}

function isRelativePath(url: string): boolean {
  // Matches: ./, ../, or bare filenames without protocol/absolute path
  return (
    url.startsWith('./') || url.startsWith('../') || (!url.startsWith('/') && !url.includes('://'))
  )
}

function stripQueryParams(url: string): string {
  return url.split('?')[0]
}

// In-memory cache to prevent duplicate copies across parallel compilations
const copiedFiles = new Set<string>()

async function copyAndRewritePath(node: ImageNode, options: Options): Promise<void> {
  const { contentDir, publicDir, currentSlug } = options

  const urlWithoutParams = stripQueryParams(node.url)
  const slugParts = currentSlug.split('/')
  const contentPostDir = path.join(process.cwd(), contentDir, ...slugParts.slice(0, -1))

  const sourcePath = path.resolve(contentPostDir, urlWithoutParams)

  const allowedBasePath = path.join(process.cwd(), contentDir)
  if (!sourcePath.startsWith(allowedBasePath)) {
    throw new Error(`Invalid image path outside content directory: ${node.url}`)
  }

  const relativeToContent = path.relative(path.join(process.cwd(), contentDir), sourcePath)
  const destPath = path.join(process.cwd(), publicDir, relativeToContent)

  try {
    await fs.access(sourcePath)
  } catch {
    throw new Error(
      `Image not found: ${sourcePath}\nReferenced in: ${currentSlug}\nURL: ${node.url}`
    )
  }

  const destDir = path.dirname(destPath)
  await fs.mkdir(destDir, { recursive: true })

  // Deduplication: check cache first
  const cacheKey = `${sourcePath}:${destPath}`
  if (copiedFiles.has(cacheKey)) {
    // Already copied, just rewrite URL
    const publicUrl =
      '/' + path.relative(path.join(process.cwd(), 'public'), destPath).replace(/\\/g, '/')
    const queryParams = node.url.includes('?') ? '?' + node.url.split('?')[1] : ''
    node.url = publicUrl + queryParams
    return
  }

  // Check if destination exists with matching size
  try {
    const [sourceStat, destStat] = await Promise.all([
      fs.stat(sourcePath),
      fs.stat(destPath).catch(() => null),
    ])

    if (destStat && sourceStat.size === destStat.size) {
      // File already exists and matches, skip copy
      copiedFiles.add(cacheKey)
      const publicUrl =
        '/' + path.relative(path.join(process.cwd(), 'public'), destPath).replace(/\\/g, '/')
      const queryParams = node.url.includes('?') ? '?' + node.url.split('?')[1] : ''
      node.url = publicUrl + queryParams
      return
    }
  } catch (error) {
    // Stat failed, proceed with copy
  }

  // Attempt copy with EBUSY retry logic
  try {
    await fs.copyFile(sourcePath, destPath)
    copiedFiles.add(cacheKey)
  } catch (error: unknown) {
    const err = error as NodeJS.ErrnoException
    if (err.code === 'EBUSY') {
      // Race condition: another process is copying this file
      // Wait briefly and check if file now exists
      await new Promise(resolve => setTimeout(resolve, 100))

      try {
        await fs.access(destPath)
        // File exists now, verify integrity
        const [sourceStat, destStat] = await Promise.all([fs.stat(sourcePath), fs.stat(destPath)])

        if (sourceStat.size === destStat.size) {
          // Successfully copied by another process
          copiedFiles.add(cacheKey)
        } else {
          // File corrupted, retry once
          await fs.copyFile(sourcePath, destPath)
          copiedFiles.add(cacheKey)
        }
      } catch {
        // File still doesn't exist, retry copy
        await fs.copyFile(sourcePath, destPath)
        copiedFiles.add(cacheKey)
      }
    } else {
      // Unknown error, rethrow
      throw error
    }
  }

  const publicUrl =
    '/' + path.relative(path.join(process.cwd(), 'public'), destPath).replace(/\\/g, '/')

  const queryParams = node.url.includes('?') ? '?' + node.url.split('?')[1] : ''
  node.url = publicUrl + queryParams
}

export function remarkCopyImages(options: Options) {
  return async (tree: Node) => {
    const promises: Promise<void>[] = []

    visit(tree, 'image', (node: Node) => {
      const imageNode = node as ImageNode
      if (isRelativePath(imageNode.url)) {
        promises.push(copyAndRewritePath(imageNode, options))
      }
    })

    await Promise.all(promises)
  }
}
