import { styleText } from "util"
import {
  PluginManifest,
  PluginCategory,
  LoadedPlugin,
  PluginResolution,
  PluginResolutionError,
  PluginResolutionOptions,
  PluginSpecifier,
} from "./types"
import {
  QuartzTransformerPlugin,
  QuartzFilterPlugin,
  QuartzEmitterPlugin,
  QuartzPageTypePlugin,
} from "../types"
import {
  parsePluginSource,
  installPlugin,
  getPluginEntryPoint,
  toFileUrl,
  isLocalSource,
  validatePluginExternals,
} from "./gitLoader"

const MINIMUM_QUARTZ_VERSION = "4.5.0"

function satisfiesVersion(required: string | undefined, current: string): boolean {
  if (!required) return true

  const parseVersion = (v: string) => {
    const parts = v.replace(/^v/, "").split(".")
    return {
      major: parseInt(parts[0]) || 0,
      minor: parseInt(parts[1]) || 0,
      patch: parseInt(parts[2]) || 0,
    }
  }

  const req = parseVersion(required)
  const cur = parseVersion(current)

  if (cur.major > req.major) return true
  if (cur.major < req.major) return false
  if (cur.minor > req.minor) return true
  if (cur.minor < req.minor) return false
  return cur.patch >= req.patch
}

async function tryImportPlugin(packageName: string): Promise<{
  module: unknown
  manifest: PluginManifest | null
}> {
  try {
    const module = await import(packageName)

    const manifest: PluginManifest | null = module.manifest ?? null

    return { module, manifest }
  } catch (error) {
    throw new Error(
      `Failed to import package: ${error instanceof Error ? error.message : String(error)}`,
    )
  }
}

function detectPluginType(
  module: unknown,
): "transformer" | "filter" | "emitter" | "pageType" | null {
  if (!module || typeof module !== "object") return null

  const mod = module as Record<string, unknown>

  if (typeof mod.default === "function") {
    return null
  }

  const hasPageTypeProps = ["match", "body", "layout"].every((key) => key in mod)

  const hasTransformerProps = ["textTransform", "markdownPlugins", "htmlPlugins"].some(
    (key) => key in mod && (typeof mod[key] === "function" || mod[key] === undefined),
  )

  const hasFilterProps = ["shouldPublish"].some(
    (key) => key in mod && typeof mod[key] === "function",
  )

  const hasEmitterProps = ["emit"].some((key) => key in mod && typeof mod[key] === "function")

  if (hasPageTypeProps) return "pageType"
  if (hasEmitterProps) return "emitter"
  if (hasFilterProps) return "filter"
  if (hasTransformerProps) return "transformer"

  return null
}

function extractPluginFactory(
  module: unknown,
  type: "transformer" | "filter" | "emitter" | "pageType",
):
  | QuartzTransformerPlugin
  | QuartzFilterPlugin
  | QuartzEmitterPlugin
  | QuartzPageTypePlugin
  | null {
  if (!module || typeof module !== "object") return null

  const mod = module as Record<string, unknown>

  const factory = mod.default ?? mod[type] ?? mod.plugin ?? null

  if (typeof factory === "function") {
    return factory as
      | QuartzTransformerPlugin
      | QuartzFilterPlugin
      | QuartzEmitterPlugin
      | QuartzPageTypePlugin
  }

  return null
}

function isGitSource(source: string): boolean {
  // Check if it's a Git-based or local file path source
  return (
    isLocalSource(source) ||
    source.startsWith("github:") ||
    source.startsWith("git+") ||
    source.startsWith("https://github.com/") ||
    source.startsWith("https://gitlab.com/") ||
    source.startsWith("https://bitbucket.org/")
  )
}

