import { argv } from "yargs";
import { z } from "zod";

import { LogLevels } from "./logger-interface";

const argvSchema = z.object({
	bundlesEnabled: z
		.string()
		.optional()
		.transform((val) => val?.split(",")),
	bundlesDisabled: z
		.string()
		.optional()
		.transform((val) => val?.split(",")),
	bundlesPaths: z
		.string()
		.optional()
		.transform((val) => val?.split(",")),
});

const parsedArgv = argvSchema.parse(argv);

export const nodecgConfigSchema = z
	.object({
		host: z
			.string()
			.default("0.0.0.0")
			.describe("The IP address or hostname that NodeCG should bind to."),
		port: z
			.number()
			.int()
			.positive()
			.default(9090)
			.describe("The port that NodeCG should listen on."),
		baseURL: z
			.string()
			.optional()
			.describe(
				"The URL of this instance. Used for things like cookies. Defaults to HOST:PORT. If you use a reverse proxy, you'll likely need to set this value.",
			),
		exitOnUncaught: z
			.boolean()
			.default(true)
			.describe("Whether or not to exit on uncaught exceptions."),

		logging: z
			.object({
				console: z
					.object({
						enabled: z
							.boolean()
							.default(true)
							.describe("Whether to enable console logging."),
						level: z
							.enum(LogLevels)
							.default("info")
							.describe("The log level to use."),
						timestamps: z
							.boolean()
							.default(true)
							.describe("Whether to add timestamps to the console logging."),
						replicants: z
							.boolean()
							.default(false)
							.describe(
								"Whether to enable logging of the Replicants subsystem. Very spammy.",
							),
					})
					.default({ enabled: true }),
				file: z
					.object({
						enabled: z
							.boolean()
							.default(false)
							.describe("Whether to enable file logging."),
						level: z
							.enum(LogLevels)
							.default("info")
							.describe("The log level to use."),
						path: z
							.string()
							.default("logs/nodecg.log")
							.describe("The filepath to log to."),
						timestamps: z
							.boolean()
							.default(true)
							.describe("Whether to add timestamps to the file logging."),
						replicants: z
							.boolean()
							.default(false)
							.describe(
								"Whether to enable logging of the Replicants subsystem. Very spammy.",
							),
					})
					.default({ enabled: false }),
			})
			.default({ console: {} }),

		bundles: z
			.object({
				enabled: z
					.array(z.string())
					.nullable()
					.default(parsedArgv.bundlesEnabled ?? null)
					.describe("A whitelist array of bundle names."),
				disabled: z
					.array(z.string())
					.nullable()
					.default(parsedArgv.bundlesDisabled ?? null)
					.describe("A blacklist array of bundle names."),
				paths: z
					.array(z.string())
					.default(parsedArgv.bundlesPaths ?? [])
					.describe("An array of additional paths where bundles are located."),
			})
			.default({
				enabled: null,
				disabled: null,
				paths: [],
			}),

		login: z
			.object({
				enabled: z
					.boolean()
					.default(false)
					.describe("Whether to enable login security."),
				sessionSecret: z
					.string()
					.optional()
					.describe("The secret used to salt sessions."),
				forceHttpsReturn: z
					.boolean()
					.default(false)
					.describe(
						"Forces Steam & Twitch login return URLs to use HTTPS instead of HTTP. Useful in reverse proxy setups.",
					),

				steam: z
					.object({
						enabled: z
							.boolean()
							.default(false)
							.describe("Whether to enable Steam authentication."),
						apiKey: z
							.string()
							.optional()
							.describe(
								"A Steam API Key. Obtained from http://steamcommunity.com/dev/apikey",
							),
						allowedIds: z
							.array(z.string())
							.default([])
							.describe(
								"Which 64 bit Steam IDs to allow. Can be obtained from https://steamid.io/",
							),
					})
					.optional()
					.refine(
						(val) => (val?.enabled ? typeof val.apiKey === "string" : true),
						{
							message: '"login.steam.apiKey" must be a string',
						},
					),

				twitch: z
					.object({
						enabled: z
							.boolean()
							.default(false)
							.describe("Whether to enable Twitch authentication."),
						clientID: z
							.string()
							.optional()
							.describe(
								"A Twitch application ClientID http://twitch.tv/kraken/oauth2/clients/new",
							),
						clientSecret: z
							.string()
							.optional()
							.describe(
								"A Twitch application ClientSecret http://twitch.tv/kraken/oauth2/clients/new",
							),
						scope: z
							.string()
							.default("user_read")
							.describe(
								"A space-separated string of Twitch application permissions.",
							),
						allowedUsernames: z
							.array(z.string())
							.default([])
							.describe("Which Twitch usernames to allow."),
						allowedIds: z
							.array(z.string())
							.default([])
							.describe(
								"Which Twitch IDs to allow. Can be obtained from https://twitchinsights.net/checkuser",
							),
					})
					.optional()
					.refine(
						(val) => (val?.enabled ? typeof val.clientID === "string" : true),
						{
							message: '"login.twitch.clientID" must be a string',
						},
					)
					.refine(
						(val) =>
							val?.enabled ? typeof val.clientSecret === "string" : true,
						{
							message: '"login.twitch.clientID" must be a string',
						},
					),

				discord: z
					.object({
						enabled: z
							.boolean()
							.default(false)
							.describe("Whether to enable Discord authentication."),
						clientID: z
							.string()
							.optional()
							.describe(
								"A Discord application ClientID https://discord.com/developers/applications",
							),
						clientSecret: z
							.string()
							.optional()
							.describe(
								"A Discord application ClientSecret https://discord.com/developers/applications",
							),
						scope: z
							.string()
							.default("identify")
							.describe(
								"A space-separated string of Discord application scopes. https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes",
							),
						allowedUserIDs: z
							.array(z.string())
							.default([])
							.describe("Which Discord user IDs to allow."),
						allowedGuilds: z
							.array(
								z.object({
									guildID: z
										.string()
										.describe(
											"Users in this Discord Server are allowed to log in.",
										),
									allowedRoleIDs: z
										.array(z.string())
										.default([])
										.describe(
											"Additionally require one of the roles on the server to log in.",
										),
									guildBotToken: z
										.string()
										.default("")
										.describe(
											"Discord bot token, needed if allowedRoleIDs is used.",
										),
								}),
							)
							.default([]),
					})
					.optional()
					.refine(
						(val) => (val?.enabled ? typeof val.clientID === "string" : true),
						{
							message: '"login.discord.clientID" must be a string',
						},
					)
					.refine(
						(val) =>
							val?.enabled ? typeof val.clientSecret === "string" : true,
						{
							message: '"login.discord.clientSecret" must be a string',
						},
					),

				local: z
					.object({
						enabled: z
							.boolean()
							.default(false)
							.describe("Enable Local authentication."),
						allowedUsers: z
							.array(
								z.object({
									username: z.string(),
									password: z.string(),
								}),
							)
							.default([])
							.describe("Which users can log in."),
					})
					.optional(),
			})
			.refine(
				(val) => (val.enabled ? typeof val.sessionSecret === "string" : true),
				{
					message: '"login.sessionSecret" must be a string',
				},
			)
			.default({ enabled: false }),

		ssl: z
			.object({
				enabled: z
					.boolean()
					.default(false)
					.describe("Whether to enable SSL/HTTPS encryption."),
				allowHTTP: z
					.boolean()
					.default(false)
					.describe(
						"Whether to allow insecure HTTP connections while SSL is active.",
					),
				keyPath: z.string().optional().describe("The path to an SSL key file."),
				certificatePath: z
					.string()
					.optional()
					.describe("The path to an SSL certificate file."),
				passphrase: z
					.string()
					.optional()
					.describe("The passphrase for the provided key file."),
			})
			.default({ enabled: false })
			.refine((val) => (val.enabled ? typeof val.keyPath === "string" : true), {
				message: '"ssl.keyPath" must be a string',
			})
			.refine(
				(val) => (val.enabled ? typeof val.certificatePath === "string" : true),
				{
					message: '"ssl.certificatePath" must be a string',
				},
			),

		sentry: z
			.object({
				enabled: z
					.boolean()
					.default(true)
					.describe("Whether to enable Sentry error reporting."),
				dsn: z
					.string()
					.optional()
					.describe(
						"Your project's DSN, used to route alerts to the correct place.",
					),
			})
			.default({ enabled: false })
			.refine((val) => (val.enabled ? typeof val.dsn === "string" : true), {
				message: '"sentry.dsn" must be a string',
			}),
	})
	.transform((val) => {
		const host = val.host === "0.0.0.0" ? "localhost" : val.host;
		return {
			...val,
			baseURL: val.baseURL ?? `${host}:${val.port}`,
		};
	});

export type NodeCGConfig = z.infer<typeof nodecgConfigSchema>;
