import type { RouteRecordRaw } from 'vue-router'
import { writeFileSync, readFileSync, unlinkSync } from 'node:fs'
import { resolve } from 'node:path'
import { argv, cwd, exit } from 'node:process'

export interface ProcessedRoute {
	name: string
	path: string
	params: { name: string, isArray: boolean }[]
	componentInfo: string
}

/**
 * Extract parameters from a route path without regex.
 */
function extractParamsFromPath(routePath: string): { name: string, isArray: boolean }[] {
	const segments = routePath.split('/')
	const params: { name: string, isArray: boolean }[] = []

	for (const segment of segments) {
		if (segment.startsWith(':')) {
			// e.g. ":id+" or ":id*"
			const paramName = segment.substring(1)
			const isArray = paramName.endsWith('+') || paramName.endsWith('*')
			params.push({
				name: isArray ? paramName.slice(0, -1) : paramName,
				isArray,
			})
		} else {
			// If a segment has parentheses, e.g. ":id(\\d+)", handle it as needed
			// Avoid explicit regex, just do naive scans
			if (segment.includes('(') && segment.includes(')')) {
				const openParenIndex = segment.indexOf('(')
				if (segment.startsWith(':') && openParenIndex > 1) {
					const paramName = segment.substring(1, openParenIndex)
					params.push({
						name: paramName,
						isArray: false,
					})
				}
			}
		}
	}

	return params
}

/**
 * Format a component path/name for documentation.
 */
function formatComponentInfo(component: string): string {
	// If there is an async import, try to capture the path (simple substring logic, no regex)
	const searchStr = 'import('
	const idx = component.indexOf(searchStr)
	if (idx >= 0) {
		// Look for the closing parenthesis
		const closingParenIdx = component.indexOf(')', idx)
		if (closingParenIdx > idx) {
			const inner = component.substring(idx, closingParenIdx + 1)
			// Attempt to find the path inside import('...')
			const quoteStart = inner.includes('\'') ? inner.indexOf('\'') : inner.indexOf('"')
			const quoteEnd = quoteStart >= 0 ? inner.indexOf(inner[quoteStart], quoteStart + 1) : -1
			if (quoteStart >= 0 && quoteEnd > quoteStart) {
				const importPath = inner.substring(quoteStart + 1, quoteEnd)
				const replacedPath = importPath.replace('@/', './../')
				const componentName = replacedPath.split('/').pop()?.split('.')[0] || 'Component'
				return `[${componentName}](${replacedPath})`
			}
		}
		return 'Dynamic Import'
	}

	// If it is just a direct reference
	if (!component.includes('function')) {
		return component
	}

	return component
}

function extractComponentInfo(route: RouteRecordRaw): string {
	if (!route.component) return 'Unknown'

	// If already augmented with `componentInfo`
	if ((route as any).componentInfo) {
		return formatComponentInfo((route as any).componentInfo)
	}

	// If dynamic import function
	if (typeof route.component === 'function') {
		const fnString = route.component.toString()
		return formatComponentInfo(fnString)
	}

	// If direct object reference
	if (typeof route.component === 'object') {
		const componentName = route.component.name || 'Component Object'
		return componentName
	}

	// Fallback
	return String(route.component)
}

/**
 * Recursively gather routes with parameter inheritance.
 */
function processRoute(
	route: RouteRecordRaw,
	basePath = '',
	parentParams: { name: string, isArray: boolean }[] = []
): ProcessedRoute[] {
	const results: ProcessedRoute[] = []

	// Normalize path
	const fullPath = route.path.startsWith('/')
		? route.path
		: basePath
			? `${basePath}/${route.path}`
			: route.path

	const routeParams = extractParamsFromPath(route.path)

	// Merge parent and child params
	const paramsMap = new Map<string, { name: string, isArray: boolean }>()
	for (const p of parentParams) paramsMap.set(p.name, p)
	for (const p of routeParams) paramsMap.set(p.name, p)
	const combinedParams = Array.from(paramsMap.values())

	if (route.name) {
		results.push({
			name: String(route.name),
			path: fullPath,
			params: combinedParams,
			componentInfo: extractComponentInfo(route),
		})
	}

	if (route.children) {
		for (const child of route.children) {
			results.push(...processRoute(child, fullPath, combinedParams))
		}
	}
	return results
}

/**
 * Convert processed routes into a RouteNamedMap interface.
 */