async function resolveSinglePlugin(
  specifier: PluginSpecifier,
  options: PluginResolutionOptions,
): Promise<{ plugin: LoadedPlugin | null; error: PluginResolutionError | null }> {
  let packageName: string
  let manifest: Partial<PluginManifest> = {}
  let pluginSource = "npm"

  if (typeof specifier === "string") {
    packageName = specifier
    // Check if it's a Git-based source
    if (isGitSource(specifier)) {
      pluginSource = "git"
    }
  } else if ("name" in specifier) {
    packageName = specifier.name
    if (isGitSource(specifier.name)) {
      pluginSource = "git"
    }
  } else if ("plugin" in specifier) {
    const rawType = specifier.manifest?.category ?? "transformer"
    const type = Array.isArray(rawType) ? rawType[0] : rawType
    return {
      plugin: {
        plugin: specifier.plugin as QuartzTransformerPlugin,
        manifest: {
          name: specifier.manifest?.name ?? "inline-plugin",
          displayName: specifier.manifest?.displayName ?? "Inline Plugin",
          description: specifier.manifest?.description ?? "Inline plugin instance",
          version: specifier.manifest?.version ?? "1.0.0",
          category: rawType,
          ...specifier.manifest,
        } as PluginManifest,
        type,
        source: "inline",
      },
      error: null,
    }
  } else {
    return {
      plugin: null,
      error: {
        plugin: "unknown",
        message: "Invalid plugin specifier format",
        type: "invalid-manifest",
      },
    }
  }

  if (pluginSource === "git") {
    try {
      const gitSpec = parsePluginSource(packageName)
      await installPlugin(gitSpec, { verbose: options.verbose })
      const entryPoint = getPluginEntryPoint(gitSpec.name)

      // Import the plugin
      const module = await import(toFileUrl(entryPoint))
      const importedManifest: PluginManifest | null = module.manifest ?? null

      validatePluginExternals(gitSpec.name, entryPoint, { verbose: options.verbose })

      manifest = importedManifest ?? {}

      const categoryOrCategories = manifest.category ?? detectPluginType(module)

      if (!categoryOrCategories) {
        return {
          plugin: null,
          error: {
            plugin: packageName,
            message: "Could not detect plugin type from Git source",
            type: "invalid-manifest",
          },
        }
      }

      // Normalize to single processing category for factory extraction
      const processingCategories = ["transformer", "filter", "emitter", "pageType"] as const
      type ProcessingCategory = (typeof processingCategories)[number]
      const detectedType: PluginCategory = Array.isArray(categoryOrCategories)
        ? categoryOrCategories[0]
        : categoryOrCategories
      const processingType: ProcessingCategory | undefined = Array.isArray(categoryOrCategories)
        ? (categoryOrCategories.find((c) =>
            (processingCategories as readonly string[]).includes(c),
          ) as ProcessingCategory | undefined)
        : (processingCategories as readonly string[]).includes(categoryOrCategories)
          ? (categoryOrCategories as ProcessingCategory)
          : undefined

      // Component-only plugins don't have a processing factory
      if (!processingType) {
        const fullManifest: PluginManifest = {
          name: manifest.name ?? gitSpec.name,
          displayName: manifest.displayName ?? gitSpec.name,
          description: manifest.description ?? "No description provided",
          version: manifest.version ?? "1.0.0",
          author: manifest.author,
          homepage: manifest.homepage,
          keywords: manifest.keywords,
          category: manifest.category ?? detectedType,
          quartzVersion: manifest.quartzVersion,
          configSchema: manifest.configSchema,
        }

        if (options.verbose) {
          console.log(
            styleText("green", `\u2713`) +
              ` Loaded ${detectedType} plugin: ${styleText("cyan", fullManifest.displayName)}@${fullManifest.version} ${styleText("gray", `(from ${gitSpec.repo})`)}`,
          )
        }

        return { plugin: null, error: null }
      }

      const factory = extractPluginFactory(module, processingType)
      if (!factory) {
        return {
          plugin: null,
          error: {
            plugin: packageName,
            message: "Could not find plugin factory in Git source",
            type: "invalid-manifest",
          },
        }
      }

      const fullManifest: PluginManifest = {
        name: manifest.name ?? gitSpec.name,
        displayName: manifest.displayName ?? gitSpec.name,
        description: manifest.description ?? "No description provided",
        version: manifest.version ?? "1.0.0",
        author: manifest.author,
        homepage: manifest.homepage,
        keywords: manifest.keywords,
        category: manifest.category ?? detectedType,
        quartzVersion: manifest.quartzVersion,
        configSchema: manifest.configSchema,
      }

      const loadedPlugin: LoadedPlugin = {
        plugin: factory,
        manifest: fullManifest,
        type: detectedType,
        source: gitSpec.local ? `local:${gitSpec.repo}` : `${gitSpec.repo}#${gitSpec.ref}`,
      }

      if (options.verbose) {
        console.log(
          styleText("green", `✓`) +
            ` Loaded ${detectedType} plugin: ${styleText("cyan", fullManifest.displayName)}@${fullManifest.version} ${styleText("gray", `(from ${gitSpec.repo})`)}`,
        )
      }

      return { plugin: loadedPlugin, error: null }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error)
      return {
        plugin: null,
        error: {
          plugin: packageName,
          message: `Failed to load Git plugin: ${errorMessage}`,
          type: "import-error",
        },
      }
    }
  }

  try {
    const { module: importedModule, manifest: importedManifest } =
      await tryImportPlugin(packageName)

    manifest = importedManifest ?? {}

    // Load components if the plugin declares any
    if (manifest.components && Object.keys(manifest.components).length > 0) {
      const { loadComponentsFromPackage } = await import("./componentLoader")
      await loadComponentsFromPackage(packageName, manifest as PluginManifest)
    }

    const categoryOrCategories = manifest.category ?? detectPluginType(importedModule)

    if (!categoryOrCategories) {
      return {
        plugin: null,
        error: {
          plugin: packageName,
          message: `Could not detect plugin type. Ensure the plugin exports a valid factory function or has a 'category' field in its manifest.`,
          type: "invalid-manifest",
        },
      }
    }

    // Normalize to single processing category for factory extraction
    const processingCategories = ["transformer", "filter", "emitter", "pageType"] as const
    type ProcessingCategory = (typeof processingCategories)[number]
    const detectedType: PluginCategory = Array.isArray(categoryOrCategories)
      ? categoryOrCategories[0]
      : categoryOrCategories
    const processingType: ProcessingCategory | undefined = Array.isArray(categoryOrCategories)
      ? (categoryOrCategories.find((c) =>
          (processingCategories as readonly string[]).includes(c),
        ) as ProcessingCategory | undefined)
      : (processingCategories as readonly string[]).includes(categoryOrCategories)
        ? (categoryOrCategories as ProcessingCategory)
        : undefined

    if (
      manifest.quartzVersion &&
      !satisfiesVersion(manifest.quartzVersion, options.quartzVersion)
    ) {
      return {
        plugin: null,
        error: {
          plugin: packageName,
          message: `Plugin requires Quartz ${manifest.quartzVersion} but current version is ${options.quartzVersion}`,
          type: "version-mismatch",
        },
      }
    }

    // Component-only plugins don't have a processing factory
    if (!processingType) {
      const fullManifest: PluginManifest = {
        name: manifest.name ?? packageName,
        displayName: manifest.displayName ?? packageName,
        description: manifest.description ?? "No description provided",
        version: manifest.version ?? "1.0.0",
        author: manifest.author,
        homepage: manifest.homepage,
        keywords: manifest.keywords,
        category: manifest.category ?? detectedType,
        quartzVersion: manifest.quartzVersion,
        configSchema: manifest.configSchema,
      }

      if (options.verbose) {
        console.log(
          styleText("green", `\u2713`) +
            ` Loaded ${detectedType} plugin: ${styleText("cyan", fullManifest.displayName)}@${fullManifest.version}`,
        )
      }

      return { plugin: null, error: null }
    }

    const factory = extractPluginFactory(importedModule, processingType)
    if (!factory) {
      return {
        plugin: null,
        error: {
          plugin: packageName,
          message: `Could not find plugin factory in module. Expected 'export default' or '${processingType}' export.`,
          type: "invalid-manifest",
        },
      }
    }

    const fullManifest: PluginManifest = {
      name: manifest.name ?? packageName,
      displayName: manifest.displayName ?? packageName,
      description: manifest.description ?? "No description provided",
      version: manifest.version ?? "1.0.0",
      author: manifest.author,
      homepage: manifest.homepage,
      keywords: manifest.keywords,
      category: manifest.category ?? detectedType,
      quartzVersion: manifest.quartzVersion,
      configSchema: manifest.configSchema,
    }

    const loadedPlugin: LoadedPlugin = {
      plugin: factory,
      manifest: fullManifest,
      type: detectedType,
      source: packageName,
    }

    if (options.verbose) {
      console.log(
        styleText("green", `✓`) +
          ` Loaded ${detectedType} plugin: ${styleText("cyan", fullManifest.displayName)}@${fullManifest.version}`,
      )
    }

    return { plugin: loadedPlugin, error: null }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error)

    if (errorMessage.includes("Cannot find module") || errorMessage.includes("MODULE_NOT_FOUND")) {
      return {
        plugin: null,
        error: {
          plugin: packageName,
          message: `Plugin package not found. Run 'npm install ${packageName}' to install it.`,
          type: "not-found",
        },
      }
    }

    return {
      plugin: null,
      error: {
        plugin: packageName,
        message: errorMessage,
        type: "import-error",
      },
    }
  }
}

