// Handle the configuration loading for headplane.
// Functionally only used for all sorts of sanity checks across headplane.
//
// Around the codebase, this is referred to as the context

import { access, constants, readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'

import { parse } from 'yaml'

import { checkIntegration, Integration } from '~/integration'

import { HeadscaleConfig, loadConfig } from './headscale'

export interface HeadplaneContext {
	headscaleUrl: string
	cookieSecret: string
	integration: Integration | undefined

	config: {
		read: boolean
		write: boolean
	}

	acl: {
		read: boolean
		write: boolean
	}

	oidc?: {
		issuer: string
		client: string
		secret: string
		rootKey: string
		disableKeyLogin: boolean
	}
}

let context: HeadplaneContext | undefined

export async function loadContext(): Promise<HeadplaneContext> {
	if (context) {
		return context
	}

	const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
	const { config, contextData } = await checkConfig(path)

	let headscaleUrl = process.env.HEADSCALE_URL
	if (!headscaleUrl && !config) {
		throw new Error('HEADSCALE_URL not set')
	}

	if (config) {
		headscaleUrl = headscaleUrl ?? config.server_url
	}

	if (!headscaleUrl) {
		throw new Error('Missing server_url in headscale config')
	}

	const cookieSecret = process.env.COOKIE_SECRET
	if (!cookieSecret) {
		throw new Error('COOKIE_SECRET not set')
	}

	context = {
		headscaleUrl,
		cookieSecret,
		integration: await checkIntegration(),
		config: contextData,
		acl: await checkAcl(config),
		oidc: await checkOidc(config),
	}

	console.log('Completed loading the Headplane Context')
	console.log('Headscale URL:', headscaleUrl)
	console.log('Integration:', context.integration?.name ?? 'None')
	console.log('Config:', contextData.read ? `Found ${contextData.write ? '' : '(Read Only)'}` : 'Unavailable')
	console.log('ACL:', context.acl.read ? `Found ${context.acl.write ? '' : '(Read Only)'}` : 'Unavailable')
	console.log('OIDC:', context.oidc ? 'Configured' : 'Unavailable')

	return context
}

export async function loadAcl(): Promise<{ data: string, type: 'json' | 'yaml' }> {
	let path = process.env.ACL_FILE
	if (!path) {
		try {
			const config = await loadConfig()
			path = config.acl_policy_path
		} catch {}
	}

	if (!path) {
		return { data: '', type: 'json' }
	}

	const data = await readFile(path, 'utf8')

	// Naive check for YAML over JSON
	// This is because JSON.parse doesn't support comments
	try {
		parse(data)
		return { data, type: 'yaml' }
	} catch {
		return { data, type: 'json' }
	}
}

export async function patchAcl(data: string) {
	let path = process.env.ACL_FILE
	if (!path) {
		try {
			const config = await loadConfig()
			path = config.acl_policy_path
		} catch {}
	}

	if (!path) {
		throw new Error('No ACL file defined')
	}

	await writeFile(path, data, 'utf8')
}

async function checkConfig(path: string) {
	let config: HeadscaleConfig | undefined
	try {
		config = await loadConfig(path)
	} catch {
		return {
			config: undefined,
			contextData: {
				read: false,
				write: false,
			},
		}
	}

	let write = false
	try {
		await access(path, constants.W_OK)
		write = true
	} catch {}

	return {
		config,
		contextData: {
			read: true,
			write,
		},
	}
}

async function checkAcl(config?: HeadscaleConfig) {
	let path = process.env.ACL_FILE
	if (!path && config) {
		path = config.acl_policy_path
	}

	let read = false
	let write = false
	if (path) {
		try {
			await access(path, constants.R_OK)
			read = true
		} catch {}

		try {
			await access(path, constants.W_OK)
			write = true
		} catch {}
	}

	return {
		read,
		write,
	}
}

async function checkOidc(config?: HeadscaleConfig) {
	const disableKeyLogin = process.env.DISABLE_API_KEY_LOGIN === 'true'
	const rootKey = process.env.ROOT_API_KEY ?? process.env.API_KEY
	if (!rootKey) {
		throw new Error('ROOT_API_KEY or API_KEY not set')
	}

	let issuer = process.env.OIDC_ISSUER
	let client = process.env.OIDC_CLIENT_ID
	let secret = process.env.OIDC_CLIENT_SECRET

	if (
		(issuer ?? client ?? secret)
		&& !(issuer && client && secret)
		&& !config
	) {
		throw new Error('OIDC environment variables are incomplete')
	}

	if ((!issuer || !client || !secret) && config) {
		issuer = config.oidc?.issuer
		client = config.oidc?.client_id
		secret = config.oidc?.client_secret

		if (!secret && config.oidc?.client_secret_path) {
			try {
				const data = await readFile(
					config.oidc.client_secret_path,
					'utf8',
				)

				if (data && data.length > 0) {
					secret = data.trim()
				}
			} catch {}
		}
	}

	if (
		(issuer ?? client ?? secret)
		&& !(issuer && client && secret)
	) {
		throw new Error('OIDC configuration is incomplete')
	}

	if (!issuer || !client || !secret) {
		return
	}

	return {
		issuer,
		client,
		secret,
		rootKey,
		disableKeyLogin,
	}
}
