import fs from "fs"
import path from "path"
import YAML from "yaml"
import { styleText } from "util"
import { QuartzConfig, GlobalConfiguration, FullPageLayout } from "../../cfg"
import { QuartzComponent, QuartzComponentConstructor } from "../../components/types"
import { PluginTypes } from "../types"
import {
  PluginManifest,
  PluginJsonEntry,
  PluginSource,
  QuartzPluginsJson,
  LayoutConfig,
  PluginLayoutDeclaration,
  FlexGroupConfig,
} from "./types"
import {
  parsePluginSource,
  installPlugin,
  installNativeDeps,
  getPluginEntryPoint,
  toFileUrl,
  isLocalSource,
} from "./gitLoader"
import { loadComponentsFromPackage } from "./componentLoader"
import { loadFramesFromPackage } from "./frameLoader"
import { componentRegistry } from "../../components/registry"
import { getCondition } from "./conditions"

const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml")
const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml")
const LEGACY_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
const LEGACY_DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json")

function resolveConfigPath(): string {
  if (fs.existsSync(CONFIG_YAML_PATH)) return CONFIG_YAML_PATH
  if (fs.existsSync(LEGACY_PLUGINS_JSON_PATH)) return LEGACY_PLUGINS_JSON_PATH
  if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH
  if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH
  return CONFIG_YAML_PATH
}
function readPluginsJson(): QuartzPluginsJson | null {
  const configPath = resolveConfigPath()
  if (!fs.existsSync(configPath)) {
    return null
  }
  const raw = fs.readFileSync(configPath, "utf-8")
  if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
    return YAML.parse(raw) as QuartzPluginsJson
  }
  return JSON.parse(raw) as QuartzPluginsJson
}

