#!/usr/bin/env node

import { execSync } from "node:child_process"
import { promises as fs } from "node:fs"
import chalk from "chalk"
import figlet from "figlet"
import { select, input } from "@inquirer/prompts"
import { simpleGit } from "simple-git"
import { generateNextApp } from "./generate-next"
import { generateReactApp } from "./generate-react"
import type { Chain } from "./types"
import { promptForChains, chainOptions } from "./utils"

const FRAMEWORKS = {
	NEXT: "nextjs",
	REACT: "react",
	REACT_NATIVE: "react-native",
	SCAFFOLD_ETH: "scaffold-eth",
} as const

type Framework = (typeof FRAMEWORKS)[keyof typeof FRAMEWORKS]

const PACKAGE_MANAGERS = {
	NPM: "npm",
	YARN: "yarn",
	PNPM: "pnpm",
	BUN: "bun",
} as const

type PackageManager = (typeof PACKAGE_MANAGERS)[keyof typeof PACKAGE_MANAGERS]

const ETH_LIBRARIES = {
	VIEM: "viem",
	ETHERS: "ethers",
} as const

type EthLibrary = (typeof ETH_LIBRARIES)[keyof typeof ETH_LIBRARIES]

const FRAMEWORK_CHOICES = [
	{ name: "NextJS", value: FRAMEWORKS.NEXT },
	{ name: "ReactJS", value: FRAMEWORKS.REACT },
	{ name: "React Native", value: FRAMEWORKS.REACT_NATIVE },
]

const LIBRARY_CHOICES = [
	{ name: "Happy with Viem", value: ETH_LIBRARIES.VIEM },
	{ name: "I need Ethers", value: ETH_LIBRARIES.ETHERS },
]

const PACKAGE_MANAGER_CHOICES = [
	{ name: "npm", value: PACKAGE_MANAGERS.NPM },
	{ name: "yarn", value: PACKAGE_MANAGERS.YARN },
	{ name: "pnpm", value: PACKAGE_MANAGERS.PNPM },
	{ name: "bun", value: PACKAGE_MANAGERS.BUN },
]

const REPO_BASE_URL = "https://github.com/dynamic-labs/"

interface CommandLineArgs {
	projectName?: string
	packageManager?: PackageManager
	framework?: Framework
	library?: EthLibrary
	wagmi?: boolean
	chains?: Chain[]
}

function showHelp(): never {
	console.log(`
${chalk.yellow("CREATE DYNAMIC APP")} - A CLI tool to generate web3 applications with Dynamic authentication

${chalk.cyan("USAGE:")}
  npx create-dynamic-app [project-name] [options]

${chalk.cyan("OPTIONS:")}
  --help, -h                           Show this help message
  --framework, -f <framework>          Specify framework (nextjs, react, react-native)
  --pm, --package-manager <manager>    Specify package manager (npm, yarn, pnpm, bun)
  --library, -l <library>              Specify Ethereum library (viem, ethers)
  --wagmi <boolean>                    Use Wagmi with Viem (true, false)
  --chains, -c <chains>                Comma-separated list of chains to include
                                       (ethereum,solana,flow,starknet,algorand,cosmos,bitcoin,sui)

${chalk.cyan("EXAMPLES:")}
  npx create-dynamic-app my-app
  npx create-dynamic-app my-app --pm yarn
  npx create-dynamic-app my-app --framework nextjs --chains ethereum,solana
  npx create-dynamic-app my-app --framework react --library viem --wagmi true --pm pnpm
`)
	process.exit(0)
}

function parseArgs(): CommandLineArgs {
	const args = process.argv.slice(2)
	const result: CommandLineArgs = {}

	const projectNameIndex = args.findIndex((arg) => !arg.startsWith("-"))
	if (projectNameIndex !== -1) {
		result.projectName = args[projectNameIndex]
	}

	for (let i = 0; i < args.length; i++) {
		const arg = args[i]
		const nextArg = args[i + 1]

		const hasValue = nextArg && !nextArg.startsWith("-")

		switch (arg) {
			case "--pm":
			case "--package-manager":
				if (
					hasValue &&
					Object.values(PACKAGE_MANAGERS).includes(
						nextArg.toLowerCase() as PackageManager
					)
				) {
					result.packageManager = nextArg.toLowerCase() as PackageManager
					i++
				}
				break

			case "--framework":
			case "-f":
				if (
					hasValue &&
					Object.values(FRAMEWORKS).includes(nextArg.toLowerCase() as Framework)
				) {
					result.framework = nextArg.toLowerCase() as Framework
					i++
				}
				break

			case "--library":
			case "-l":
				if (
					hasValue &&
					Object.values(ETH_LIBRARIES).includes(
						nextArg.toLowerCase() as EthLibrary
					)
				) {
					result.library = nextArg.toLowerCase() as EthLibrary
					i++
				}
				break

			case "--wagmi":
				if (hasValue) {
					result.wagmi = nextArg.toLowerCase() === "true"
					i++
				}
				break

			case "--chains":
			case "-c":
				if (hasValue) {
					const requestedChains = nextArg.toLowerCase().split(",")
					const validChains = chainOptions.filter((chain) =>
						requestedChains.includes(chain.name.toLowerCase())
					)

					if (validChains.length > 0) {
						result.chains = validChains
						i++
					}
				}
				break
		}
	}

	return result
}

