import * as path from "path"
import * as fs from "fs/promises"
import * as vscode from "vscode"
import * as yaml from "js-yaml"
import { SimpleGit } from "simple-git"
import { validateAnyMetadata } from "./schemas"
import {
	ComponentMetadata,
	ComponentType,
	LocalizationOptions,
	LocalizedMetadata,
	PackageManagerItem,
	PackageMetadata,
} from "./types"
import { getUserLocale } from "./utils"

/**
 * Handles component discovery and metadata loading
 */
export class MetadataScanner {
	private readonly git?: SimpleGit
	private localizationOptions: LocalizationOptions
	private originalRootDir: string | null = null

	constructor(git?: SimpleGit, localizationOptions?: LocalizationOptions) {
		this.git = git
		this.localizationOptions = localizationOptions || {
			userLocale: getUserLocale(),
			fallbackLocale: "en",
		}
	}

	/**
	 * Scans a directory for components
	 * @param rootDir The root directory to scan
	 * @param repoUrl The repository URL
	 * @param sourceName Optional source repository name
	 * @returns Array of discovered items
	 */
	async scanDirectory(
		rootDir: string,
		repoUrl: string,
		sourceName?: string,
		isRecursiveCall: boolean = false,
	): Promise<PackageManagerItem[]> {
		const items: PackageManagerItem[] = []

		// Only set originalRootDir on the first call, not recursive calls
		if (!isRecursiveCall && !this.originalRootDir) {
			this.originalRootDir = rootDir
		}

		try {
			const entries = await fs.readdir(rootDir, { withFileTypes: true })

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

				const componentDir = path.join(rootDir, entry.name)
				// Always calculate paths relative to the original root directory
				const relativePath = path.relative(this.originalRootDir || rootDir, componentDir).replace(/\\/g, "/")
				const metadata = await this.loadComponentMetadata(componentDir)
				// If no metadata found, or metadata validation fails, try recursing
				if (!metadata || !this.getLocalizedMetadata(metadata)) {
					// Pass the current directory as the root for this recursive call
					const subItems = await this.scanDirectory(componentDir, repoUrl, sourceName, true)
					items.push(...subItems)
					continue
				}

				// Get localized metadata with fallback
				const localizedMetadata = this.getLocalizedMetadata(metadata)
				if (!localizedMetadata) continue

				// Always use the original root directory for path calculations
				const item = await this.createPackageManagerItem(
					localizedMetadata,
					componentDir,
					repoUrl,
					this.originalRootDir || rootDir,
					sourceName,
				)
				if (item) {
					// If this is a package, scan for subcomponents
					if (this.isPackageMetadata(localizedMetadata)) {
						// Load metadata for items listed in package metadata
						if (localizedMetadata.items) {
							const subcomponents = await Promise.all(
								localizedMetadata.items.map(async (subItem) => {
									const subPath = path.join(componentDir, subItem.path)
									const subRelativePath = path.relative(rootDir, subPath).replace(/\\/g, "/")
									const subMetadata = await this.loadComponentMetadata(subPath)

									// Skip if no metadata found
									if (!subMetadata) return null

									// Get localized metadata with fallback
									const localizedSubMetadata = this.getLocalizedMetadata(subMetadata)
									if (!localizedSubMetadata) return null

									return {
										type: subItem.type,
										path: subItem.path,
										metadata: localizedSubMetadata,
										lastUpdated: await this.getLastModifiedDate(subPath),
									}
								}),
							)
							item.items = subcomponents.filter((sub): sub is NonNullable<typeof sub> => sub !== null)
						}

						// Also scan directory for unlisted subcomponents
						await this.scanPackageSubcomponents(componentDir, item)
					}
					items.push(item)
					// Skip recursion if this is a package directory
					if (this.isPackageMetadata(localizedMetadata)) {
						continue
					}
				}

				// Recursively scan subdirectories only if not in a package
				if (!metadata || !this.isPackageMetadata(localizedMetadata)) {
					const subItems = await this.scanDirectory(componentDir, repoUrl, sourceName, true)
					items.push(...subItems)
				}
			}
		} catch (error) {
			console.error(`Error scanning directory ${rootDir}:`, error)
		}

