import vm from 'vm'

import { parse, resolve } from '../resolve'
import AsyncCache from '../utils/async-cache'
import { error } from '../utils/error'

declare module 'vm' {
	export abstract class Module {
		public readonly namespace: any
		public readonly dependencySpecifiers: string[]

		link(
			linker: (
				specifier: string,
				referencingModule: Module,
			) => Module | Promise<Module>,
		): Promise<void>
		evaluate(): Promise<void>
	}

	export class SourceTextModule extends Module {
		constructor(
			code: string,
			options: { identifier?: string; context?: vm.Context },
		)
	}
}

const rootModule = '@refloat/boot.js'

export interface ConfigModule {
	code: string
	path: string
}

export type ConfigPlatform = 'macos' | 'linux' | 'windows'

export interface ConfigRunContext {
	org: {
		id: string
		slug: string
	}
	user: {
		id: string
		displayName: string
	}
	build: {
		id: string
		number: number
	}
	repo: {
		id: string
		slug: string
		defaultBranch: string
	}
	commit: {
		sha: string
		branch: string
		defaultBranch: boolean
	}
}

export async function runConfig(
	path: string,
	configSource: string,
	checkout: (source: string) => Promise<unknown>,
	fetch: (source: string) => Promise<string>,
	configCtx: ConfigRunContext,
	overrides: Record<string, string>,
) {
	const config = {
		source: configSource,
		linux: '{}',
		macos: '{}',
		windows: '{}',
	}
	const dependencies = new Set<string>()
	const paths = new WeakMap<vm.SourceTextModule, string>()
	const checkouts = new AsyncCache(checkout)
	const sources = new AsyncCache(async (source: string) => {
		const dep = parse(source)

		await checkouts.get(
			`${dep.scheme}://${dep.host}/${dep.owner}/${dep.repo}${
				dep.ref ? `#${dep.ref}` : ''
			}`,
		)

		return fetch(source)
	})

	await Promise.all(
		Array.from<ConfigPlatform>(['linux', 'macos', 'windows']).map(
			async (platform) => {
				const context = vm.createContext(
					{},
					{ codeGeneration: { strings: false, wasm: false } },
				)
				const modules = new AsyncCache(async (sourceIdentifier: string) => {
					const source = getSource(sourceIdentifier, overrides)
					const code =
						source === path ? configSource : await sources.get(source)
					const module = new vm.SourceTextModule(code, {
						context,
						identifier: source,
					})

					if (source !== path) {
						dependencies.add(source)
					}

					for (const specifier of module.dependencySpecifiers) {
						if (!specifier.startsWith('@refloat/')) {
							modules.preload(resolve(source, specifier))
						}
					}

					paths.set(module, source)

					return module
				})
				const module = new vm.SourceTextModule(
					`
						import { jobs } from ${JSON.stringify(path)}

						export const json = JSON.stringify({ jobs })
					`,
					{ context, identifier: rootModule },
				)

				paths.set(module, rootModule)

				try {
					await module.link((specifier, referrer) => {
						const path = resolve(
							paths.get(referrer) ?? error('Failed to find VM referrer path'),
							specifier,
						)

						return path === '@refloat/context'
							? new vm.SourceTextModule(createContext(configCtx, platform), {
									context,
									identifier: path,
							  })
							: modules.get(path)
					})

					await module.evaluate()
				} catch (error: any) {
					throw new Error(
						String(error?.stack ?? error ?? 'Unexpected error')
							.replace(/at SourceTextModule\.evaluate(.|\s)*/, '')
							.trimEnd(),
					)
				}

				const { json } = module.namespace

				config[platform] =
					typeof json === 'string'
						? json
						: error(`Unexpected VM script result type: "${typeof json}"`)
			},
		),
	)

	return {
		config,
		dependencies: Array.from(dependencies),
	}
}

const getSource = (path: string, overrides: Record<string, string>) => {
	for (const key of Object.keys(overrides)) {
		if (path === key || path.startsWith(key + '/')) {
			return path.replace(key, overrides[key])
		}
	}

	return path
}
const createContext = (context: ConfigRunContext, platform: ConfigPlatform) => `
	export const platform = ${JSON.stringify(platform)}
	export const {build, repo, org, commit, user} = ${JSON.stringify(context)}
`
