import { resolve } from 'path'
import { tmpdir } from 'os'
import { mkdir, rm } from 'fs/promises'
import glob from 'glob'

import { docker } from '../utils/docker'
import { exec } from './exec'
import { Graph, Node } from './graph'
import { scripts } from './scripts'
import { error } from '../utils/error'
import { APIClient } from '../api/api-client'
import { RepoProviderType } from '../generated/schema'
import { assertNever } from '../utils/assert-never'

const getSecret = (name: string) =>
	process.env[`SECRET_${name}`] ?? error(`Secret "${name}" is not defined`)

export class RunEnv {
	private readonly cwd: string
	private docker = false

	constructor(private readonly graph: Graph, private readonly api: APIClient) {
		this.cwd = resolve(tmpdir(), this.graph.job.id, this.graph.job.repo.slug)
	}

	public async init() {
		await mkdir(this.cwd, { recursive: true })
	}

	public async clean() {
		await Promise.all([
			rm(this.cwd, { recursive: true }),
			Promise.resolve().then(() => {
				if (this.docker) {
					return docker.kill(this.graph.job.id)
				}
			}),
		])
	}

	public async exec(
		node: Node,
		output?: (chunk: Buffer) => void,
	): Promise<number | null> {
		const { job } = this.graph
		const { step } = node
		const pathEnv = process.platform === 'win32' ? 'Path' : 'PATH'
		const env: Record<string, string | undefined> = {
			PWD: this.cwd,
			SHELL: process.env.SHELL,
			HOME: process.env.HOME,
			[pathEnv]: process.env[pathEnv] ?? '',
		}

		for (const { name, value, secret } of step.env) {
			env[name] = secret ? getSecret(value) : value
		}

		let command = step.command
		const init = command[0] === '@refloat/init'
		const skip = command[0] === '@refloat/init-no-checkout'
		const artifact = command[0] === '@refloat/artifact'

		if (skip) {
			output?.(Buffer.from('Repository checkout skipped\n'))
			return 0
		}

		if (artifact) {
			const { cwd } = this
			const matches = await Promise.all(
				command.slice(1).map(
					(file) =>
						new Promise<string[]>((resolve, reject) => {
							glob(file, { cwd, nonull: true }, (err, result) =>
								err ? reject(err) : resolve(result),
							)
						}),
				),
			)
			const files = Array.from(new Set(matches.flat()))

			await Promise.all(
				files.map((path) =>
					this.api.upload({
						stepId: step.id,
						cwd,
						path,
					}),
				),
			)

			const text =
				'Uploaded artifacts:' + files.map((file) => `- ${file}`).join('\n')

			output?.(Buffer.from(text))

			return 0
		}

		if (init) {
			const { providerType, providerSource } = job.repo

			if (
				providerType === RepoProviderType.Custom &&
				providerSource === 'refloat.io/script'
			) {
				return 0
			}

			command = [scripts.init]

			Object.assign(env, {
				REFLOAT_CLONE_URL:
					providerType === RepoProviderType.GitHub
						? `git@github.com:${providerSource}.git`
						: providerType === RepoProviderType.Bitbucket
						? `git@bitbucket.org:${providerSource}.git`
						: providerType === RepoProviderType.Custom
						? providerSource
						: assertNever(providerType),
				REFLOAT_CLONE_SHA: job.build.sha,
				REFLOAT_CLONE_BRANCH: job.build.branch,
				REFLOAT_DOCKER_FILE: job.docker?.file ?? '',
				REFLOAT_DOCKER_IMAGE: job.docker?.image ?? '',
				REFLOAT_DOCKER_CONTEXT: job.docker?.context ?? '',
				REFLOAT_JOB_ID: job.id,
				REFLOAT_REPO_NAME: job.repo.slug,
			})
		}

		if (!init && (job.docker?.image || job.docker?.file)) {
			this.docker = true

			command = [
				'docker',
				'exec',
				'-t',
				'-w',
				resolve('/refloat', job.repo.slug),
				...step.env.flatMap((name) => ['-e', name.name]),
				job.id,
				...command,
			]
		}

		return exec(command[0], command.slice(1), {
			env,
			cwd: this.cwd,
			output,
		})
	}
}