		return items
	}

	/**
	 * Gets localized metadata with fallback
	 * @param metadata The localized metadata object
	 * @returns The metadata in the user's locale or fallback locale, or null if neither is available
	 */
	private getLocalizedMetadata(metadata: LocalizedMetadata<ComponentMetadata>): ComponentMetadata | null {
		const { userLocale, fallbackLocale } = this.localizationOptions

		// First try user's locale
		if (metadata[userLocale]) {
			return metadata[userLocale]
		}

		// Fall back to fallbackLocale (typically English)
		if (metadata[fallbackLocale]) {
			return metadata[fallbackLocale]
		}

		// No suitable metadata found
		return null
	}

	/**
	 * Loads metadata for a component
	 * @param componentDir The component directory
	 * @returns Localized metadata or null if no metadata found
	 */
	private async loadComponentMetadata(componentDir: string): Promise<LocalizedMetadata<ComponentMetadata> | null> {
		const metadata: LocalizedMetadata<ComponentMetadata> = {}
		try {
			const entries = await fs.readdir(componentDir, { withFileTypes: true })

			// Look for metadata.{locale}.yml files
			for (const entry of entries) {
				if (!entry.isFile()) continue

				const match = entry.name.match(/^metadata\.([a-z]{2})\.yml$/)
				if (!match) continue

				const locale = match[1]
				const metadataPath = path.join(componentDir, entry.name)

				try {
					const content = await fs.readFile(metadataPath, "utf-8")
					const parsed = yaml.load(content) as Record<string, any>

					// Add type field if missing but has a parent directory indicating type
					if (!parsed.type) {
						const parentDir = path.basename(componentDir)
						if (parentDir === "mcp servers" || parentDir === "mcp-servers") {
							parsed.type = "mcp server"
						}
					}

					metadata[locale] = validateAnyMetadata(parsed) as ComponentMetadata
				} catch (error) {
					const errorMessage = error instanceof Error ? error.message : String(error)
					console.error(`Error loading metadata from ${metadataPath}:`, error)

					// Show validation errors to user
					if (errorMessage.includes("Invalid metadata:")) {
						vscode.window.showErrorMessage(
							`Invalid metadata in ${path.basename(metadataPath)}: ${errorMessage.replace("Invalid metadata:", "").trim()}`,
						)
					}
				}
			}
		} catch (error) {
			console.error(`Error reading directory ${componentDir}:`, error)
		}

		return Object.keys(metadata).length > 0 ? metadata : null
	}

	/**
	 * Creates a PackageManagerItem from component metadata
	 * @param metadata The component metadata
	 * @param componentDir The component directory
	 * @param repoUrl The repository URL
	 * @param sourceName Optional source repository name
	 * @returns PackageManagerItem or null if invalid
	 */
	private async createPackageManagerItem(
		metadata: ComponentMetadata,
		componentDir: string,
		repoUrl: string,
		rootDir: string,
		sourceName?: string,
	): Promise<PackageManagerItem | null> {
		// Skip if no type or invalid type
		if (!metadata.type || !this.isValidComponentType(metadata.type)) {
			return null
		}
		// Always use the original root directory for path calculations
		const effectiveRootDir = this.originalRootDir || rootDir
		// Always calculate path relative to the original root directory
		const fullPath = path.relative(effectiveRootDir, componentDir).replace(/\\/g, "/")
		// Don't encode spaces in URL to match test expectations
		const urlPath = fullPath
			.split("/")
			.map((part) => encodeURIComponent(part))
			.join("/")
		// Create the item with the correct path and URL
		return {
			name: metadata.name,
			description: metadata.description,
			type: metadata.type,
			version: metadata.version,
			tags: metadata.tags,
			url: `${repoUrl}/tree/main/${urlPath}`,
			repoUrl,
			sourceName,
			path: fullPath,
			lastUpdated: await this.getLastModifiedDate(componentDir),
			items: [], // Initialize empty items array for all components
			author: metadata.author,
			authorUrl: metadata.authorUrl,
		}
	}

	/**
	 * Gets the last modified date for a component using git history
	 * @param componentDir The component directory
	 * @returns ISO date string
	 */
	private async getLastModifiedDate(componentDir: string): Promise<string> {
		if (this.git) {
			try {
				// Get the latest commit date for the directory and its contents
				const result = await this.git.raw([
					"log",
					"-1",
					"--format=%aI", // ISO 8601 format
					"--",
					componentDir,
				])
				if (result) {
					return result.trim()
				}
			} catch (error) {
				console.error(`Error getting git history for ${componentDir}:`, error)
				// Fall through to fs.stat fallback
			}
		}

		// Fallback to fs.stat if git is not available or fails
		try {
			const stats = await fs.stat(componentDir)
			return stats.mtime.toISOString()
		} catch {
			return new Date().toISOString()
		}
	}

	/**
	 * Recursively scans a package directory for subcomponents
	 * @param packageDir The package directory to scan
	 * @param packageItem The package item to add subcomponents to
	 */
	private async scanPackageSubcomponents(
		packageDir: string,
		packageItem: PackageManagerItem,
		parentPath: string = "",
	): Promise<void> {
		const entries = await fs.readdir(packageDir, { withFileTypes: true })

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

			const subPath = path.join(packageDir, entry.name)
			// Normalize path to use forward slashes
			const relativePath = parentPath ? `${parentPath}/${entry.name}` : entry.name

			// Try to load metadata directly
			const subMetadata = await this.loadComponentMetadata(subPath)

			if (subMetadata) {
				// Get localized metadata with fallback
				const localizedSubMetadata = this.getLocalizedMetadata(subMetadata)
				if (localizedSubMetadata) {
					const isListed = packageItem.items?.some((i) => i.path === relativePath)

					if (!isListed) {
						const subItem = {
							type: localizedSubMetadata.type,
							path: relativePath,
							metadata: localizedSubMetadata,
							lastUpdated: await this.getLastModifiedDate(subPath),
						}
						packageItem.items = packageItem.items || []
						packageItem.items.push(subItem)
					}
				}
			}

			// Recursively scan this directory
			await this.scanPackageSubcomponents(subPath, packageItem, relativePath)
		}
	}

	/**
	 * Type guard for component types
	 * @param type The type to check
	 * @returns Whether the type is valid
	 */
	private isValidComponentType(type: string): type is ComponentType {
		return ["role", "mcp server", "storage", "mode", "prompt", "package"].includes(type)
	}

	/**
	 * Type guard for package metadata
	 * @param metadata The metadata to check
	 * @returns Whether the metadata is for a package
	 */
	private isPackageMetadata(metadata: ComponentMetadata): metadata is PackageMetadata {
		return metadata.type === "package"
	}
}