async function checkDirectoryExists(directory: string): Promise<boolean> {
	try {
		await fs.access(directory)
		return true
	} catch {
		return false
	}
}

function generateRepoUrl(
	framework: Framework,
	library = "",
	wagmi = ""
): string {
	switch (framework) {
		case FRAMEWORKS.REACT_NATIVE:
			return `${REPO_BASE_URL}react-native-expo`
		case FRAMEWORKS.SCAFFOLD_ETH:
			return `${REPO_BASE_URL}hackathon-starter-kit`
		default:
			return `${REPO_BASE_URL}${framework}${library ? `-${library}` : ""}${
				wagmi ? `-${wagmi}` : ""
			}`
	}
}

async function cloneRepository(
	repoUrl: string,
	directoryName: string
): Promise<boolean> {
	try {
		await simpleGit().clone(repoUrl, directoryName)
		await simpleGit(directoryName).removeRemote("origin")
		return true
	} catch (error) {
		console.error(
			chalk.red("Failed to clone repository:"),
			error instanceof Error ? error.message : String(error)
		)
		return false
	}
}

function getInstallCommand(packageManager: PackageManager): string {
	const commands = {
		[PACKAGE_MANAGERS.NPM]: "npm install",
		[PACKAGE_MANAGERS.YARN]: "yarn",
		[PACKAGE_MANAGERS.PNPM]: "pnpm install",
		[PACKAGE_MANAGERS.BUN]: "bun install",
	}

	return commands[packageManager] || "npm install"
}

function getRunCommand(packageManager: PackageManager): string {
	const commands = {
		[PACKAGE_MANAGERS.NPM]: "npm run dev",
		[PACKAGE_MANAGERS.YARN]: "yarn dev",
		[PACKAGE_MANAGERS.PNPM]: "pnpm dev",
		[PACKAGE_MANAGERS.BUN]: "bun dev",
	}

	return commands[packageManager] || "npm run dev"
}

async function generateApp(
	directoryName: string,
	framework: Framework,
	library: string,
	wagmi: string,
	selectedChains: Chain[]
): Promise<void> {
	const useViem = library === ETH_LIBRARIES.VIEM
	const useWagmi = wagmi === "wagmi"

	if (framework === FRAMEWORKS.NEXT) {
		await generateNextApp(
			process.cwd(),
			directoryName,
			useViem,
			useWagmi,
			selectedChains
		)
	} else if (framework === FRAMEWORKS.REACT) {
		await generateReactApp(
			process.cwd(),
			directoryName,
			useViem,
			useWagmi,
			selectedChains
		)
	}
}

async function handleSpecialFrameworks(
	framework: Framework,
	directoryName: string,
	packageManager: PackageManager
): Promise<void> {
	const repoUrl = generateRepoUrl(framework as Framework)
	const success = await cloneRepository(repoUrl, directoryName)
	if (!success) return

	const installCommand = getInstallCommand(packageManager)

	const setupMessage =
		framework === FRAMEWORKS.SCAFFOLD_ETH
			? `Project setup complete! Cd into the ${directoryName} directory and run ${installCommand}.`
			: `Project setup complete! Check it out in the ${directoryName} directory.`

	console.log(chalk.green(setupMessage))
}

async function askForNewDirectory(originalDirectory: string): Promise<string> {
	const newDirectory = await input({
		message: `The directory "${originalDirectory}" already exists. Enter a new directory name:`,
		validate: (value) =>
			value ? true : "Please enter a valid directory name.",
	})

	return (await checkDirectoryExists(newDirectory))
		? askForNewDirectory(newDirectory)
		: newDirectory
}

