import fs from "fs"
import path from "path"
import os from "os"
import { exec as execCb } from "child_process"
import { styleText, promisify } from "util"
import {
  readPluginsJson,
  writePluginsJson,
  readLockfile,
  writeLockfile,
  extractPluginName,
  readManifestFromPackageJson,
  parseGitSource,
  getGitCommit,
  PLUGINS_DIR,
  LOCKFILE_PATH,
  isLocalSource,
  getSourceUrl,
  formatSource,
  resolveLockfileName,
  getNameOverrides,
} from "./plugin-data.js"
import { symlinkOrCopySync } from "./helpers.js"

const INTERNAL_EXPORTS = new Set(["manifest", "default"])

const execAsync = promisify(execCb)

async function cloneWithSubdirAsync({ url, ref, subdir, pluginDir }) {
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "quartz-plugin-"))
  try {
    if (ref) {
      await execAsync(`git clone --depth 1 --branch ${ref} "${url}" "${tmpDir}"`)
    } else {
      await execAsync(`git clone --depth 1 "${url}" "${tmpDir}"`)
    }
    const subdirPath = path.join(tmpDir, subdir)
    if (!fs.existsSync(subdirPath)) {
      throw new Error(`Subdirectory "${subdir}" not found in cloned repository`)
    }
    fs.cpSync(subdirPath, pluginDir, { recursive: true })
    const { stdout } = await execAsync("git rev-parse HEAD", { cwd: tmpDir })
    return stdout.trim()
  } finally {
    fs.rmSync(tmpDir, { recursive: true, force: true })
  }
}

async function buildPluginAsync(pluginDir, name) {
  if (hasPrebuiltDist(pluginDir)) {
    console.log(styleText("green", `  ✓ ${name}: using pre-built dist/`))
    linkPeerPlugins(pluginDir)
    return true
  }

  try {
    const skipBuild = !needsBuild(pluginDir)
    console.log(styleText("cyan", `  → ${name}: installing dependencies...`))
    await execAsync("npm install --ignore-scripts", { cwd: pluginDir })
    if (!skipBuild) {
      console.log(styleText("cyan", `  → ${name}: building...`))
      await execAsync("npm run build", { cwd: pluginDir })
    }
    await execAsync("npm prune --omit=dev", { cwd: pluginDir })
    linkPeerPlugins(pluginDir)
    return true
  } catch (error) {
    console.log(styleText("red", `  ✗ ${name}: build failed`))
    return false
  }
}

/**
 * Run async tasks with bounded concurrency.
 * @param {Array} items - Items to process
 * @param {number} concurrency - Max parallel tasks
 * @param {Function} fn - Async function to run per item
 * @returns {Promise<Array>} Results in order
 */
async function runParallel(items, concurrency, fn) {
  const results = new Array(items.length)
  let nextIndex = 0

  async function worker() {
    while (nextIndex < items.length) {
      const i = nextIndex++
      results[i] = await fn(items[i], i)
    }
  }

  const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
  await Promise.all(workers)
  return results
}

/**
 * Check whether a plugin's .gitignore excludes dist/.
 * When dist/ is gitignored, the plugin cannot ship pre-built output in version
 * control (e.g. because it uses tree-shaking) and must always be built locally.
 */
function isDistGitignored(pluginDir) {
  const gitignorePath = path.join(pluginDir, ".gitignore")
  if (!fs.existsSync(gitignorePath)) return false

  const lines = fs.readFileSync(gitignorePath, "utf-8").split("\n")
  return lines.some((line) => {
    const trimmed = line.trim()
    return trimmed === "dist" || trimmed === "dist/" || trimmed === "/dist" || trimmed === "/dist/"
  })
}

function hasPrebuiltDist(pluginDir) {
  const distDir = path.join(pluginDir, "dist")
  return fs.existsSync(distDir) && !isDistGitignored(pluginDir)
}

function needsBuild(pluginDir) {
  if (isDistGitignored(pluginDir)) return true
  const distDir = path.join(pluginDir, "dist")
  return !fs.existsSync(distDir)
}

/**
 * After pruning devDependencies, peerDependencies may no longer be installed
 * in the plugin's own node_modules. This function resolves them:
 *
 *  1. @quartz-community/* peers → symlink to the co-installed sibling plugin
 *  2. All other peers → symlink to the host Quartz node_modules so plugins
 *     share a single copy of packages like unified, vfile, rehype-raw, etc.
 */
function trySymlink(target, linkPath) {
  symlinkOrCopySync(target, linkPath)
}

function linkPeerPlugins(pluginDir) {
  const pkgPath = path.join(pluginDir, "package.json")
  if (!fs.existsSync(pkgPath)) return

  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
  const peers = pkg.peerDependencies ?? {}

  const quartzRoot = path.resolve(pluginDir, "..", "..", "..")
  const hostNodeModules = path.join(quartzRoot, "node_modules")

  for (const peerName of Object.keys(peers)) {
    const peerNodeModulesPath = path.join(pluginDir, "node_modules", ...peerName.split("/"))
    if (fs.existsSync(peerNodeModulesPath)) continue

    if (peerName.startsWith("@quartz-community/")) {
      const siblingPlugin = findPluginByPackageName(peerName)
      if (!siblingPlugin) continue

      const scopeDir = path.join(pluginDir, "node_modules", peerName.split("/")[0])
      fs.mkdirSync(scopeDir, { recursive: true })

      const target = path.relative(scopeDir, siblingPlugin)
      trySymlink(target, peerNodeModulesPath)
      continue
    }

    const hostPeerPath = path.join(hostNodeModules, ...peerName.split("/"))
    if (!fs.existsSync(hostPeerPath)) continue

    const parts = peerName.split("/")
    if (parts.length > 1) {
      const scopeDir = path.join(pluginDir, "node_modules", parts[0])
      fs.mkdirSync(scopeDir, { recursive: true })
    } else {
      fs.mkdirSync(path.join(pluginDir, "node_modules"), { recursive: true })
    }

    const target = path.relative(path.dirname(peerNodeModulesPath), hostPeerPath)
    trySymlink(target, peerNodeModulesPath)
  }
}

/**
 * Search installed plugins for one whose package.json "name" matches the given
 * npm package name (e.g. "@quartz-community/bases-page").
 */
function findPluginByPackageName(packageName) {
  if (!fs.existsSync(PLUGINS_DIR)) return null

  const plugins = fs.readdirSync(PLUGINS_DIR).filter((entry) => {
    const entryPath = path.join(PLUGINS_DIR, entry)
    return fs.statSync(entryPath).isDirectory()
  })

  for (const pluginDirName of plugins) {
    const pkgPath = path.join(PLUGINS_DIR, pluginDirName, "package.json")
    if (!fs.existsSync(pkgPath)) continue
    try {
      const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
      if (pkg.name === packageName) {
        return path.join(PLUGINS_DIR, pluginDirName)
      }
    } catch {}
  }
  return null
}

const PLUGIN_TYPE_PATTERN =
  /Quartz(?:Emitter|Transformer|Filter|PageType)Plugin|QuartzComponentConstructor|\(.*\)\s*=>\s*QuartzComponent\b/

function resolveOriginalName(exportName, dtsContent) {
  const aliasPattern = new RegExp(`(\\w+)\\s+as\\s+${exportName}\\b`)
  const match = dtsContent.match(aliasPattern)
  return match ? match[1] : exportName
}