export async function resolvePlugins(
  specifiers: PluginSpecifier[],
  options: PluginResolutionOptions,
): Promise<PluginResolution> {
  const plugins: LoadedPlugin[] = []
  const errors: PluginResolutionError[] = []

  if (options.verbose) {
    console.log(styleText("cyan", `Resolving ${specifiers.length} external plugin(s)...`))
  }

  for (const specifier of specifiers) {
    const { plugin, error } = await resolveSinglePlugin(specifier, options)

    if (plugin) {
      plugins.push(plugin)
    } else if (error) {
      errors.push(error)
      console.error(
        styleText("red", `✗`) +
          ` Failed to load plugin: ${styleText("yellow", error.plugin)}\n` +
          `  ${error.message}`,
      )
    }
  }

  if (options.verbose && plugins.length > 0) {
    const byType = plugins.reduce(
      (acc, p) => {
        acc[p.type] = (acc[p.type] || 0) + 1
        return acc
      },
      {} as Record<string, number>,
    )

    console.log(
      styleText("cyan", `External plugins loaded:`) +
        ` ${byType.transformer ?? 0} transformers, ${byType.filter ?? 0} filters, ${byType.emitter ?? 0} emitters, ${byType.pageType ?? 0} pageTypes`,
    )
  }

  return { plugins, errors }
}

export function instantiatePlugin<T>(
  loadedPlugin: LoadedPlugin,
  options?: T,
): ReturnType<typeof loadedPlugin.plugin> {
  const factory = loadedPlugin.plugin as (opts?: T) => ReturnType<typeof loadedPlugin.plugin>
  return factory(options)
}

export { satisfiesVersion, MINIMUM_QUARTZ_VERSION }
