import path from "path";
import { FileSystem } from "./fs.js";
import { ServiceAnalyzer, ConfigVar, ConfigVarCategory, ConfigVarSemanticType } from "./analyze.js";
import { parse as parseDotenv } from "dotenv";
import { parsePrismaSchema } from "@loancrate/prisma-schema-parser";
import { augmentConfigVar } from "./semantics.js";
import { expandValue } from "./env.js";
import { z } from "zod";

const packageJsonFileName = "package.json";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const packageJsonSchema = z.object({
	name: z.optional(z.string()),
	version: z.optional(z.string()),
	engines: z.optional(
		z.object({
			node: z.optional(z.string()),
			deno: z.optional(z.string()),
		}),
	),
	dependencies: z.optional(z.record(z.string(), z.string())),
	devDependencies: z.optional(z.record(z.string(), z.string())),
});

type PackageJson = z.infer<typeof packageJsonSchema>;

interface ConfigVarDetector {
	(fs: FileSystem, root: string, packageJson: PackageJson): Promise<ConfigVar | ConfigVar[] | null>;
}

// TODO(gio): add bun, deno, ...
type NodeJSPackageManager =
	| {
			name: "npm";
			version?: string;
	  }
	| {
			name: "pnpm";
			version?: string;
	  }
	| {
			name: "yarn";
			version?: string;
	  };

type Runtime =
	| {
			name: "node";
			version?: string;
	  }
	| {
			name: "deno";
			version?: string;
	  };

const defaultRuntime: Runtime = {
	name: "node",
};

const defaultPackageManager: NodeJSPackageManager = {
	name: "npm",
};

export class NodeJSAnalyzer implements ServiceAnalyzer {
	detect(fs: FileSystem, root: string) {
		const packageJsonPath = path.join(root, packageJsonFileName);
		if (!fs.exists(packageJsonPath)) {
			return false;
		}
		// TODO(gio): maybe it's deno
		return true;
	}

	async analyze(fs: FileSystem, root: string) {
		const packageJsonPath = path.join(root, packageJsonFileName);
		const packageJson = JSON.parse(await fs.readFile(packageJsonPath));
		const runtime = this.detectRuntime(packageJson);
		const packageManager = this.detectPackageManager(fs, root);
		console.log(runtime, packageManager);
		let envVars = await this.detectEnvVars(fs, root);
		const detectors: ConfigVarDetector[] = [this.detectPrismaSchema, this.detectNextjs, this.detectExpressjs];
		const all = await Promise.all(
			detectors.map(async (detector) => {
				return await detector(fs, root, packageJson);
			}),
		);
		all.map((cv) => {
			if (Array.isArray(cv)) {
				cv.forEach((v) => this.mergeConfigVars(envVars, v));
			} else {
				this.mergeConfigVars(envVars, cv);
			}
		});
		envVars = envVars.filter((v) => v.semanticType != ConfigVarSemanticType.EXPANDED_ENV_VAR);
		envVars.forEach((v) => augmentConfigVar(v));
		return {
			name: "name" in packageJson ? packageJson.name : "NodeJS",
			location: root,
			configVars: envVars,
			commands: [],
		};
	}

	private mergeConfigVars(configVars: ConfigVar[], v: ConfigVar | null) {
		if (v == null) {
			return;
		}
		const existing = configVars.find((c) => c.name === v.name);
		if (existing != null) {
			existing.category = existing.category ?? v.category;
			existing.semanticType = existing.semanticType ?? v.semanticType;
			existing.defaultValue = existing.defaultValue ?? v.defaultValue;
			existing.description = existing.description ?? v.description;
			existing.required = existing.required ?? v.required;
			existing.sensitive = v.sensitive;
		} else {
			configVars.push(v);
		}
	}

	private detectRuntime(packageJson: PackageJson): Runtime {
		if (packageJson.engines && packageJson.engines.node) {
			return {
				name: "node",
				version: packageJson.engines.node,
			};
		} else if (packageJson.engines && packageJson.engines.deno) {
			return {
				name: "deno",
				version: packageJson.engines.deno,
			};
		}
		return defaultRuntime;
	}