function extractPluginName(source: PluginSource): string {
  if (typeof source === "object" && source !== null) {
    if (source.name) return source.name
    return extractPluginName(source.repo)
  }

  if (isLocalSource(source)) {
    return path.basename(source.replace(/[\/]+$/, ""))
  }
  if (source.startsWith("github:")) {
    const withoutPrefix = source.replace("github:", "")
    const [repoPath] = withoutPrefix.split("#")
    const parts = repoPath.split("/")
    return parts[parts.length - 1]
  }
  if (source.startsWith("git+") || source.startsWith("https://")) {
    const url = source.replace("git+", "")
    const match = url.match(/\/([^/]+?)(?:\.git)?(?:#|$)/)
    return match?.[1] ?? source
  }
  return source
}

function formatSourceDisplay(source: PluginSource): string {
  if (typeof source === "string") return source
  const parts = [source.repo]
  if (source.subdir) parts.push(`(subdir: ${source.subdir})`)
  if (source.ref) parts.push(`(ref: ${source.ref})`)
  return parts.join(" ")
}

function sourceKey(source: PluginSource): string {
  if (typeof source === "string") return source
  return JSON.stringify(source)
}

interface DependencyValidationResult {
  errors: string[]
  warnings: string[]
}

function validateDependencies(
  entries: PluginJsonEntry[],
  manifests: Map<string, PluginManifest>,
): DependencyValidationResult {
  const errors: string[] = []
  const warnings: string[] = []

  const sourceToEntry = new Map<string, PluginJsonEntry>()
  const nameToSource = new Map<string, string>()
  for (const entry of entries) {
    sourceToEntry.set(sourceKey(entry.source), entry)
    nameToSource.set(extractPluginName(entry.source), sourceKey(entry.source))
  }

  for (const entry of entries) {
    if (!entry.enabled) continue
    const manifest = manifests.get(sourceKey(entry.source))
    if (!manifest?.dependencies?.length) continue

    const pluginName = manifest.displayName || extractPluginName(entry.source)
    const pluginOrder = entry.order ?? manifest.defaultOrder ?? 50

    for (const dep of manifest.dependencies) {
      const depEntry = sourceToEntry.get(dep)
      const depName = extractPluginName(dep)

      if (!depEntry) {
        errors.push(
          `Plugin "${pluginName}" requires "${depName}". Run: npx quartz plugin add ${dep}`,
        )
        continue
      }

      if (!depEntry.enabled) {
        warnings.push(
          `Plugin "${pluginName}" depends on "${depName}" which is disabled. "${pluginName}" may not function correctly.`,
        )
      }

      const depManifest = manifests.get(dep)
      const depOrder = depEntry.order ?? depManifest?.defaultOrder ?? 50

      if (pluginOrder < depOrder) {
        errors.push(
          `Plugin "${pluginName}" (order: ${pluginOrder}) depends on "${depName}" (order: ${depOrder}), ` +
            `but "${pluginName}" is configured to run first. Either increase "${pluginName}"'s order above ${depOrder} ` +
            `or decrease "${depName}"'s order below ${pluginOrder}.`,
        )
      }
    }
  }

  const graph = new Map<string, string[]>()
  for (const entry of entries) {
    const manifest = manifests.get(sourceKey(entry.source))
    if (manifest?.dependencies?.length) {
      graph.set(sourceKey(entry.source), manifest.dependencies)
    }
  }

  const visited = new Set<string>()
  const inStack = new Set<string>()

  function detectCycle(node: string, pathSoFar: string[]): string[] | null {
    if (inStack.has(node)) {
      const cycleStart = pathSoFar.indexOf(node)
      return pathSoFar.slice(cycleStart).concat(node)
    }
    if (visited.has(node)) return null

    visited.add(node)
    inStack.add(node)

    for (const dep of graph.get(node) ?? []) {
      const cycle = detectCycle(dep, [...pathSoFar, node])
      if (cycle) return cycle
    }

    inStack.delete(node)
    return null
  }

  for (const node of graph.keys()) {
    const cycle = detectCycle(node, [])
    if (cycle) {
      const names = cycle.map(extractPluginName)
      errors.push(`Circular dependency detected: ${names.join(" → ")}`)
      break
    }
  }

  return { errors, warnings }
}

async function resolvePluginManifest(source: PluginSource): Promise<PluginManifest | null> {
  try {
    const gitSpec = parsePluginSource(source)
    const entryPoint = getPluginEntryPoint(gitSpec.name)
    const module = await import(toFileUrl(entryPoint))
    return module.manifest ?? null
  } catch {
    return null
  }
}

async function readManifestFromPackageJson(source: PluginSource): Promise<PluginManifest | null> {
  try {
    const gitSpec = parsePluginSource(source)
    const pluginDir = path.join(process.cwd(), ".quartz", "plugins", gitSpec.name)
    const pkgPath = path.join(pluginDir, "package.json")
    if (!fs.existsSync(pkgPath)) return null

    const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
    if (!pkg.quartz) return null

    const q = pkg.quartz
    return {
      name: q.name ?? gitSpec.name,
      displayName: q.displayName ?? q.name ?? gitSpec.name,
      description: q.description ?? pkg.description ?? "No description",
      version: q.version ?? pkg.version ?? "1.0.0",
      author: q.author ?? pkg.author,
      homepage: q.homepage ?? pkg.homepage,
      category: q.category,
      quartzVersion: q.quartzVersion,
      dependencies: q.dependencies,
      defaultOrder: q.defaultOrder,
      defaultEnabled: q.defaultEnabled,
      defaultOptions: q.defaultOptions,
      configSchema: q.configSchema,
      components: q.components,
      frames: q.frames,
    }
  } catch {
    return null
  }
}

async function getManifest(source: PluginSource): Promise<PluginManifest | null> {
  // Try package.json quartz field first (preferred), then fall back to manifest.ts export
  return (await readManifestFromPackageJson(source)) ?? (await resolvePluginManifest(source))
}

export async function loadQuartzConfig(
  configOverrides?: Partial<GlobalConfiguration>,
): Promise<QuartzConfig> {
  const json = readPluginsJson()

  if (!json) {
    // Fallback: import old-style config directly
    const oldConfig = await import("../../../quartz")
    return oldConfig.default
  }

  const configuration = {
    ...(json.configuration as unknown as GlobalConfiguration),
    ...configOverrides,
  }

  const enabledEntries = json.plugins.filter((e) => e.enabled)
  const manifests = new Map<string, PluginManifest>()

  // Ensure all plugins are installed and collect native deps
  const allNativeDeps = new Map<string, Map<string, string>>()
  for (const entry of enabledEntries) {
    try {
      const gitSpec = parsePluginSource(entry.source)
      const result = await installPlugin(gitSpec, { verbose: false })
      if (result.nativeDeps.size > 0) {
        allNativeDeps.set(gitSpec.name, result.nativeDeps)
      }
    } catch (err) {
      console.error(
        styleText("red", `✗`) +
          ` Failed to install plugin: ${styleText("yellow", formatSourceDisplay(entry.source))}\n` +
          `  ${err instanceof Error ? err.message : String(err)}`,
      )
    }
  }

  if (allNativeDeps.size > 0) {
    installNativeDeps(allNativeDeps, { verbose: false })
  }

  // Collect manifests (requires native deps to be installed first)
  for (const entry of enabledEntries) {
    try {
      const manifest = await getManifest(entry.source)
      if (manifest) {
        manifests.set(sourceKey(entry.source), manifest)
      }
    } catch (err) {
      console.error(
        styleText("red", `✗`) +
          ` Failed to load manifest: ${styleText("yellow", formatSourceDisplay(entry.source))}\n` +
          `  ${err instanceof Error ? err.message : String(err)}`,
      )
    }
  }

  // Validate dependencies
  const validation = validateDependencies(enabledEntries, manifests)
  for (const warning of validation.warnings) {
    console.warn(styleText("yellow", `⚠`) + ` ${warning}`)
  }
  if (validation.errors.length > 0) {
    for (const error of validation.errors) {
      console.error(styleText("red", `✗`) + ` ${error}`)
    }
    throw new Error(
      `Plugin dependency validation failed with ${validation.errors.length} error(s). See above for details.`,
    )
  }

  // Categorize and sort plugins
  const transformers: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []
  const filters: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []
  const emitters: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []
  const pageTypes: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []

  for (const entry of enabledEntries) {
    const manifest = manifests.get(sourceKey(entry.source))
    const category = manifest?.category
    // Resolve processing categories: for array categories (e.g. ["transformer", "pageType", "component"]),
    // push the plugin into ALL matching processing category buckets.
    // "component" is handled separately via loadComponentsFromPackage during instantiation.
    const processingCategories = ["transformer", "filter", "emitter", "pageType"] as const
    const categoryMap: Record<string, typeof transformers> = {
      transformer: transformers,
      filter: filters,
      emitter: emitters,
      pageType: pageTypes,
    }

    const categories = Array.isArray(category) ? category : category ? [category] : []
    const matchedProcessing = categories.filter((c) =>
      (processingCategories as readonly string[]).includes(c),
    )

    if (matchedProcessing.length > 0) {
      for (const cat of matchedProcessing) {
        categoryMap[cat].push({ entry, manifest })
      }
    } else {
      const gitSpec = parsePluginSource(entry.source)
      const isComponentOnly = categories.length > 0 && categories.every((c) => c === "component")

      if (isComponentOnly) {
        // Always import the main entry point for component-only plugins.
        // Some plugins (e.g. Bases view registrations) rely on side effects
        // in their index module to register functionality.
        const entryPoint = getPluginEntryPoint(gitSpec.name)
        try {
          const module = await import(toFileUrl(entryPoint))
          // If the module exports an init() function, call it with merged options
          // so component-only plugins can receive user configuration from YAML.
          if (typeof module.init === "function") {
            const initOverrides = componentRegistry.getOptionOverrides(gitSpec.name)
            const options = { ...manifest?.defaultOptions, ...entry.options, ...initOverrides }
            await module.init(Object.keys(options).length > 0 ? options : undefined)
          }
        } catch (e) {
          // Side-effect import failed — continue with manifest-based loading
        }
        if (manifest?.components && Object.keys(manifest.components).length > 0) {
          await loadComponentsFromPackage(gitSpec.name, manifest)
        }
        if (manifest?.frames && Object.keys(manifest.frames).length > 0) {
          await loadFramesFromPackage(gitSpec.name, manifest)
        }
      } else {
        const entryPoint = getPluginEntryPoint(gitSpec.name)
        try {
          const module = await import(toFileUrl(entryPoint))
          const detected = detectCategoryFromModule(module)
          if (detected) {
            categoryMap[detected].push({ entry, manifest })
          } else if (manifest?.components && Object.keys(manifest.components).length > 0) {
            await loadComponentsFromPackage(gitSpec.name, manifest)
            if (manifest?.frames && Object.keys(manifest.frames).length > 0) {
              await loadFramesFromPackage(gitSpec.name, manifest)
            }
          } else {
            console.warn(
              styleText("yellow", `⚠`) +
                ` Could not determine category for plugin "${extractPluginName(entry.source)}". Skipping.`,
            )
          }
        } catch {
          const hasComponents = manifest?.components && Object.keys(manifest.components).length > 0
          const hasFrames = manifest?.frames && Object.keys(manifest.frames).length > 0
          if (hasComponents) {
            await loadComponentsFromPackage(gitSpec.name, manifest)
          }
          if (hasFrames) {
            await loadFramesFromPackage(gitSpec.name, manifest)
          }
          if (!hasComponents && !hasFrames) {
            console.warn(
              styleText("yellow", `⚠`) +
                ` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`,
            )
          }
        }
      }
    }
  }

  // Sort by order within each category
  const sortByOrder = (
    a: { entry: PluginJsonEntry; manifest: PluginManifest | undefined },
    b: { entry: PluginJsonEntry; manifest: PluginManifest | undefined },
  ) => {
    const orderA = a.entry.order ?? a.manifest?.defaultOrder ?? 50
    const orderB = b.entry.order ?? b.manifest?.defaultOrder ?? 50
    return orderA - orderB
  }

  transformers.sort(sortByOrder)
  filters.sort(sortByOrder)
  emitters.sort(sortByOrder)
  pageTypes.sort(sortByOrder)

  // Instantiate plugins
  const instantiate = async (
    items: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[],
    expectedCategory: ProcessingCategory,
  ) => {
    const instances = []
    for (const { entry, manifest } of items) {
      try {
        const gitSpec = parsePluginSource(entry.source)
        const entryPoint = getPluginEntryPoint(gitSpec.name)
        const module = await import(toFileUrl(entryPoint))
        if (manifest?.components && Object.keys(manifest.components).length > 0) {
          await loadComponentsFromPackage(gitSpec.name, manifest)
        }
        if (manifest?.frames && Object.keys(manifest.frames).length > 0) {
          await loadFramesFromPackage(gitSpec.name, manifest)
        }

        const factory = findFactory(module, expectedCategory)
        if (!factory) {
          console.warn(
            styleText("yellow", `⚠`) +
              ` Plugin "${extractPluginName(entry.source)}" has no factory function for category "${expectedCategory}". Skipping.`,
          )
          continue
        }
        const pluginOverrides = componentRegistry.getOptionOverrides(gitSpec.name)
        const options = { ...manifest?.defaultOptions, ...entry.options, ...pluginOverrides }
        instances.push(factory(Object.keys(options).length > 0 ? options : undefined))
      } catch (err) {
        console.error(
          styleText("red", `✗`) +
            ` Failed to instantiate plugin "${extractPluginName(entry.source)}": ${err instanceof Error ? err.message : String(err)}`,
        )
      }
    }
    return instances
  }

  // Import built-in plugins
  const builtinPlugins = await import("../index")
  const builtinTransformers: unknown[] = []
  const builtinEmitters = [
    builtinPlugins.ComponentResources(),
    builtinPlugins.Assets(),
    builtinPlugins.Static(),
  ]
  const builtinPageTypes = [builtinPlugins.PageTypes.NotFoundPageType()]

  const plugins: PluginTypes = {
    transformers: [...builtinTransformers, ...(await instantiate(transformers, "transformer"))],
    filters: await instantiate(filters, "filter"),
    emitters: [...builtinEmitters, ...(await instantiate(emitters, "emitter"))],
    pageTypes: [...(await instantiate(pageTypes, "pageType")), ...builtinPageTypes],
  }

  // Load layout and add PageTypeDispatcher to emitters.
  // This must happen after plugin instantiation so the component registry is populated.
  const layout = await loadQuartzLayout()
  plugins.emitters.push(
    builtinPlugins.PageTypes.PageTypeDispatcher({
      defaults: layout.defaults,
      byPageType: layout.byPageType,
    }),
  )
  return {
    configuration,
    plugins,
  }
}

type ProcessingCategory = "transformer" | "filter" | "emitter" | "pageType"

function matchesCategory(factory: Function, expected: ProcessingCategory): boolean {
  try {
    const instance = factory()
    if (!instance || typeof instance !== "object") return false
    switch (expected) {
      case "pageType":
        return "match" in instance && "body" in instance && "layout" in instance
      case "emitter":
        return "emit" in instance
      case "filter":
        return "shouldPublish" in instance
      case "transformer":
        return (
          "textTransform" in instance || "markdownPlugins" in instance || "htmlPlugins" in instance
        )
    }
  } catch {
    return false
  }
}

function findFactory(
  module: Record<string, unknown>,
  expectedCategory: ProcessingCategory,
): Function | null {
  if (
    typeof module.default === "function" &&
    matchesCategory(module.default as Function, expectedCategory)
  ) {
    return module.default as Function
  }
  if (
    typeof module.plugin === "function" &&
    matchesCategory(module.plugin as Function, expectedCategory)
  ) {
    return module.plugin as Function
  }

  for (const [, value] of Object.entries(module)) {
    if (typeof value === "function" && matchesCategory(value as Function, expectedCategory)) {
      return value as Function
    }
  }

  return null
}

function detectCategoryFromModule(module: unknown): ProcessingCategory | null {
  if (!module || typeof module !== "object") return null
  const mod = module as Record<string, unknown>

  if (typeof mod.default === "function") {
    // Try to instantiate and inspect
    try {
      const instance = (mod.default as Function)()
      if (instance && typeof instance === "object") {
        if ("match" in instance && "body" in instance && "layout" in instance) return "pageType"
        if ("emit" in instance) return "emitter"
        if ("shouldPublish" in instance) return "filter"
        if (
          "textTransform" in instance ||
          "markdownPlugins" in instance ||
          "htmlPlugins" in instance
        )
          return "transformer"
      }
    } catch {
      // Couldn't instantiate, skip detection
    }
  }

  return null
}

export async function loadQuartzLayout(layoutOverrides?: {
  defaults?: Partial<FullPageLayout>
  byPageType?: Record<string, Partial<FullPageLayout>>
}): Promise<{
  defaults: Partial<FullPageLayout>
  byPageType: Record<string, Partial<FullPageLayout>>
}> {
  const json = readPluginsJson()

  if (!json) {
    // Fallback: import old-style layout directly
    const oldLayout = await import("../../../quartz")
    return oldLayout.layout
  }

  const enabledWithLayout = json.plugins.filter((e) => e.enabled && e.layout)
  const layoutConfig = json.layout ?? {}

  // Build default layout for all page types
  const defaultLayout = buildLayoutForEntries(enabledWithLayout, layoutConfig)

  // Build per-page-type overrides
  const byPageType: Record<string, Partial<FullPageLayout>> = {}
  if (layoutConfig.byPageType) {
    for (const [pageType, override] of Object.entries(layoutConfig.byPageType)) {
      let filteredEntries = enabledWithLayout

      // Apply exclusions
      if (override.exclude?.length) {
        filteredEntries = filteredEntries.filter((e) => {
          const name = extractPluginName(e.source)
          return !override.exclude!.includes(name)
        })
      }

      const ptLayout = buildLayoutForEntries(filteredEntries, layoutConfig)

      // Apply position overrides (empty array = clear position)
      if (override.positions) {
        for (const [pos, components] of Object.entries(override.positions)) {
          if (Array.isArray(components) && components.length === 0) {
            const key = pos as keyof Pick<
              FullPageLayout,
              "left" | "right" | "beforeBody" | "afterBody"
            >
            if (key in ptLayout) {
              ;(ptLayout as Record<string, unknown>)[key] = []
            }
          }
        }
      }

      // Apply frame template override
      if (override.template) {
        ptLayout.frame = override.template
      }

      byPageType[pageType] = ptLayout
    }
  }

  // Add Head (built-in) and Footer (plugin)
  const HeadModule = await import("../../components/Head")
  const head = HeadModule.default()

  // Find footer from component registry (loaded during plugin instantiation)
  const footerEntry = json.plugins.find(
    (e) => e.enabled && extractPluginName(e.source) === "footer",
  )
  let footer: QuartzComponent | undefined
  if (footerEntry) {
    // Try registry lookup: plugin name ("footer") or export name ("Footer")
    const footerReg = componentRegistry.get("footer") ?? componentRegistry.get("Footer")
    if (footerReg) {
      if (typeof footerReg.component === "function" && !("displayName" in footerReg.component)) {
        // It's a constructor — use registry cache for consistent instances
        const footerOverrides = componentRegistry.getOptionOverrides("footer")
        const opts = { ...footerEntry.options, ...footerOverrides }
        footer = componentRegistry.instantiate(
          footerReg.component as QuartzComponentConstructor,
          Object.keys(opts).length > 0 ? opts : undefined,
        )
      } else {
        footer = footerReg.component as QuartzComponent
      }
    }
  }

  // Apply structural defaults
  defaultLayout.head = head
  defaultLayout.header = defaultLayout.header ?? []
  if (footer) {
    defaultLayout.footer = footer
  }

  // Ensure all byPageType entries inherit structural slots
  for (const pageType of Object.keys(byPageType)) {
    const pt = byPageType[pageType]
    if (!pt.head) pt.head = head
    if (!pt.header) pt.header = []
    if (footer && !pt.footer) pt.footer = footer
  }

  const mergedDefaults = { ...defaultLayout, ...layoutOverrides?.defaults }
  const mergedByPageType = { ...byPageType }
  if (layoutOverrides?.byPageType) {
    for (const [pageType, overrideLayout] of Object.entries(layoutOverrides.byPageType)) {
      mergedByPageType[pageType] = { ...mergedByPageType[pageType], ...overrideLayout }
    }
  }

  return { defaults: mergedDefaults, byPageType: mergedByPageType }
}

function buildLayoutForEntries(
  entries: PluginJsonEntry[],
  layoutConfig: LayoutConfig,
): Partial<FullPageLayout> {
  const positions: Record<
    string,
    {
      component: QuartzComponent
      priority: number
      group?: string
      groupOptions?: PluginLayoutDeclaration["groupOptions"]
    }[]
  > = {
    left: [],
    right: [],
    beforeBody: [],
    afterBody: [],
  }

  for (const entry of entries) {
    if (!entry.layout) continue

    const layout = entry.layout
    const name = extractPluginName(entry.source)

    // Look up component from registry
    const registered =
      componentRegistry.get(name) ??
      componentRegistry.get(`${formatSourceDisplay(entry.source)}/${name}`)
    if (!registered) {
      // Try common naming patterns
      const pascalName = name
        .split("-")
        .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
        .join("")
      const altRegistered = componentRegistry.get(pascalName)
      if (!altRegistered) continue
    }

    const reg =
      registered ??
      componentRegistry.get(
        name
          .split("-")
          .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
          .join(""),
      )
    if (!reg) continue

    let component: QuartzComponent
    if (typeof reg.component === "function" && !("displayName" in reg.component)) {
      // It's a constructor — use registry cache to avoid duplicate instances
      // (and duplicate afterDOMLoaded scripts) across page-type layouts
      const tsOverrides = componentRegistry.getOptionOverrides(name)
      const opts = { ...entry.options, ...tsOverrides }
      const optsArg = Object.keys(opts).length > 0 ? opts : undefined
      component = componentRegistry.instantiate(
        reg.component as QuartzComponentConstructor,
        optsArg,
      )
    } else {
      component = reg.component as QuartzComponent
    }

    // Apply display modifier
    if (layout.display && layout.display !== "all") {
      component = applyDisplayWrapper(component, layout.display)
    }

    // Apply condition
    if (layout.condition) {
      component = applyConditionWrapper(component, layout.condition)
    }

    const posArray = positions[layout.position]
    if (posArray) {
      posArray.push({
        component,
        priority: layout.priority,
        group: layout.group,
        groupOptions: layout.groupOptions,
      })
    }
  }

  // Sort by priority and resolve groups
  const result: Partial<FullPageLayout> = {}

  for (const [position, items] of Object.entries(positions)) {
    items.sort((a, b) => a.priority - b.priority)

    const resolved = resolveGroups(items, layoutConfig.groups ?? {})
    const key = position as keyof Pick<
      FullPageLayout,
      "left" | "right" | "beforeBody" | "afterBody"
    >
    ;(result as Record<string, QuartzComponent[]>)[key] = resolved
  }

  return result
}

function resolveGroups(
  items: {
    component: QuartzComponent
    priority: number
    group?: string
    groupOptions?: PluginLayoutDeclaration["groupOptions"]
  }[],
  groups: Record<string, FlexGroupConfig>,
): QuartzComponent[] {
  // Collect grouped components and track the effective priority for each group.
  // Effective priority = explicit group config priority ?? first member's priority.
  const groupedComponents = new Map<
    string,
    { component: QuartzComponent; groupOptions?: PluginLayoutDeclaration["groupOptions"] }[]
  >()
  const groupPriority = new Map<string, number>()

  for (const item of items) {
    if (item.group) {
      if (!groupedComponents.has(item.group)) {
        groupedComponents.set(item.group, [])
        // Use explicit group priority from config if set, otherwise fall back to first member's priority
        const groupConfig = groups[item.group]
        groupPriority.set(item.group, groupConfig?.priority ?? item.priority)
      }
      groupedComponents.get(item.group)!.push({
        component: item.component,
        groupOptions: item.groupOptions,
      })
    }
  }

  // Build a unified list of renderable entries (ungrouped components + flex groups),
  // each with a priority, so we can sort them together.
  type RenderEntry = { priority: number; component: QuartzComponent }
  const entries: RenderEntry[] = []
  const processedGroups = new Set<string>()

  for (const item of items) {
    if (item.group) {
      // Only emit the flex group once (on first encounter)
      if (processedGroups.has(item.group)) continue
      processedGroups.add(item.group)

      const members = groupedComponents.get(item.group)!
      const groupConfig = groups[item.group] ?? {}

      const flexComponents = members.map((m) => ({
        Component: m.component,
        grow: m.groupOptions?.grow,
        shrink: m.groupOptions?.shrink,
        basis: m.groupOptions?.basis,
        order: m.groupOptions?.order,
        align: m.groupOptions?.align,
        justify: m.groupOptions?.justify,
      }))

      // Dynamically import Flex to avoid circular dependencies
      const FlexModule = require("../../components/Flex")
      const Flex = FlexModule.default as Function
      const flexComponent = Flex({
        components: flexComponents,
        direction: groupConfig.direction ?? "row",
        wrap: groupConfig.wrap,
        gap: groupConfig.gap ?? "1rem",
      }) as QuartzComponent

      entries.push({ priority: groupPriority.get(item.group)!, component: flexComponent })
    } else {
      entries.push({ priority: item.priority, component: item.component })
    }
  }

  // Stable sort by priority (items already arrive sorted, so equal priorities preserve order)
  entries.sort((a, b) => a.priority - b.priority)

  return entries.map((e) => e.component)
}

function applyDisplayWrapper(
  component: QuartzComponent,
  display: "mobile-only" | "desktop-only",
): QuartzComponent {
  if (display === "mobile-only") {
    const MobileOnly = require("../../components/MobileOnly").default as Function
    return MobileOnly(component) as QuartzComponent
  } else {
    const DesktopOnly = require("../../components/DesktopOnly").default as Function
    return DesktopOnly(component) as QuartzComponent
  }
}

function applyConditionWrapper(component: QuartzComponent, conditionName: string): QuartzComponent {
  const predicate = getCondition(conditionName)
  if (!predicate) {
    console.warn(
      styleText("yellow", `⚠`) +
        ` Unknown condition "${conditionName}". Component will always render.`,
    )
    return component
  }

  const ConditionalRender = require("../../components/ConditionalRender").default as Function
  return ConditionalRender({
    component,
    condition: predicate,
  }) as QuartzComponent
}