function isOverridableExport(name, dtsContent) {
  const declName = resolveOriginalName(name, dtsContent)
  const declPattern = new RegExp(`declare\\s+const\\s+${declName}\\s*:\\s*(.+?)(?:;|$)`, "m")
  const match = dtsContent.match(declPattern)
  if (!match) return false
  return PLUGIN_TYPE_PATTERN.test(match[1])
}

function parseExportsFromDts(content) {
  const exports = []
  const exportMatches = content.matchAll(/export\s*{\s*([^}]+)\s*}(?:\s*from\s*['"]([^'"]+)['"])?/g)
  for (const match of exportMatches) {
    const fromModule = match[2]
    if (fromModule?.startsWith("@")) continue

    const names = match[1]
      .split(",")
      .map((n) => n.trim())
      .filter(Boolean)
    for (const name of names) {
      const cleanName = name.split(" as ").pop()?.trim() || name.trim()
      if (cleanName && !cleanName.startsWith("_") && !INTERNAL_EXPORTS.has(cleanName)) {
        const finalName = cleanName.replace(/^type\s+/, "")
        if (name.includes("type ")) {
          exports.push(`type ${finalName}`)
        } else {
          exports.push(finalName)
        }
      }
    }
  }
  return exports
}

async function regeneratePluginIndex() {
  if (!fs.existsSync(PLUGINS_DIR)) return

  const pluginDirs = fs.readdirSync(PLUGINS_DIR).filter((name) => {
    const pluginPath = path.join(PLUGINS_DIR, name)
    return fs.statSync(pluginPath).isDirectory()
  })

  // Phase 1: Collect all exports per plugin, detect conflicts
  const pluginExports = new Map()
  const nameCount = new Map()

  for (const pluginName of pluginDirs) {
    const pluginDir = path.join(PLUGINS_DIR, pluginName)
    const distIndex = path.join(pluginDir, "dist", "index.d.ts")

    if (!fs.existsSync(distIndex)) continue

    const dtsContent = fs.readFileSync(distIndex, "utf-8")
    const exportedNames = parseExportsFromDts(dtsContent)
    const named = exportedNames.filter((e) => !e.startsWith("type "))
    const types = exportedNames.filter((e) => e.startsWith("type ")).map((e) => e.slice(5))

    const overridable = named.filter((n) => isOverridableExport(n, dtsContent))
    const passthrough = named.filter((n) => !isOverridableExport(n, dtsContent))

    if (overridable.length > 0 || passthrough.length > 0 || types.length > 0) {
      pluginExports.set(pluginName, { overridable, passthrough, types })
      for (const n of [...overridable, ...passthrough]) {
        nameCount.set(n, (nameCount.get(n) ?? 0) + 1)
      }
    }
  }

  // Phase 2: Generate index with registry import, plugin map, and conditional top-level exports
  const lines = []

  lines.push(`import { componentRegistry } from "../../quartz/components/registry"`)
  lines.push("")

  // Type re-exports
  for (const [pluginName, { types }] of pluginExports) {
    if (types.length > 0) {
      lines.push(`export type { ${types.join(", ")} } from "./${pluginName}"`)
    }
  }

  // Direct re-exports for non-overridable values (constants, utility functions, etc.)
  for (const [pluginName, { passthrough }] of pluginExports) {
    if (passthrough.length === 0) continue
    const unique = passthrough.filter((n) => (nameCount.get(n) ?? 0) === 1)
    if (unique.length > 0) {
      lines.push(`export { ${unique.join(", ")} } from "./${pluginName}"`)
    }
  }
  lines.push("")

  // Generate the plugins map with override wrappers (overridable exports only)
  lines.push(
    `export const plugins: Record<string, Record<string, (...args: unknown[]) => void>> = {`,
  )
  for (const [pluginName, { overridable }] of pluginExports) {
    if (overridable.length === 0) continue
    const escapedName = pluginName.replace(/"/g, '\\"')
    lines.push(`  "${escapedName}": {`)
    for (const n of overridable) {
      lines.push(
        `    ${n}: (...args: unknown[]) => { componentRegistry.setOptionOverrides("${escapedName}", args[0] as Record<string, unknown>); },`,
      )
    }
    lines.push(`  },`)
  }
  lines.push(`}`)
  lines.push("")

  // Top-level exports for overridable names: alias to the plugins map wrapper
  for (const [pluginName, { overridable }] of pluginExports) {
    if (overridable.length === 0) continue

    const unique = overridable.filter((n) => (nameCount.get(n) ?? 0) === 1)
    const conflicting = overridable.filter((n) => (nameCount.get(n) ?? 0) > 1)

    if (unique.length > 0) {
      const escapedName = pluginName.replace(/"/g, '\\"')
      for (const n of unique) {
        lines.push(`export const ${n} = plugins["${escapedName}"].${n}`)
      }
    }

    if (conflicting.length > 0) {
      for (const n of conflicting) {
        console.warn(
          styleText("yellow", `⚠`),
          `Export "${n}" conflicts across plugins — use plugins["${pluginName}"].${n} in quartz.ts`,
        )
      }
    }
  }

  lines.push("")

  const indexContent = lines.join("\n")
  const indexPath = path.join(PLUGINS_DIR, "index.ts")
  fs.writeFileSync(indexPath, indexContent)
}

export async function handlePluginInstallUnified({
  names,
  fromConfig = false,
  latest = false,
  clean = false,
  dryRun = false,
  concurrency: concurrencyOption,
} = {}) {
  if (clean && latest) {
    console.log(styleText("red", "✗ --clean and --latest cannot be used together"))
    return
  }

  const resolvedConcurrency = Math.max(1, concurrencyOption ?? os.cpus().length)

  const pluginsJson = readPluginsJson()
  let lockfile = readLockfile()

  if (!fromConfig && !lockfile) {
    console.log(
      styleText("yellow", "⚠ No quartz.lock.json found. Run 'npx quartz plugin add <repo>' first."),
    )
    return
  }

  const resolvedNames = names
    ? names.map((name) =>
        resolveLockfileName(name, lockfile ?? { version: "1.0.0", plugins: {} }, pluginsJson),
      )
    : null
  const nameFilter = resolvedNames ? new Set(resolvedNames) : null

  if (dryRun && latest) {
    if (!lockfile || Object.keys(lockfile.plugins).length === 0) {
      console.log(styleText("gray", "No plugins installed"))
      return
    }

    const nameOverrides = getNameOverrides(lockfile, pluginsJson)

    const rows = Object.entries(lockfile.plugins)
      .filter(([name]) => !nameFilter || nameFilter.has(name))
      .map(([name, entry]) => ({
        name,
        entry,
        displayName: nameOverrides.get(name) ?? name,
      }))

    const isTTY = process.stdout.isTTY
    const nameWidth = Math.max(6, ...rows.map((row) => row.displayName.length)) + 2
    const header = `${"Plugin".padEnd(nameWidth)}${"Installed".padEnd(12)}${"Latest".padEnd(12)}Status`

    const renderRow = ({ displayName }, installed, latest, statusLabel) =>
      `${displayName.padEnd(nameWidth)}${installed.padEnd(12)}${latest.padEnd(12)}${statusLabel}`

    const updateRow = (index, installed, latest, statusLabel) => {
      if (!isTTY) return
      const offset = rows.length - index
      process.stdout.write(
        `\x1b[${offset}A\x1b[2K\r${renderRow(rows[index], installed, latest, statusLabel)}\x1b[${offset}B`,
      )
    }

    if (isTTY) {
      console.log(styleText("bold", "Checking for plugin updates...\n"))
      console.log(styleText("bold", header))
      console.log("─".repeat(header.length))
      for (const row of rows) {
        if (row.entry.commit === "local") {
          console.log(renderRow(row, "local", "—", styleText("green", "local")))
          continue
        }
        console.log(renderRow(row, row.entry.commit.slice(0, 7), "—", styleText("cyan", "⋯")))
      }
    }

    const promises = rows.map((row, index) => {
      if (row.entry.commit === "local") {
        return Promise.resolve({
          index,
          installed: "local",
          latest: "—",
          status: "local",
        })
      }

      const lsRemoteRef = row.entry.ref ? `refs/heads/${row.entry.ref}` : "HEAD"
      return execAsync(`git ls-remote "${row.entry.resolved}" ${lsRemoteRef}`)
        .then(({ stdout }) => {
          const latestCommit = stdout.split("\t")[0].trim()
          const isCurrent = latestCommit === row.entry.commit
          const installed = row.entry.commit.slice(0, 7)
          const latest = latestCommit.slice(0, 7)
          const statusLabel = isCurrent
            ? styleText("green", "up to date")
            : styleText("yellow", "update available")
          updateRow(index, installed, latest, statusLabel)
          return {
            index,
            installed,
            latest,
            status: isCurrent ? "up to date" : "update available",
          }
        })
        .catch(() => {
          const installed = row.entry.commit.slice(0, 7)
          const latest = "?"
          const statusLabel = styleText("red", "check failed")
          updateRow(index, installed, latest, statusLabel)
          return {
            index,
            installed,
            latest,
            status: "check failed",
          }
        })
    })

    const results = await Promise.all(promises)

    if (!isTTY) {
      console.log(styleText("bold", "Checking for plugin updates...\n"))
      console.log(styleText("bold", header))
      console.log("─".repeat(header.length))
      for (const { index, installed, latest, status } of results) {
        const color =
          status === "up to date" || status === "local"
            ? "green"
            : status === "check failed"
              ? "red"
              : "yellow"
        console.log(renderRow(rows[index], installed, latest, styleText(color, status)))
      }
    }
    return
  }

  if (fromConfig) {
    if (!pluginsJson?.plugins || pluginsJson.plugins.length === 0) {
      console.log(styleText("gray", "No plugins configured"))
      return
    }

    if (!lockfile) {
      lockfile = { version: "1.0.0", plugins: {} }
    }

    if (!fs.existsSync(PLUGINS_DIR)) {
      fs.mkdirSync(PLUGINS_DIR, { recursive: true })
    }

    const configNames = new Set(pluginsJson.plugins.map((entry) => extractPluginName(entry.source)))
    const orphans = Object.keys(lockfile.plugins).filter((name) => !configNames.has(name))

    const missing = pluginsJson.plugins
      .filter((entry) => {
        const name = extractPluginName(entry.source)
        const pluginDir = path.join(PLUGINS_DIR, name)
        if (lockfile.plugins[name] && fs.existsSync(pluginDir)) return false
        const src = getSourceUrl(entry.source)
        return (
          src.startsWith("github:") ||
          src.startsWith("git+") ||
          src.startsWith("https://") ||
          isLocalSource(src)
        )
      })
      .filter((entry) => {
        if (!nameFilter) return true
        const name = extractPluginName(entry.source)
        return nameFilter.has(name)
      })

    if (missing.length === 0) {
      console.log(styleText("green", "✓ All configured plugins are already installed"))
      if (dryRun) {
        if (orphans.length > 0) {
          console.log()
          console.log(`Found ${orphans.length} orphaned plugin(s) in lockfile:\n`)
          for (const name of orphans) {
            console.log(`  ${styleText("yellow", name)} — in lockfile but not in config`)
          }
          console.log()
          console.log(
            styleText("cyan", "Dry run — no changes made. Re-run without --dry-run to resolve."),
          )
        }
        return
      }
      if (orphans.length === 0) {
        return
      }
    }

    if (missing.length > 0) {
      console.log(`Found ${missing.length} uninstalled plugin(s) in config:\n`)
      for (const entry of missing) {
        const name = extractPluginName(entry.source)
        console.log(`  ${styleText("yellow", name)} — ${formatSource(entry.source)}`)
      }
      console.log()

      if (dryRun) {
        if (orphans.length > 0) {
          console.log(`Found ${orphans.length} orphaned plugin(s) in lockfile:\n`)
          for (const name of orphans) {
            console.log(`  ${styleText("yellow", name)} — in lockfile but not in config`)
          }
          console.log()
        }
        console.log(
          styleText("cyan", "Dry run — no changes made. Re-run without --dry-run to resolve."),
        )
        return
      }
    }

    const installed = []
    let failed = 0
    let lockfileChanged = false

    // Handle existing dirs and local symlinks (fast), collect remote clones
    const remoteEntries = []
    for (const entry of missing) {
      try {
        const { name, url, ref, local, subdir } = parseGitSource(entry.source)
        const pluginDir = path.join(PLUGINS_DIR, name)

        if (fs.existsSync(pluginDir)) {
          if (local) {
            console.log(
              styleText("yellow", `⚠ ${name} directory already exists, updating lockfile`),
            )
            lockfile.plugins[name] = {
              source: entry.source,
              resolved: url,
              commit: "local",
              ...(subdir && { subdir }),
              installedAt: new Date().toISOString(),
            }
            installed.push({ name, pluginDir })
            lockfileChanged = true
            continue
          }
          console.log(styleText("yellow", `⚠ ${name} directory already exists, updating lockfile`))
          const commit = getGitCommit(pluginDir)
          lockfile.plugins[name] = {
            source: entry.source,
            resolved: url,
            commit,
            ...(ref && { ref }),
            ...(subdir && { subdir }),
            installedAt: new Date().toISOString(),
          }
          installed.push({ name, pluginDir })
          lockfileChanged = true
          continue
        }

        if (local) {
          let resolvedPath = path.resolve(url)
          if (subdir) resolvedPath = path.join(resolvedPath, subdir)
          if (!fs.existsSync(resolvedPath)) {
            console.log(styleText("red", `✗ Local path does not exist: ${resolvedPath}`))
            failed++
            continue
          }
          console.log(styleText("cyan", `→ Linking ${name} from ${resolvedPath}...`))
          fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
          symlinkOrCopySync(resolvedPath, pluginDir)
          lockfile.plugins[name] = {
            source: entry.source,
            resolved: resolvedPath,
            commit: "local",
            ...(subdir && { subdir }),
            installedAt: new Date().toISOString(),
          }
          installed.push({ name, pluginDir })
          lockfileChanged = true
          console.log(styleText("green", `✓ Linked ${name} (local)`))
        } else {
          remoteEntries.push({ entry, name, url, ref, subdir, pluginDir })
        }
      } catch (error) {
        console.log(styleText("red", `✗ Failed to resolve ${formatSource(entry.source)}: ${error}`))
        failed++
      }
    }

    // Clone remote plugins in parallel
    if (remoteEntries.length > 0) {
      const concurrency = resolvedConcurrency
      await runParallel(
        remoteEntries,
        concurrency,
        async ({ entry, name, url, ref, subdir, pluginDir }) => {
          try {
            if (subdir) {
              console.log(styleText("cyan", `→ Cloning ${name} from ${url} (subdir: ${subdir})...`))
              fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
              const commit = await cloneWithSubdirAsync({ url, ref, subdir, pluginDir })
              lockfile.plugins[name] = {
                source: entry.source,
                resolved: url,
                commit,
                ...(ref && { ref }),
                subdir,
                installedAt: new Date().toISOString(),
              }
              installed.push({ name, pluginDir })
              lockfileChanged = true
              console.log(
                styleText("green", `✓ Cloned ${name}@${commit.slice(0, 7)} (subdir: ${subdir})`),
              )
            } else {
              console.log(styleText("cyan", `→ Cloning ${name} from ${url}...`))

              const branchArg = ref ? ` --branch ${ref}` : ""
              await execAsync(`git clone --depth 1${branchArg} "${url}" "${pluginDir}"`)

              const { stdout } = await execAsync("git rev-parse HEAD", { cwd: pluginDir })
              const commit = stdout.trim()
              lockfile.plugins[name] = {
                source: entry.source,
                resolved: url,
                commit,
                ...(ref && { ref }),
                installedAt: new Date().toISOString(),
              }

              installed.push({ name, pluginDir })
              lockfileChanged = true
              console.log(styleText("green", `✓ Cloned ${name}@${commit.slice(0, 7)}`))
            }
          } catch (error) {
            console.log(
              styleText("red", `✗ Failed to resolve ${formatSource(entry.source)}: ${error}`),
            )
            failed++
          }
        },
      )
    }

    if (installed.length > 0) {
      console.log()
      console.log(styleText("cyan", "→ Building plugins..."))
      const concurrency = resolvedConcurrency
      const results = await runParallel(installed, concurrency, async ({ name, pluginDir }) => {
        const ok = await buildPluginAsync(pluginDir, name)
        if (ok) console.log(styleText("green", `  ✓ ${name} built`))
        return ok
      })
      for (const ok of results) {
        if (!ok) failed++
      }
      await regeneratePluginIndex()
    }

    if (orphans.length > 0) {
      console.log()
      let removedOrphans = false
      for (const name of orphans) {
        const entry = lockfile.plugins[name]
        if (entry?.commit === "local") {
          console.log(
            styleText(
              "yellow",
              `⚠ ${name} is a local plugin not in config — skipping (remove manually with 'plugin remove')`,
            ),
          )
          continue
        }
        const pluginDir = path.join(PLUGINS_DIR, name)
        if (fs.existsSync(pluginDir)) {
          fs.rmSync(pluginDir, { recursive: true })
        }
        delete lockfile.plugins[name]
        lockfileChanged = true
        removedOrphans = true
        console.log(styleText("yellow", `✗ Removed ${name} (not in config)`))
      }
      if (removedOrphans) {
        await regeneratePluginIndex()
      }
    }

    if (lockfileChanged) {
      writeLockfile(lockfile)
      console.log()
      if (failed === 0) {
        console.log(styleText("green", `✓ Resolved ${installed.length} plugin(s)`))
      } else {
        console.log(
          styleText("yellow", `⚠ Resolved ${installed.length} plugin(s), ${failed} failed`),
        )
      }
      console.log(styleText("gray", "Updated quartz.lock.json"))
    } else if (failed > 0) {
      console.log()
      console.log(styleText("yellow", `⚠ Resolved ${installed.length} plugin(s), ${failed} failed`))
    }

    return
  }

  if (dryRun) {
    const entries = Object.entries(lockfile.plugins).filter(([name]) =>
      nameFilter ? nameFilter.has(name) : true,
    )
    if (entries.length === 0) {
      console.log(styleText("gray", "No plugins installed"))
      return
    }

    console.log(styleText("cyan", "→ Dry run: plugins to install from lockfile..."))
    for (const [name, entry] of entries) {
      const sourceLabel = entry.source ? formatSource(entry.source) : entry.resolved
      const commitLabel = entry.commit === "local" ? "local" : entry.commit.slice(0, 7)
      console.log(`  ${styleText("yellow", name)} — ${sourceLabel} (${commitLabel})`)
    }
    return
  }

  if (clean) {
    console.log(styleText("cyan", "→ Restoring plugins from lockfile..."))
    console.log()

    if (!fs.existsSync(PLUGINS_DIR)) {
      fs.mkdirSync(PLUGINS_DIR, { recursive: true })
    }

    let installed = 0
    let failed = 0
    const restoredPlugins = []

    const entries = Object.entries(lockfile.plugins).filter(([name]) =>
      nameFilter ? nameFilter.has(name) : true,
    )

    // Handle local symlinks and collect remote plugins to clone
    const remotePlugins = []
    for (const [name, entry] of entries) {
      const pluginDir = path.join(PLUGINS_DIR, name)

      if (fs.existsSync(pluginDir)) {
        console.log(styleText("yellow", `⚠ ${name}: directory exists, skipping`))
        continue
      }

      if (entry.commit === "local") {
        try {
          if (!fs.existsSync(entry.resolved)) {
            console.log(styleText("red", `  ✗ ${name}: local path missing: ${entry.resolved}`))
            failed++
            continue
          }
          fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
          symlinkOrCopySync(entry.resolved, pluginDir)
          console.log(styleText("green", `✓ ${name} restored (local symlink)`))
          restoredPlugins.push({ name, pluginDir })
          installed++
        } catch {
          console.log(styleText("red", `✗ ${name}: failed to restore local symlink`))
          failed++
        }
        continue
      }

      remotePlugins.push({ name, entry, pluginDir })
    }

    // Clone remote plugins in parallel
    if (remotePlugins.length > 0) {
      const concurrency = resolvedConcurrency
      await runParallel(remotePlugins, concurrency, async ({ name, entry, pluginDir }) => {
        try {
          if (entry.subdir) {
            console.log(
              styleText(
                "cyan",
                `→ ${name}: cloning ${entry.resolved}@${entry.commit.slice(0, 7)} (subdir: ${entry.subdir})...`,
              ),
            )
            fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
            await cloneWithSubdirAsync({
              url: entry.resolved,
              ref: entry.ref,
              subdir: entry.subdir,
              pluginDir,
            })
          } else {
            console.log(
              styleText(
                "cyan",
                `→ ${name}: cloning ${entry.resolved}@${entry.commit.slice(0, 7)}...`,
              ),
            )
            const branchArg = entry.ref ? ` --branch ${entry.ref}` : ""
            await execAsync(`git clone --depth 1${branchArg} "${entry.resolved}" "${pluginDir}"`)
            await execAsync(`git checkout ${entry.commit}`, { cwd: pluginDir })
          }
          console.log(styleText("green", `✓ ${name} restored`))
          restoredPlugins.push({ name, pluginDir })
          installed++
        } catch {
          console.log(styleText("red", `✗ ${name}: failed to restore`))
          failed++
        }
      })
    }

    if (restoredPlugins.length > 0) {
      console.log()
      console.log(styleText("cyan", "→ Building restored plugins..."))
      const concurrency = resolvedConcurrency
      const results = await runParallel(
        restoredPlugins,
        concurrency,
        async ({ name, pluginDir }) => {
          const ok = await buildPluginAsync(pluginDir, name)
          if (ok) console.log(styleText("green", `  ✓ ${name} built`))
          return ok
        },
      )
      for (const ok of results) {
        if (!ok) {
          failed++
          installed--
        }
      }
      await regeneratePluginIndex()
    }

    console.log()
    if (failed === 0) {
      console.log(styleText("green", `✓ Restored ${installed} plugin(s)`))
    } else {
      console.log(styleText("yellow", `⚠ Restored ${installed} plugin(s), ${failed} failed`))
    }
    return
  }

  if (latest) {
    const pluginsToUpdate = nameFilter ? Array.from(nameFilter) : Object.keys(lockfile.plugins)
    const updatedPlugins = []
    let lockfileChanged = false

    // Phase 1: Validate and categorize plugins (fast, sequential)
    const validPlugins = []
    for (const name of pluginsToUpdate) {
      const entry = lockfile.plugins[name]
      if (!entry) {
        console.log(styleText("yellow", `⚠ ${name} is not installed`))
        continue
      }

      const pluginDir = path.join(PLUGINS_DIR, name)
      if (!fs.existsSync(pluginDir)) {
        console.log(
          styleText("yellow", `⚠ ${name} directory missing. Run 'npx quartz plugin install'.`),
        )
        continue
      }

      if (entry.commit === "local") {
        console.log(styleText("cyan", `→ Rebuilding local plugin ${name}...`))
        updatedPlugins.push({ name, pluginDir })
        continue
      }

      validPlugins.push({ name, pluginDir, entry })
    }

    // Phase 2: Fetch/update plugins in parallel
    if (validPlugins.length > 0) {
      const concurrency = resolvedConcurrency
      await runParallel(validPlugins, concurrency, async ({ name, pluginDir, entry }) => {
        try {
          console.log(styleText("cyan", `→ Updating ${name}...`))

          if (entry.subdir) {
            fs.rmSync(pluginDir, { recursive: true })
            fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
            const newCommit = await cloneWithSubdirAsync({
              url: entry.resolved,
              ref: entry.ref,
              subdir: entry.subdir,
              pluginDir,
            })
            if (needsBuild(pluginDir)) {
              updatedPlugins.push({ name, pluginDir })
            }
            if (newCommit !== entry.commit) {
              entry.commit = newCommit
              entry.installedAt = new Date().toISOString()
              lockfileChanged = true
              console.log(
                styleText(
                  "green",
                  `✓ Updated ${name} to ${newCommit.slice(0, 7)} (subdir: ${entry.subdir})`,
                ),
              )
            } else {
              console.log(styleText("gray", `✓ ${name} rebuilt (subdir: ${entry.subdir})`))
            }
          } else {
            const fetchRef = entry.ref || ""
            const resetTarget = entry.ref ? `origin/${entry.ref}` : "origin/HEAD"
            await execAsync(`git fetch --depth 1 origin${fetchRef ? " " + fetchRef : ""}`, {
              cwd: pluginDir,
            })
            await execAsync(`git reset --hard ${resetTarget}`, { cwd: pluginDir })

            const { stdout } = await execAsync("git rev-parse HEAD", { cwd: pluginDir })
            const newCommit = stdout.trim()
            if (newCommit !== entry.commit) {
              entry.commit = newCommit
              entry.installedAt = new Date().toISOString()
              updatedPlugins.push({ name, pluginDir })
              lockfileChanged = true
              console.log(styleText("green", `✓ Updated ${name} to ${newCommit.slice(0, 7)}`))
            } else {
              console.log(styleText("gray", `✓ ${name} already up to date`))
            }
          }
        } catch (error) {
          console.log(styleText("red", `✗ Failed to update ${name}: ${error}`))
        }
      })
    }

    // Phase 3: Build updated plugins in parallel
    if (updatedPlugins.length > 0) {
      console.log()
      console.log(styleText("cyan", "→ Rebuilding updated plugins..."))
      const concurrency = resolvedConcurrency
      await runParallel(updatedPlugins, concurrency, async ({ name, pluginDir }) => {
        const ok = await buildPluginAsync(pluginDir, name)
        if (ok) console.log(styleText("green", `  ✓ ${name} rebuilt`))
        return ok
      })
      await regeneratePluginIndex()
    }

    if (lockfileChanged) {
      writeLockfile(lockfile)
      console.log()
      console.log(styleText("gray", "Updated quartz.lock.json"))
    }
    return
  }

  if (!fs.existsSync(PLUGINS_DIR)) {
    fs.mkdirSync(PLUGINS_DIR, { recursive: true })
  }

  const entries = Object.entries(lockfile.plugins).filter(([name]) =>
    nameFilter ? nameFilter.has(name) : true,
  )
  if (entries.length === 0) {
    console.log(styleText("gray", "No plugins installed"))
    return
  }

  console.log(styleText("cyan", "→ Installing plugins from lockfile..."))
  let installed = 0
  let failed = 0
  const pluginsToBuild = []

  // Handle local plugins and collect entries needing git operations
  const gitEntries = []
  for (const [name, entry] of entries) {
    const pluginDir = path.join(PLUGINS_DIR, name)

    if (entry.commit === "local") {
      try {
        if (fs.existsSync(pluginDir)) {
          const stat = fs.lstatSync(pluginDir)
          if (stat.isSymbolicLink() && fs.readlinkSync(pluginDir) === entry.resolved) {
            console.log(styleText("gray", `  ✓ ${name} (local) already linked`))
            installed++
            continue
          }
          if (stat.isSymbolicLink()) fs.unlinkSync(pluginDir)
          else fs.rmSync(pluginDir, { recursive: true })
        }
        if (!fs.existsSync(entry.resolved)) {
          console.log(styleText("red", `  ✗ ${name}: local path missing: ${entry.resolved}`))
          failed++
          continue
        }
        fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
        symlinkOrCopySync(entry.resolved, pluginDir)
        console.log(styleText("green", `  ✓ ${name} (local) linked`))
        pluginsToBuild.push({ name, pluginDir })
        installed++
      } catch {
        console.log(styleText("red", `  ✗ ${name}: failed to link local path`))
        failed++
      }
      continue
    }

    if (fs.existsSync(pluginDir)) {
      if (entry.subdir) {
        if (!needsBuild(pluginDir)) {
          console.log(
            styleText("gray", `  ✓ ${name}@${entry.commit.slice(0, 7)} already installed (subdir)`),
          )
          installed++
          continue
        }
        pluginsToBuild.push({ name, pluginDir })
        installed++
      } else {
        const currentCommit = getGitCommit(pluginDir)
        if (currentCommit === entry.commit && !needsBuild(pluginDir)) {
          console.log(
            styleText("gray", `  ✓ ${name}@${entry.commit.slice(0, 7)} already installed`),
          )
          installed++
          continue
        }
        if (currentCommit !== entry.commit) {
          gitEntries.push({ name, entry, pluginDir, action: "update" })
        } else {
          pluginsToBuild.push({ name, pluginDir })
          installed++
        }
      }
    } else {
      gitEntries.push({ name, entry, pluginDir, action: "clone" })
    }
  }

  // Run git fetch/clone operations in parallel
  if (gitEntries.length > 0) {
    const concurrency = resolvedConcurrency
    await runParallel(gitEntries, concurrency, async ({ name, entry, pluginDir, action }) => {
      try {
        if (action === "update") {
          console.log(styleText("cyan", `  → ${name}: updating to ${entry.commit.slice(0, 7)}...`))
          const fetchRef = entry.ref ? ` ${entry.ref}` : ""
          await execAsync(`git fetch --depth 1 origin${fetchRef}`, { cwd: pluginDir })
          await execAsync(`git reset --hard ${entry.commit}`, { cwd: pluginDir })
          pluginsToBuild.push({ name, pluginDir })
          installed++
        } else {
          if (entry.subdir) {
            console.log(styleText("cyan", `  → ${name}: cloning (subdir: ${entry.subdir})...`))
            fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
            await cloneWithSubdirAsync({
              url: entry.resolved,
              ref: entry.ref,
              subdir: entry.subdir,
              pluginDir,
            })
          } else {
            console.log(styleText("cyan", `  → ${name}: cloning...`))
            const branchArg = entry.ref ? ` --branch ${entry.ref}` : ""
            await execAsync(`git clone --depth 1${branchArg} "${entry.resolved}" "${pluginDir}"`)
            if (entry.commit !== "unknown") {
              await execAsync(`git fetch --depth 1 origin ${entry.commit}`, { cwd: pluginDir })
              await execAsync(`git checkout ${entry.commit}`, { cwd: pluginDir })
            }
          }
          console.log(styleText("green", `  ✓ ${name}@${entry.commit.slice(0, 7)}`))
          pluginsToBuild.push({ name, pluginDir })
          installed++
        }
      } catch {
        console.log(
          styleText("red", `  ✗ ${name}: failed to ${action === "update" ? "update" : "clone"}`),
        )
        failed++
      }
    })
  }

  if (pluginsToBuild.length > 0) {
    console.log()
    console.log(styleText("cyan", "→ Building plugins..."))
    const concurrency = resolvedConcurrency
    const results = await runParallel(pluginsToBuild, concurrency, async ({ name, pluginDir }) => {
      const ok = await buildPluginAsync(pluginDir, name)
      if (ok) console.log(styleText("green", `  ✓ ${name} built`))
      return ok
    })
    for (const ok of results) {
      if (!ok) {
        failed++
        installed--
      }
    }
  }

  await regeneratePluginIndex()

  console.log()
  if (failed === 0) {
    console.log(styleText("green", `✓ Installed ${installed} plugin(s)`))
  } else {
    console.log(styleText("yellow", `⚠ Installed ${installed} plugin(s), ${failed} failed`))
  }
}

export async function handlePluginInstall() {
  return handlePluginInstallUnified()
}

export async function handlePluginAdd(
  sources,
  { name: nameOverride, subdir: subdirOverride, concurrency: concurrencyOption } = {},
) {
  if (nameOverride && sources.length > 1) {
    console.log(styleText("red", "✗ --name/--as can only be used when adding a single plugin"))
    return
  }
  if (subdirOverride && sources.length > 1) {
    console.log(styleText("red", "✗ --subdir can only be used when adding a single plugin"))
    return
  }

  const resolvedConcurrency = Math.max(1, concurrencyOption ?? os.cpus().length)

  let lockfile = readLockfile()
  if (!lockfile) {
    lockfile = { version: "1.0.0", plugins: {} }
  }

  if (!fs.existsSync(PLUGINS_DIR)) {
    fs.mkdirSync(PLUGINS_DIR, { recursive: true })
  }

  const addedPlugins = []

  // Handle local plugins and collect remote sources to clone
  const remoteSources = []
  for (const source of sources) {
    try {
      const parsed = parseGitSource(source)
      const name = nameOverride ?? parsed.name
      const url = parsed.url
      const ref = parsed.ref
      const local = parsed.local
      const subdir = subdirOverride ?? parsed.subdir
      const pluginDir = path.join(PLUGINS_DIR, name)

      let configSource = undefined
      if (nameOverride || subdirOverride) {
        configSource = { repo: source }
        if (nameOverride) configSource.name = nameOverride
        if (subdirOverride) configSource.subdir = subdirOverride
      }

      if (fs.existsSync(pluginDir)) {
        console.log(styleText("yellow", `⚠ ${name} already exists. Use 'update' to refresh.`))
        continue
      }

      if (local) {
        let resolvedPath = path.resolve(url)
        if (subdir) resolvedPath = path.join(resolvedPath, subdir)
        if (!fs.existsSync(resolvedPath)) {
          console.log(styleText("red", `✗ Local path does not exist: ${resolvedPath}`))
          continue
        }
        console.log(styleText("cyan", `→ Adding ${name} from local path ${resolvedPath}...`))
        fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
        symlinkOrCopySync(resolvedPath, pluginDir)
        lockfile.plugins[name] = {
          source,
          resolved: resolvedPath,
          commit: "local",
          ...(subdir && { subdir }),
          installedAt: new Date().toISOString(),
        }
        addedPlugins.push({ name, pluginDir, source, configSource })
        console.log(styleText("green", `✓ Added ${name} (local symlink)`))
      } else {
        remoteSources.push({ source, name, url, ref, subdir, pluginDir, configSource })
      }
    } catch (error) {
      console.log(styleText("red", `✗ Failed to add ${formatSource(source)}: ${error}`))
    }
  }

  // Clone remote plugins in parallel
  if (remoteSources.length > 0) {
    const concurrency = resolvedConcurrency
    await runParallel(
      remoteSources,
      concurrency,
      async ({ source, name, url, ref, subdir, pluginDir, configSource }) => {
        try {
          if (subdir) {
            console.log(styleText("cyan", `→ Adding ${name} from ${url} (subdir: ${subdir})...`))
            fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
            const commit = await cloneWithSubdirAsync({ url, ref, subdir, pluginDir })
            lockfile.plugins[name] = {
              source,
              resolved: url,
              commit,
              ...(ref && { ref }),
              subdir,
              installedAt: new Date().toISOString(),
            }
            addedPlugins.push({ name, pluginDir, source, configSource })
            console.log(
              styleText("green", `✓ Added ${name}@${commit.slice(0, 7)} (subdir: ${subdir})`),
            )
          } else {
            console.log(styleText("cyan", `→ Adding ${name} from ${url}...`))

            const branchArg = ref ? ` --branch ${ref}` : ""
            await execAsync(`git clone --depth 1${branchArg} "${url}" "${pluginDir}"`)

            const { stdout } = await execAsync("git rev-parse HEAD", { cwd: pluginDir })
            const commit = stdout.trim()
            lockfile.plugins[name] = {
              source,
              resolved: url,
              commit,
              ...(ref && { ref }),
              installedAt: new Date().toISOString(),
            }

            addedPlugins.push({ name, pluginDir, source, configSource })
            console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)}`))
          }
        } catch (error) {
          console.log(styleText("red", `✗ Failed to add ${formatSource(source)}: ${error}`))
        }
      },
    )
  }

  if (addedPlugins.length > 0) {
    console.log()
    console.log(styleText("cyan", "→ Building plugins..."))
    const concurrency = resolvedConcurrency
    await runParallel(addedPlugins, concurrency, async ({ name, pluginDir }) => {
      const ok = await buildPluginAsync(pluginDir, name)
      if (ok) console.log(styleText("green", `  ✓ ${name} built`))
      return ok
    })
    await regeneratePluginIndex()
  }

  writeLockfile(lockfile)
  const pluginsJson = readPluginsJson()
  if (pluginsJson?.plugins) {
    for (const { pluginDir, source, configSource } of addedPlugins) {
      const manifest = readManifestFromPackageJson(pluginDir)
      const newEntry = {
        source: configSource ?? source,
        enabled: manifest?.defaultEnabled ?? true,
        options: manifest?.defaultOptions ?? {},
        order: manifest?.defaultOrder ?? 50,
      }

      if (manifest?.components) {
        const layoutPositions = new Set(["left", "right", "beforeBody", "afterBody"])
        const firstComponentKey = Object.keys(manifest.components)[0]
        const comp = manifest.components[firstComponentKey]
        if (comp?.defaultPosition && layoutPositions.has(comp.defaultPosition)) {
          newEntry.layout = {
            position: comp.defaultPosition,
            priority: comp.defaultPriority ?? 50,
          }
        }
      }

      pluginsJson.plugins.push(newEntry)
    }
    writePluginsJson(pluginsJson)
  }
  console.log()
  console.log(styleText("gray", "Updated quartz.lock.json"))
}

export async function handlePluginRemove(names) {
  const lockfile = readLockfile()
  if (!lockfile) {
    console.log(styleText("yellow", "⚠ No plugins installed"))
    return
  }

  const pluginsJson = readPluginsJson()
  let removed = false
  const resolvedNames = []
  for (const name of names) {
    const lockKey = resolveLockfileName(name, lockfile, pluginsJson)
    resolvedNames.push(lockKey)
    const pluginDir = path.join(PLUGINS_DIR, lockKey)

    if (!lockfile.plugins[lockKey] && !fs.existsSync(pluginDir)) {
      console.log(styleText("yellow", `⚠ ${name} is not installed`))
      continue
    }

    const displayName = lockKey !== name ? `${name} (${lockKey})` : name
    console.log(styleText("cyan", `→ Removing ${displayName}...`))

    if (fs.existsSync(pluginDir)) {
      fs.rmSync(pluginDir, { recursive: true })
    }

    delete lockfile.plugins[lockKey]
    console.log(styleText("green", `✓ Removed ${displayName}`))
    removed = true
  }

  if (removed) {
    await regeneratePluginIndex()
  }

  writeLockfile(lockfile)
  if (pluginsJson?.plugins) {
    pluginsJson.plugins = pluginsJson.plugins.filter(
      (plugin) =>
        !names.includes(extractPluginName(plugin.source)) &&
        !names.includes(formatSource(plugin.source)) &&
        !resolvedNames.includes(extractPluginName(plugin.source)),
    )
    writePluginsJson(pluginsJson)
  }
  console.log()
  console.log(styleText("gray", "Updated quartz.lock.json"))
}

export async function handlePluginEnable(names) {
  const json = readPluginsJson()
  if (!json) {
    console.log(styleText("red", "✗ No quartz.config.yaml found. Cannot enable plugins."))
    return
  }

  for (const name of names) {
    const entry = json.plugins.find(
      (e) => extractPluginName(e.source) === name || formatSource(e.source) === name,
    )
    if (!entry) {
      console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.config.yaml`))
      continue
    }
    if (entry.enabled) {
      console.log(styleText("gray", `✓ ${name} is already enabled`))
      continue
    }
    entry.enabled = true
    console.log(styleText("green", `✓ Enabled ${name}`))
  }

  writePluginsJson(json)
}

export async function handlePluginDisable(names) {
  const json = readPluginsJson()
  if (!json) {
    console.log(styleText("red", "✗ No quartz.config.yaml found. Cannot disable plugins."))
    return
  }

  for (const name of names) {
    const entry = json.plugins.find(
      (e) => extractPluginName(e.source) === name || formatSource(e.source) === name,
    )
    if (!entry) {
      console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.config.yaml`))
      continue
    }
    if (!entry.enabled) {
      console.log(styleText("gray", `✓ ${name} is already disabled`))
      continue
    }
    entry.enabled = false
    console.log(styleText("green", `✓ Disabled ${name}`))
  }

  writePluginsJson(json)
}

export async function handlePluginConfig(name, options = {}) {
  const json = readPluginsJson()
  if (!json) {
    console.log(styleText("red", "✗ No quartz.config.yaml found."))
    return
  }

  const entry = json.plugins.find(
    (e) => extractPluginName(e.source) === name || formatSource(e.source) === name,
  )
  if (!entry) {
    console.log(styleText("red", `✗ Plugin "${name}" not found in quartz.config.yaml`))
    return
  }

  if (options.set) {
    const eqIndex = options.set.indexOf("=")
    if (eqIndex === -1) {
      console.log(styleText("red", "✗ Invalid format. Use: --set key=value"))
      return
    }
    const key = options.set.slice(0, eqIndex)
    let value = options.set.slice(eqIndex + 1)

    try {
      value = JSON.parse(value)
    } catch {}

    if (!entry.options) entry.options = {}
    entry.options[key] = value
    writePluginsJson(json)
    console.log(styleText("green", `✓ Set ${name}.${key} = ${JSON.stringify(value)}`))
  } else {
    console.log(styleText("bold", `Plugin: ${name}`))
    console.log(`  Source: ${formatSource(entry.source)}`)
    console.log(`  Enabled: ${entry.enabled}`)
    console.log(`  Order: ${entry.order ?? 50}`)
    if (entry.options && Object.keys(entry.options).length > 0) {
      console.log(`  Options:`)
      for (const [k, v] of Object.entries(entry.options)) {
        console.log(`    ${k}: ${JSON.stringify(v)}`)
      }
    } else {
      console.log(`  Options: (none)`)
    }
    if (entry.layout) {
      console.log(`  Layout:`)
      for (const [k, v] of Object.entries(entry.layout)) {
        console.log(`    ${k}: ${JSON.stringify(v)}`)
      }
    }
  }
}

export async function handlePluginCheck() {
  return handlePluginInstallUnified({ latest: true, dryRun: true })
}

export async function handlePluginUpdate(names) {
  return handlePluginInstallUnified({ names, latest: true })
}

export async function handlePluginList() {
  const lockfile = readLockfile()
  if (!lockfile || Object.keys(lockfile.plugins).length === 0) {
    console.log(styleText("gray", "No plugins installed"))
    return
  }

  const pluginsJson = readPluginsJson()
  const nameOverrides = getNameOverrides(lockfile, pluginsJson)

  console.log(styleText("bold", "Installed Plugins:"))
  console.log()

  for (const [name, entry] of Object.entries(lockfile.plugins)) {
    const pluginDir = path.join(PLUGINS_DIR, name)
    const exists = fs.existsSync(pluginDir)
    const overriddenName = nameOverrides.get(name)
    const displayLabel = overriddenName
      ? `${overriddenName} ${styleText("gray", `(dir: ${name})`)}`
      : name

    if (entry.commit === "local") {
      const isLinked = exists && fs.lstatSync(pluginDir).isSymbolicLink()
      const status = isLinked ? styleText("green", "✓") : styleText("red", "✗")
      console.log(`  ${status} ${styleText("bold", displayLabel)}`)
      console.log(`    Source: ${formatSource(entry.source)}`)
      console.log(`    Type: local symlink`)
      console.log(`    Target: ${entry.resolved}`)
      console.log(`    Installed: ${new Date(entry.installedAt).toLocaleDateString()}`)
      console.log()
      continue
    }

    let currentCommit = entry.commit

    if (exists) {
      currentCommit = getGitCommit(pluginDir)
    }

    const status = exists
      ? currentCommit === entry.commit
        ? styleText("green", "✓")
        : styleText("yellow", "⚡")
      : styleText("red", "✗")

    console.log(`  ${status} ${styleText("bold", displayLabel)}`)
    console.log(`    Source: ${formatSource(entry.source)}`)
    console.log(`    Commit: ${entry.commit.slice(0, 7)}`)
    if (currentCommit !== entry.commit && exists) {
      console.log(`    Current: ${currentCommit.slice(0, 7)} (modified)`)
    }
    console.log(`    Installed: ${new Date(entry.installedAt).toLocaleDateString()}`)
    console.log()
  }
}

export async function handlePluginStatus() {
  const lockfile = readLockfile()
  if (!lockfile || Object.keys(lockfile.plugins).length === 0) {
    console.log(styleText("gray", "No plugins installed"))
    return
  }

  const pluginsJson = readPluginsJson()
  const nameOverrides = getNameOverrides(lockfile, pluginsJson)
  const enabledByName = new Map(
    (pluginsJson?.plugins ?? []).map((entry) => [
      extractPluginName(entry.source),
      entry.enabled !== false,
    ]),
  )

  const rows = Object.entries(lockfile.plugins).map(([name, entry]) => {
    const pluginDir = path.join(PLUGINS_DIR, name)
    const exists = fs.existsSync(pluginDir)
    const displayName = nameOverrides.get(name) ?? name
    const sourceLabel = formatSource(entry.source)
    const commitLabel = entry.commit === "local" ? "local" : `@${entry.commit.slice(0, 7)}`
    const enabled = enabledByName.get(name) ?? false
    return { name, entry, exists, displayName, sourceLabel, commitLabel, enabled }
  })

  const nameWidth = Math.max(8, ...rows.map((row) => row.displayName.length)) + 2
  const sourceWidth = Math.max(8, ...rows.map((row) => row.sourceLabel.length)) + 2
  const commitWidth = Math.max(6, ...rows.map((row) => row.commitLabel.length)) + 2
  const enabledWidth = Math.max("enabled".length, "disabled".length) + 2
  const updateWidth =
    Math.max(
      "— local".length,
      "⋯".length,
      "✓ up to date".length,
      "↑ update available".length,
      "✗ check failed".length,
    ) + 2

  const formatRow = (row, updateLabel, updateText) => {
    const statusIcon = row.exists ? styleText("green", "✓") : styleText("red", "✗")
    const enabledText = row.enabled ? "enabled" : "disabled"
    const enabledLabel = row.enabled
      ? styleText("green", enabledText)
      : styleText("gray", enabledText)
    const enabledColumn = `${enabledLabel}${" ".repeat(enabledWidth - enabledText.length)}`
    const updateColumn = `${updateLabel}${" ".repeat(Math.max(0, updateWidth - updateText.length))}`
    return `  ${statusIcon} ${row.displayName.padEnd(nameWidth)}${row.sourceLabel.padEnd(
      sourceWidth,
    )}${row.commitLabel.padEnd(commitWidth)}${enabledColumn}${updateColumn}`
  }

  const updateDisplay = (status) => {
    switch (status) {
      case "local":
        return { text: "— local", label: styleText("gray", "— local") }
      case "up_to_date":
        return { text: "✓ up to date", label: styleText("green", "✓ up to date") }
      case "update_available":
        return { text: "↑ update available", label: styleText("yellow", "↑ update available") }
      case "failed":
        return { text: "✗ check failed", label: styleText("red", "✗ check failed") }
      default:
        return { text: "⋯", label: styleText("cyan", "⋯") }
    }
  }

  const isTTY = process.stdout.isTTY

  const updateLine = (index, updateLabel, updateText) => {
    if (!isTTY) return
    const offset = rows.length - index
    process.stdout.write(
      `\x1b[${offset}A\x1b[2K\r${formatRow(rows[index], updateLabel, updateText)}\x1b[${offset}B`,
    )
  }

  if (isTTY) {
    console.log(styleText("bold", "Installed Plugins:"))
    console.log()
    for (const row of rows) {
      const display =
        row.entry.commit === "local" ? updateDisplay("local") : updateDisplay("checking")
      console.log(formatRow(row, display.label, display.text))
    }
  }

  const promises = rows.map((row, index) => {
    if (row.entry.commit === "local") {
      return Promise.resolve({
        index,
        status: "local",
        name: row.displayName,
      })
    }

    const lsRemoteRef = row.entry.ref ? `refs/heads/${row.entry.ref}` : "HEAD"
    return execAsync(`git ls-remote "${row.entry.resolved}" ${lsRemoteRef}`)
      .then(({ stdout }) => {
        const latestCommit = stdout.split("\t")[0].trim()
        const status = latestCommit === row.entry.commit ? "up_to_date" : "update_available"
        const display = updateDisplay(status)
        updateLine(index, display.label, display.text)
        return { index, status, name: row.displayName }
      })
      .catch(() => {
        const display = updateDisplay("failed")
        updateLine(index, display.label, display.text)
        return { index, status: "failed", name: row.displayName }
      })
  })

  const results = await Promise.all(promises)
  const updatesAvailable = results
    .filter((result) => result.status === "update_available")
    .map((result) => result.name)
  const failedChecks = results
    .filter((result) => result.status === "failed")
    .map((result) => result.name)

  if (!isTTY) {
    console.log(styleText("bold", "Installed Plugins:"))
    console.log()
    for (const result of results) {
      const row = rows[result.index]
      const display = updateDisplay(result.status)
      console.log(formatRow(row, display.label, display.text))
    }
  }

  if (updatesAvailable.length === 0 && failedChecks.length === 0) {
    console.log(styleText("green", "\n✓ All plugins up to date"))
    return
  }

  if (updatesAvailable.length > 0) {
    console.log(styleText("yellow", `\nUpdates available: ${updatesAvailable.join(", ")}`))
    console.log(styleText("gray", "Run 'npx quartz plugin install --latest' to update."))
  }

  if (failedChecks.length > 0) {
    console.log(styleText("red", `\nChecks failed: ${failedChecks.join(", ")}`))
  }
}

export async function handlePluginRestore() {
  return handlePluginInstallUnified({ clean: true })
}

export async function handlePluginPrune({ dryRun = false } = {}) {
  const lockfile = readLockfile()
  if (!lockfile || Object.keys(lockfile.plugins).length === 0) {
    console.log(styleText("gray", "No plugins installed"))
    return
  }

  const pluginsJson = readPluginsJson()
  const configuredNames = new Set(
    (pluginsJson?.plugins ?? []).map((entry) => extractPluginName(entry.source)),
  )

  const orphans = Object.keys(lockfile.plugins).filter((name) => !configuredNames.has(name))

  if (orphans.length === 0) {
    console.log(styleText("green", "✓ No orphaned plugins found — nothing to prune"))
    return
  }

  console.log(`Found ${orphans.length} orphaned plugin(s):\n`)
  for (const name of orphans) {
    console.log(`  ${styleText("yellow", name)} — in lockfile but not in config`)
  }
  console.log()

  if (dryRun) {
    console.log(styleText("cyan", "Dry run — no changes made. Re-run without --dry-run to prune."))
    return
  }

  let removed = 0
  for (const name of orphans) {
    const pluginDir = path.join(PLUGINS_DIR, name)

    console.log(styleText("cyan", `→ Removing ${name}...`))

    if (fs.existsSync(pluginDir)) {
      fs.rmSync(pluginDir, { recursive: true })
    }

    delete lockfile.plugins[name]
    console.log(styleText("green", `✓ Removed ${name}`))
    removed++
  }

  if (removed > 0) {
    await regeneratePluginIndex()
  }

  writeLockfile(lockfile)
  console.log()
  console.log(styleText("green", `✓ Pruned ${removed} plugin(s)`))
  console.log(styleText("gray", "Updated quartz.lock.json"))
}

export async function handlePluginResolve({ dryRun = false } = {}) {
  return handlePluginInstallUnified({ fromConfig: true, dryRun })
}
