import path from "node:path";

import * as E from "fp-ts/Either";
import { flow, pipe } from "fp-ts/function";
import * as IOE from "fp-ts/IOEither";
import * as O from "fp-ts/Option";

import type { NodeCG } from "../../types/nodecg";
import { readJsonFileSync } from "../util-fp/read-json-file-sync";
import { parseAssets } from "./assets";
import { parseBundleConfig, parseDefaults } from "./config";
import { parseExtension } from "./extension";
import { parseGit } from "./git";
import { parseGraphics } from "./graphics";
import { parseManifest } from "./manifest";
import { parseMounts } from "./mounts";
import { parsePanels } from "./panels";
import { parseSounds } from "./sounds";

const readBundlePackageJson = (bundlePath: string) =>
	pipe(
		bundlePath,
		(bundlePath: string) => path.join(bundlePath, "package.json"),
		readJsonFileSync,
		IOE.map((json) => json as NodeCG.PackageJSON),
		IOE.mapLeft((error) => {
			if ((error as NodeJS.ErrnoException).code === "ENOENT") {
				return new Error(
					`Bundle at path ${bundlePath} does not contain a package.json!`,
				);
			}
			if (error instanceof SyntaxError) {
				return new Error(
					`${bundlePath}'s package.json is not valid JSON, please check it against a validator such as jsonlint.com`,
				);
			}
			return error;
		}),
	);

const parseBundleNodecgConfig = flow(
	(bundlePath: string) => path.join(bundlePath, "nodecg.config.js"),
	IOE.tryCatchK(require, E.toError),
	IOE.match(
		() => ({}),
		(config) => config.default || config,
	),
	IOE.fromIO,
	IOE.flatMap((config) => {
		if (
			typeof config !== "object" ||
			config === null ||
			Array.isArray(config)
		) {
			return IOE.left(new Error("nodecg.config.js must export an object"));
		}
		return IOE.right(config as NodeCG.NodecgBundleConfig);
	}),
);

export const parseBundle = (
	bundlePath: string,
	bundleCfg?: NodeCG.Bundle.UnknownConfig,
): NodeCG.Bundle => {
	const manifest = pipe(
		bundlePath,
		readBundlePackageJson,
		IOE.flatMap(parseManifest(bundlePath)),
		IOE.getOrElse((error) => {
			throw error;
		}),
	)();

	const dashboardDir = path.resolve(bundlePath, "dashboard");
	const graphicsDir = path.resolve(bundlePath, "graphics");

	const nodecgBundleConfig = pipe(
		parseBundleNodecgConfig(bundlePath),
		IOE.getOrElse((error) => {
			throw error;
		}),
	)();

	const config = pipe(
		bundleCfg,
		O.fromNullable,
		O.match(
			() => parseDefaults(manifest.name)(bundlePath),
			IOE.tryCatchK(
				(bundleCfg) => parseBundleConfig(manifest.name, bundlePath, bundleCfg),
				E.toError,
			),
		),
		IOE.getOrElse((error) => {
			throw error;
		}),
	)();

	const bundle: NodeCG.Bundle = {
		...manifest,
		dir: bundlePath,
		config,
		dashboard: {
			dir: dashboardDir,
			panels: parsePanels(dashboardDir, manifest),
		},
		mount: parseMounts(manifest),
		graphics: parseGraphics(graphicsDir, manifest),
		assetCategories: parseAssets(manifest),
		hasExtension: parseExtension(bundlePath, manifest),
		git: parseGit(bundlePath),
		...parseSounds(bundlePath, manifest),
		nodecgBundleConfig,
	};

	return bundle;
};