function getDocsUrl(framework: Framework): string {
	return framework === FRAMEWORKS.SCAFFOLD_ETH
		? "https://github.com/dynamic-labs/hackathon-starter-kit"
		: "https://app.dynamic.xyz/dashboard/developer/api"
}

function getDocsMessage(framework: Framework, docsUrl: string): string {
	switch (framework) {
		case FRAMEWORKS.SCAFFOLD_ETH:
			return `Check out the documentation for the Scaffold Eth Hacker Edition at ${docsUrl} to get started!`
		default:
			return `Make sure to grab your Environment ID from ${docsUrl} and add it to the DynamicContextProvider!`
	}
}

function displayWelcomeBanner(): void {
	try {
		console.log(
			chalk.yellow(
				figlet.textSync("Create Dynamic App", { horizontalLayout: "full" })
			)
		)
	} catch {
		console.log(chalk.yellow("\n=== CREATE DYNAMIC APP ===\n"))
	}

	console.log(
		chalk.green(
			"Welcome to the Dynamic App Creator! Follow the prompts to set up your project."
		)
	)
}

export async function main(): Promise<void> {
	const args = parseArgs()

	const framework =
		args.framework ||
		(await select({
			message: "What framework would you like to use?",
			choices: FRAMEWORK_CHOICES,
		}))

	let directoryName =
		args.projectName ||
		(framework === FRAMEWORKS.SCAFFOLD_ETH
			? "my-hacker-project"
			: "my-dynamic-project")

	if (await checkDirectoryExists(directoryName)) {
		directoryName = await askForNewDirectory(directoryName)
	}

	const packageManager =
		args.packageManager ||
		((await select({
			message: "Which package manager would you like to use?",
			choices: PACKAGE_MANAGER_CHOICES,
			default: PACKAGE_MANAGERS.NPM,
		})) as PackageManager)

	if (
		framework === FRAMEWORKS.REACT_NATIVE ||
		framework === FRAMEWORKS.SCAFFOLD_ETH
	) {
		await handleSpecialFrameworks(framework, directoryName, packageManager)

		const docsUrl = getDocsUrl(framework)
		console.log(chalk.magenta(getDocsMessage(framework, docsUrl)))
		return
	}

	const selectedChains = args.chains || (await promptForChains())

	const hasEthereum = selectedChains.some((chain) => chain.name === "Ethereum")

	let library = ""
	let wagmi = ""

	if (hasEthereum) {
		library =
			args.library ||
			(await select({
				message:
					"For Ethereum interactions, are you happy with Viem, or do you need Ethers?",
				choices: LIBRARY_CHOICES,
			}))

		if (library === ETH_LIBRARIES.VIEM) {
			wagmi = args.wagmi
				? "wagmi"
				: await select({
						message: "Do you want to use Wagmi on top of Viem?",
						choices: [
							{ name: "No", value: "" },
							{ name: "Yes", value: "wagmi" },
						],
					})
		}
	}

	await generateApp(directoryName, framework, library, wagmi, selectedChains)

	const installCommand = getInstallCommand(packageManager)
	const runCommand = getRunCommand(packageManager)

	console.log(
		chalk.blue(
			`Installing dependencies in ${directoryName} using ${packageManager}... This might take a moment.`
		)
	)

	try {
		execSync(installCommand, { cwd: directoryName, stdio: "inherit" })
		console.log(chalk.green("Dependencies installed successfully!"))
		console.log(chalk.yellow("\nNext steps:"))
		console.log(chalk.cyan(`1. cd ${directoryName}`))
		console.log(
			chalk.cyan(`2. Run ${runCommand} to start the development server`)
		)
	} catch (error: unknown) {
		const errorMessage = error instanceof Error ? error.message : String(error)
		console.error(chalk.red(`Failed to install dependencies: ${errorMessage}`))
		console.log(
			chalk.yellow(
				`Please try running '${installCommand}' manually in the ${directoryName} directory.`
			)
		)
	}

	const docsUrl = getDocsUrl(framework)
	console.log(chalk.magenta(getDocsMessage(framework, docsUrl)))
}

if (process.argv.includes("--help") || process.argv.includes("-h")) {
	showHelp()
}

displayWelcomeBanner()

if (!process.env.TEST_ENV) {
	await main().catch((error) => {
		console.error(
			chalk.red("An error occurred:"),
			error instanceof Error ? error.message : String(error)
		)
		process.exit(1)
	})
}
