import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'

type Shell = 'fish' | 'bash' | 'zsh'

const SUBCOMMANDS = [
	'prod',
	'-p',
	'--prod',
	'preview',
	'start',
	'run',
	'doctor',
	'-d',
	'--doctor',
	'tags',
	'init',
	'-i',
	'--init',
	'check-source-clean',
	'-c',
	'--check-source-clean',
	'install-shell',
	'-s',
	'--install-shell',
	'help',
	'-h',
	'--help',
	'version',
	'-v',
	'--version',
]
export const DEFAULT_ALIASES = ['dm', 'devmenu']
const ALIAS_NAME_RE = /^[a-zA-Z][\w-]*$/
export const BLOCK_START = '# >>> dev-menu shell integration >>>'
export const BLOCK_END = '# <<< dev-menu shell integration <<<'

export function runInstallShell(): void {
	const tokens = process.argv.slice(3)
	const print = tokens.includes('--print')
	const positional = tokens.filter((token) => !token.startsWith('-'))

	let requested: Shell | undefined
	const custom: string[] = []
	for (const token of positional) {
		const shell = normalizeShell(token)
		if (shell && !requested) requested = shell
		else custom.push(token)
	}
	requested ??= detectShell()

	if (!requested) {
		process.stderr.write(
			`  \x1b[31mcould not detect your shell\x1b[0m — pass one explicitly:\n` +
				`  \x1b[2mdev-menu install-shell fish|bash|zsh [alias...]\x1b[0m\n`,
		)
		process.exit(1)
	}

	const aliases = resolveAliases(custom)

	if (print) {
		process.stdout.write(snippetFor(requested, aliases) + '\n')
		return
	}

	if (requested === 'fish') installFish(aliases)
	else installPosix(requested, aliases)
}

export function resolveAliases(custom: string[] = []): string[] {
	const names = [...DEFAULT_ALIASES]
	for (const name of custom) {
		if (!ALIAS_NAME_RE.test(name)) {
			process.stderr.write(`  \x1b[33mskipping invalid alias name: ${name}\x1b[0m\n`)
			continue
		}
		if (!names.includes(name)) names.push(name)
	}
	return names
}

export function normalizeShell(value: string): Shell | undefined {
	const name = value.trim().toLowerCase()
	if (name === 'fish' || name === 'bash' || name === 'zsh') return name
	return undefined
}

function detectShell(): Shell | undefined {
	const shell = process.env.SHELL ?? ''
	if (shell.includes('fish')) return 'fish'
	if (shell.includes('zsh')) return 'zsh'
	if (shell.includes('bash')) return 'bash'
	return undefined
}

function home(): string {
	const dir = process.env.HOME
	if (!dir) throw new Error('HOME is not set')
	return dir
}

function installFish(aliases: string[]): void {
	const base = resolve(home(), '.config/fish')
	const written: string[] = []

	for (const alias of aliases) {
		const fnFile = resolve(base, `functions/${alias}.fish`)
		const cmpFile = resolve(base, `completions/${alias}.fish`)
		writeFile(
			fnFile,
			`function ${alias} --wraps dev-menu --description "dev-menu"\n\tdev-menu $argv\nend\n`,
		)
		writeFile(cmpFile, fishCompletions(alias))
		written.push(fnFile, cmpFile)
	}

	const cmpDevMenu = resolve(base, 'completions/dev-menu.fish')
	writeFile(cmpDevMenu, fishCompletions('dev-menu'))
	written.push(cmpDevMenu)

	report('fish', written, aliases, 'open a new shell (or run: exec fish)')
}

function installPosix(shell: Shell, aliases: string[]): void {
	const rc = resolve(home(), shell === 'zsh' ? '.zshrc' : '.bashrc')
	const block = `${BLOCK_START}\n${snippetFor(shell, aliases)}\n${BLOCK_END}\n`
	const existing = existsSync(rc) ? readFileSync(rc, 'utf8') : ''

	if (existing.includes(BLOCK_START)) {
		const cleaned = stripBlock(existing)
		writeFileSync(rc, cleaned.replace(/\n*$/, '\n') + block)
	} else {
		appendFileSync(rc, (existing.endsWith('\n') || existing === '' ? '' : '\n') + '\n' + block)
	}

	report(shell, [rc], aliases, `run: source ${rc}`)
}

export function snippetFor(shell: Shell, aliases: string[] = DEFAULT_ALIASES): string {
	const targets = ['dev-menu', ...aliases].join(' ')
	if (shell === 'fish') {
		return aliases
			.map(
				(alias) =>
					`function ${alias} --wraps dev-menu\n\tdev-menu $argv\nend\n${fishCompletions(alias)}`,
			)
			.join('')
			.replace(/\n$/, '')
	}
	const words = SUBCOMMANDS.join(' ')
	const lines = aliases.map((alias) => `alias ${alias}='dev-menu'`)
	if (shell === 'zsh') {
		lines.push(
			'_dev_menu_complete() {',
			'\tlocal -a words',
			'\tread -cA words',
			'\tif (( ${#words} >= 3 )) && [[ ${words[2]} == run ]]; then',
			'\t\treply=($(dev-menu tags 2>/dev/null))',
			'\telse',
			`\t\treply=(${words})`,
			'\tfi',
			'}',
			`compctl -K _dev_menu_complete ${targets}`,
		)
	} else {
		lines.push(
			'_dev_menu_complete() {',
			'\tlocal cur="${COMP_WORDS[COMP_CWORD]}"',
			'\tif [[ ${COMP_CWORD} -ge 2 && ${COMP_WORDS[1]} == run ]]; then',
			'\t\tCOMPREPLY=($(compgen -W "$(dev-menu tags 2>/dev/null)" -- "$cur"))',
			'\telse',
			`\t\tCOMPREPLY=($(compgen -W '${words}' -- "$cur"))`,
			'\tfi',
			'}',
			`complete -F _dev_menu_complete ${targets}`,
		)
	}
	return lines.join('\n')
}

function fishCompletions(cmd: string): string {
	const subcommands = SUBCOMMANDS.map(
		(sub) => `complete -c ${cmd} -n __fish_use_subcommand -a '${sub}'`,
	).join('\n')
	const runTags = `complete -c ${cmd} -f -n '__fish_seen_subcommand_from run' -a '(dev-menu tags 2>/dev/null)'`
	return `${subcommands}\n${runTags}\n`
}

export function stripBlock(content: string): string {
	const start = content.indexOf(BLOCK_START)
	const end = content.indexOf(BLOCK_END)
	if (start === -1 || end === -1) return content
	return content.slice(0, start) + content.slice(end + BLOCK_END.length)
}

function writeFile(path: string, content: string): void {
	mkdirSync(dirname(path), { recursive: true })
	writeFileSync(path, content)
}

function report(shell: Shell, files: string[], aliases: string[], hint: string): void {
	process.stdout.write(
		`\n  \x1b[32m✔ ${shell} integration installed\x1b[0m` +
			`  \x1b[2maliases\x1b[0m \x1b[36m${aliases.join(' ')}\x1b[0m \x1b[2m+ tab-completion\x1b[0m\n` +
			files.map((f) => `    \x1b[2m${f}\x1b[0m\n`).join('') +
			`  \x1b[2m${hint}\x1b[0m\n\n`,
	)
}