	private detectPackageManager(fs: FileSystem, root: string): NodeJSPackageManager | null {
		if (fs.exists(path.join(root, "package-lock.yaml"))) {
			return {
				name: "npm",
			};
		} else if (fs.exists(path.join(root, "pnpm-lock.yaml"))) {
			return {
				name: "pnpm",
			};
		} else if (fs.exists(path.join(root, "yarn.lock"))) {
			return {
				name: "yarn",
			};
		}
		return defaultPackageManager;
	}

	private async detectEnvVars(fs: FileSystem, root: string): Promise<ConfigVar[]> {
		const envFilePath = path.join(root, ".env");
		if (!fs.exists(envFilePath)) {
			return [];
		}
		const envVars: ConfigVar[] = [];
		const fileContent = await fs.readFile(envFilePath);
		const parsedEnv = parseDotenv(fileContent);
		for (const key in parsedEnv) {
			if (Object.prototype.hasOwnProperty.call(parsedEnv, key)) {
				const defaultValue = parsedEnv[key];
				const vars = expandValue(defaultValue);
				envVars.push({
					name: key,
					defaultValue,
					category: ConfigVarCategory.EnvironmentVariable,
					semanticType: ConfigVarSemanticType.EXPANDED_ENV_VAR,
				});
				vars.forEach((v) => {
					envVars.push({
						name: v,
						defaultValue: "", // TODO(gio): add default value
						category: ConfigVarCategory.EnvironmentVariable,
					});
				});
			}
		}
		return envVars;
	}

	private async detectPrismaSchema(
		fs: FileSystem,
		root: string,
		packageJson: PackageJson,
	): Promise<ConfigVar | ConfigVar[] | null> {
		if (packageJson?.dependencies?.prisma == null && packageJson?.devDependencies?.prisma == null) {
			return null;
		}
		let schemaPath = path.join(root, "prisma", "schema.prisma");
		if (!fs.exists(schemaPath)) {
			schemaPath = path.join(root, "schema.prisma");
			if (!fs.exists(schemaPath)) {
				return null;
			}
		}
		const schemaContent = await fs.readFile(schemaPath);
		const ast = parsePrismaSchema(schemaContent);
		let urlVar: string | null = null;
		let dbType: ConfigVarSemanticType | null = null;
		for (const element of ast.declarations) {
			if (element.kind === "datasource") {
				for (const prop of element.members) {
					if (prop.kind === "config") {
						switch (prop.name.value) {
							case "url": {
								if (
									prop.value.kind === "functionCall" &&
									prop.value.path.value[0] === "env" &&
									prop.value.args != null
								) {
									const arg = prop.value.args[0];
									if (arg.kind === "literal" && typeof arg.value === "string") {
										urlVar = arg.value;
									}
								}
								break;
							}
							case "provider": {
								if (prop.value.kind === "literal" && typeof prop.value.value === "string") {
									switch (prop.value.value) {
										case "postgresql": {
											dbType = ConfigVarSemanticType.POSTGRES_URL;
											break;
										}
										case "sqlite": {
											dbType = ConfigVarSemanticType.SQLITE_PATH;
											break;
										}
										default: {
											throw new Error(`Unsupported database type: ${prop.value.value}`);
										}
									}
								}
								break;
							}
						}
					}
				}
			}
		}
		if (urlVar == null || dbType == null) {
			return null;
		}
		return {
			name: urlVar,
			category: ConfigVarCategory.EnvironmentVariable,
			semanticType: dbType,
		};
	}

	private async detectNextjs(fs: FileSystem, root: string): Promise<ConfigVar | ConfigVar[] | null> {
		const nextConfigPath = path.join(root, "next.config.mjs");
		if (!fs.exists(nextConfigPath)) {
			return null;
		}
		return {
			name: "PORT",
			category: ConfigVarCategory.EnvironmentVariable,
			semanticType: ConfigVarSemanticType.PORT,
		};
	}

	private async detectExpressjs(
		fs: FileSystem,
		root: string,
		packageJson: PackageJson,
	): Promise<ConfigVar | ConfigVar[] | null> {
		if (packageJson?.dependencies?.express == null && packageJson?.devDependencies?.express == null) {
			return null;
		}
		return {
			name: "PORT",
			category: ConfigVarCategory.EnvironmentVariable,
			semanticType: ConfigVarSemanticType.PORT,
		};
	}
}
