import { mkdir, readFile, stat } from 'fs/promises'
import { join } from 'path'

import { parse, Source } from '../resolve'
import { exec, ExecError } from '../run/exec'
import { assertNever } from '../utils/assert-never'
import { paths } from '../utils/paths'

export class ModuleReader {
	public readonly fetcher = new ModuleFetcher()

	public async checkout(path: string) {
		await this.fetcher.init(parse(path))
	}

	public async sha(path: string) {
		const source = parse(path)
		const sha = await this.fetcher.sha(source)

		if (sha) {
			return sha
		}

		throw new Error()
	}

	public async read(path: string) {
		const source = parse(path)

		if (await this.fetcher.checkout(source)) {
			return readFile(join(this.fetcher.cwd(source), source.path), 'utf-8')
		} else {
			return null
		}
	}
}

export class ModuleFetcher {
	public async init(source: Source) {
		const cwd = this.cwd(source)
		const git = this.git(source)
		const dirStat = await stat(cwd).catch((error) => {
			if (error?.code === 'ENOENT') {
				return null
			}

			throw error
		})

		if (!dirStat) {
			await mkdir(cwd, { recursive: true })
			await git('init')
			await git('remote', 'add', 'origin', this.origin(source))
		}

		await this.refresh(source)
	}

	public async sha(source: Source) {
		const git = this.git(source)
		const sha = await git('rev-parse', 'FETCH_HEAD')

		if (sha) {
			return sha.trim()
		}

		throw new Error(`Could not find SHA for repository "${this.cwd(source)}"`)
	}

	public async checkout(source: Source) {
		const git = this.git(source)

		try {
			await git('checkout', 'FETCH_HEAD', '--', source.path)
		} catch (error: any) {
			if (error instanceof ExecError && error.code === 1) {
				// "pathspec '.refloat/config.js' did not match any file(s) known to git"
				return false
			}

			throw error
		}

		return true
	}

	public async refresh(source: Source) {
		const git = this.git(source)

		await git('fetch', 'origin', source.ref ?? 'HEAD')
	}

	public cwd({ host, owner, repo, ref }: Source) {
		return join(paths.cache, 'modules', host, owner, repo, ref ?? '~default')
	}

	private git(source: Source) {
		const cwd = this.cwd(source)

		return (...args: string[]) => exec.run('git', args, { cwd })
	}

	private origin({ scheme, host, owner, repo }: Source) {
		if (scheme === 'git+https') {
			return `https://${host}/${owner}/${repo}.git`
		} else if (scheme === 'git+ssh') {
			return `git@${host}:${owner}/${repo}.git`
		} else {
			return assertNever(scheme)
		}
	}
}