function generateRouteTypes(routes: readonly RouteRecordRaw[]): string {
	const processedRoutes: ProcessedRoute[] = []
	for (const r of routes) {
		processedRoutes.push(...processRoute(r))
	}

	let output = `// Auto-generated file

import type { RouteRecordInfo } from 'vue-router'

export interface RouteNamedMap {`

	for (const route of processedRoutes) {
		// skip catch-all or unnamed
		if (!route.name || route.name === '404' || route.path === '/:catchAll(.*)*') continue

		if (route.params.length > 0) {
			const rawParams = route.params
				.map(p => `${p.name}: ${p.isArray ? '(string)[]' : 'string'}`)
				.join(', ')
			const normalizedParams = route.params
				.map(p => `${p.name}: ${p.isArray ? 'string[]' : 'string'}`)
				.join(', ')

			output += `
    /**
     * Component: ${route.componentInfo}
     */
    '${route.name}': RouteRecordInfo<
        '${route.name}',
        '${route.path}',
        { ${rawParams} },
        { ${normalizedParams} }
    >,`
		} else {
			output += `
    /**
     * Component: ${route.componentInfo}
     */
    '${route.name}': RouteRecordInfo<
        '${route.name}',
        '${route.path}',
        Record<never, never>,
        Record<never, never>
    >,`
		}
	}

	output += `
}

declare module 'vue-router' {
  interface TypesConfig {
    RouteNamedMap: RouteNamedMap
  }
}`

	return output
}

/**
 * Generate route definitions and write them to a file.
 */
function generateRouteTypesFromRoutes(
	routes: readonly RouteRecordRaw[],
	fullOutputPath: string
) {
	try {
		const typeDefinitions = generateRouteTypes(routes)
		writeFileSync(fullOutputPath, typeDefinitions)
		console.log(`Route type definitions successfully written to ${fullOutputPath}`)
	} catch (error) {
		console.error('Error generating route type definitions:', error)
		throw error
	}
}

function stripAtImports(content: string): string {
	// split into lines
	const lines = content.split('\n')

	const modified = lines.map((line) => {
		// if line has `import X from '@/...`
		if (line.includes('from \'@/') || line.includes('from "@/')) {
			// E.g. turn:
			//   import something from '@/api'
			// Into:
			//   const something = {}
			//
			// We do it very naively by searching for 'import ' and ' from'
			const importKeyword = 'import '
			const fromKeyword = ' from'

			// Only proceed if both exist
			const importIdx = line.indexOf(importKeyword)
			const fromIdx = line.indexOf(fromKeyword, importIdx + importKeyword.length)
			const isVueComponent = line.endsWith(`'.vue'`)

			if (importIdx !== -1 && fromIdx !== -1) {
				// Extract the variable name(s). For simplicity, we take the substring after 'import ' up to ' from'
				const varNames = line
					.substring(importIdx + importKeyword.length, fromIdx)
					.trim()

				// Turn something like:
				//   "something" or "{ x, y }"
				// into a valid JS declaration. We'll do a minimal approach:
				// if it starts with '{' or '*', do `const { x, y } = {};`
				// if it's a normal identifier, do `const something = {};`
				if (varNames.startsWith('{') || varNames.startsWith('*')) {
					return `const ${varNames} = {}; // stripped @/ import`
				} else {
					if (isVueComponent) {
						return `const ${varNames} = {name: '${varNames}'}; // stripped @/ import`
					}
					// e.g. "something" or "MyClass"
					return `const ${varNames} = {}; // stripped @/ import`
				}
			}
			// If we can't parse well, just comment out the line:
			return `// ${line}`
		}

		return line
	})

	return modified.join('\n')
}

// Script usage
if (require.main === module) {
	let exitCode = 0
	const routesPath = argv[2] || './src/router/index.ts'
	const outputPath = argv[3] || './src/router/vue-routes.d.ts'

	//  create temp file that replaces the @/ with the relative path ./..
	const routesFileContent = readFileSync(routesPath, 'utf-8')

	const tempFilePath = resolve(cwd(), routesPath.replace('.ts', '.temp.ts'))

	// 1) Replace `@/...` imports with empty objects
	const strippedContent = stripAtImports(routesFileContent)

	// 2) Write the modified content
	writeFileSync(tempFilePath, strippedContent)

	// region Mocking global objects
	Object.assign(globalThis, { window: {
		history: { replaceState: () => {}, pushState: () => {} },
		location: { protocol: '', replace: () => {}, assign: () => {} },
		addEventListener: () => {},
	}	})
	Object.assign(globalThis, { location: { protocol: '', replace: () => {}, assign: () => {} } })
	// endregion Mocking global objects

	import(tempFilePath).then(({ routes }) => {
		if (!routes || routes.length === 0) {
			throw new Error('No routes found in the specified file')
		}

		const fullOutputPath = resolve(cwd(), outputPath)
		generateRouteTypesFromRoutes(routes, fullOutputPath)
	}).catch((error) => {
		console.error('Error processing routes:', error)
		exitCode = 1
	}).finally(() => {
		// Clean up temp file
		unlinkSync(tempFilePath)
		exit(exitCode)
	})
}
