"use strict";

// FIXME: Validation for all methods
// FIXME: For validation of things where there are defaults/implicits, make sure to strictly validate either the allowed operations OR the allowed implicit values (eg. `columnName` implicits should only accept strings) - these validations can be abstracted out per type of implicit
// FIXME: Upon query compilation, keep a 'stack' of operation types, to track what context we are in (eg. because values can only be parameterized in some contexts)

// FIXME: Verify that all wrapWith calls have a corresponding isObjectType arm

require("array.prototype.flat").shim();
const splitFilter = require("split-filter");
const asExpression = require("as-expression");

const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const nestedArrayOf = require("@validatem/nested-array-of");
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
const isFunction = require("@validatem/is-function");
const defaultTo = require("@validatem/default-to");
const anyProperty = require("@validatem/any-property");

const node = require("../ast-node");

const flatten = require("../validators/flatten");
const evaluateCyclicalModulesOnto = require("../evaluate-cyclical-modules-onto");

// FIXME: Hack until all operations have been moved over to modules
const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(module.exports);
const isSelectClause = require("../validators/operations/is-select-clause")(module.exports);
const isObjectType = require("../validators/operations/is-object-type")(module.exports);

function normalizeClauses(clauses) {
	if (clauses != null) {
		return clauses.flat(Infinity);
	} else {
		return clauses;
	}
}

// FIXME: All of the below need to be refactored, and moved into operation modules
let operations = {
	withRelations: function (relations) {
		// FIXME: Default relation types for literal column names and table.column syntax
		// FIXME: Flesh this out further
		// Main types: hasMany, belongsTo; hasOne is just hasMany with a 'single' constraint; simplify hasMany to 'has'?
		// through relation is actually hasMany(through(...)) -- how to make `through` itself composable?
		// test case for composable through: user -> membership -> usergroup -> community
		// should fold composed throughs in compiler step

		return node({
			type: "withRelations",
			relations: Object.entries(relations).map(([ key, relation ]) => {
				return {
					key: key,
					relation: normalizeRelation(relation)
				};
			})
		});
	},
	withDerived: function (_derivations) {
		let [ derivations ] = validateArguments(arguments, {
			derivations: [ required, anyProperty({
				key: [ required, isString ], // FIXME: Verify that this is not a foreign column name?
				value: [ required, either([
					[ isFunction ],
					[ isObjectType("sqlExpression") ]
				])]
			})]
		});

		let derivationEntries = Object.entries(derivations);

		let [ functionTransforms, sqlTransforms ] = splitFilter(derivationEntries, ([ _key, value ]) => {
			return (typeof value === "function");
		});

		let postProcessClauses = asExpression(() => {
			return functionTransforms.map(([ key, handler ]) => {
				return module.exports.postProcess((results) => {
					return results.map((result) => {
						return {
							... result,
							[key]: handler(result)
						};
					});
				});
			});
		});

		let columnClause = asExpression(() => {
			if (sqlTransforms.length > 0) {
				return module.exports.addColumns(sqlTransforms.map(([ key, expression ]) => {
					return module.exports.alias(key, expression);
				}));
			}
		});

		return [
			postProcessClauses,
			columnClause
		].filter((clause) => clause != null);
	},
	has: function (_column, _options) {
		let [ column, { query }] = validateArguments(arguments, {
			column: [ required, isPossiblyForeignColumn ],
			options: [ defaultTo({}), {
				query: [ defaultTo([]), nestedArrayOf(isSelectClause), flatten ]
			}]
		});
		
		// column: string or columnName or foreignColumnName
		// query: array of clauses
		return node({
			type: "has",
			column: normalizePossiblyRemoteColumnName(column),
			clauses: normalizeClauses(query)
		});
	},
	belongsTo: function (column, { query } = {}) {
		// column: string or columnName or foreignColumnName
		// query: array of clauses
		return node({
			type: "belongsTo",
			column: normalizePossiblyRemoteColumnName(column),
			clauses: normalizeClauses(query)
		});
	},
	// FIXME: Refactor below
	through: function (relations) {
		// relations: array of has/belongsTo or string or columnName or foreignColumnName
		return node({
			type: "through",
			relations: relations.map(normalizeRelation)
		});
	}
};

let operationModules = {
	// Base operations
	select: require("./select"),

	// Column selection
	addColumns: require("./add-columns"),
	onlyColumns: require("./only-columns"),
	alias: require("./alias"),

	// Reference/scalar types
	column: require("./column"),
	foreignColumn: require("./column"),
	table: require("./table"),
	value: require("./value"),

	// Filtering
	where: require("./where"),
	expression: require("./expression"),

	// Predicate lists/combinators
	allOf: require("./all-of"),
	anyOf: require("./any-of"),
	
	// Conditions
	equals: require("./equals"),
	lessThan: require("./less-than"),
	moreThan: require("./more-than"),
	not: require("./not"),

	// Collapsing/grouping
	collapseBy: require("./collapse-by"),
	hierarchical: require("./hierarchical"),

	// Computation
	compute: require("./compute"),
	
	// Aggregrate functions
	count: require("./count"),
	sum: require("./sum"),
	
	// Misc.
	parameter: require("./parameter"),
	postProcess: require("./post-process"),
	unsafeSQL: require("./unsafe-sql"),
};

Object.assign(module.exports, operations);
evaluateCyclicalModulesOnto(module.exports, operationModules);

// module.exports = {
// 	... operations,
// 	... evaluateCyclicalModules(operationModules)
// };

// function normalizeNotExpression(input) {
// 	if (input == null || typeOf(input) === "condition") {
// 		return input;
// 	} else {
// 		return module.exports.equals(input);
// 	}
// }

// function normalizePossiblyRemoteColumnName(input) {
// 	// FIXME: Validation

// 	if (typeof input === "object") {
// 		// FIXME: Better check
// 		return input;
// 	} else if (input != null) {
// 		if (input.includes(".")) {
// 			return module.exports.foreignColumn(input);
// 		} else {
// 			return module.exports.column(input);
// 		}
// 	} else {
// 		return input;
// 	}
// }

// function normalizeRelation(input) {
// 	// FIXME: Validation
// 	// accept columnName, foreignColumnName, string with or without dot

// 	if (typeof input === "object" && [ "has", "belongsTo", "through" ].includes(typeOf(input))) {
// 		return input;
// 	} else {
// 		let columnName = (typeof input === "string")
// 			? normalizePossiblyRemoteColumnName(input)
// 			: input;

// 		if (typeOf(columnName) === "columnName") {
// 			return module.exports.belongsTo(input);
// 		} else if (typeOf(columnName) === "foreignColumnName") {
// 			return module.exports.has(input);
// 		} else {
// 			unreachable(`Invalid type: ${typeOf(columnName)}`);
// 		}
// 	}
// }

// NOTE: normalizeExpression should sometimes only accept sql/literal, but sometimes also sql/literal/condition?
