// SPDX-FileCopyrightText: 2025 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-only

use rusqlite::params_from_iter;
use rusqlite::types::Value;
use rusqlite::ParamsFromIter;

use std::slice::Iter;

use crate::sql::Field;
use crate::sql::Join;
use crate::sql::JoinType;
use crate::sql::Prefix;
use crate::sql::Table;

/// Represents an assembled Query for rusqlite that can be
/// concatenated into an SQL-Query to add dynamic features.
#[derive(Debug, Clone)]
pub struct RusqliteQuery<F: Field> {
	/// The prefix that was used for assembling this query.
	///
	/// Not guaranteed to be accurate after concatenation.
	/// Set manually when updating is needed, leave it alone otherwise.
	///
	/// Used by [inner_join()][Self::inner_join] to get the current prefix.
	pub used_prefix: String,

	/// The dynamic part of the `WHERE` clause.
	/// Use as only query or combine with custom extra logic.
	///
	/// Can safely be concatenated into an SQL query.
	pub sql_where_clause: String,

	/// Dynamically generated `JOIN` clauses.
	///
	/// Can safely be concatenated into an SQL query.
	pub sql_joins: Vec<Join<F>>,

	/// Contains the dynamic values needed for the
	/// where clause. Additional needed values should be
	/// `.push()`ed before passing it to the [`params_from_iter()`]
	/// function to get the paramters in a form that can be used in the query.
	///
	/// [`params_from_iter()`]: rusqlite::params_from_iter
	pub where_values: Vec<Value>,
}

impl<F: Field> RusqliteQuery<F> {
	/// Negates the where clause by putting a `NOT` in front of it.
	///
	/// Template used: `NOT ({old_where_clause})`
	pub fn negate_where_clause(mut self) -> Self {
		self.sql_where_clause = format!("NOT ({})", self.sql_where_clause);
		return self;
	}

	/// Puts the where clause in parenthesis,
	/// recommended when done with `concat()` operations.
	///
	/// Template used: `({old_where_clause})`
	pub fn parenthesise_where_clause(mut self) -> Self {
		self.sql_where_clause = format!("({})", self.sql_where_clause);
		return self;
	}

	/// Wrap the where clause in a [likelihood function](https://sqlite.org/lang_corefunc.html#likelihood).
	///
	/// Template used: `likelihood(({old_where_clause}), {likelihood})`
	pub fn with_likelihood(mut self, likelihood: f32) -> Self {
		if !likelihood.is_nan() {
			self.sql_where_clause = format!(
				"likelihood(({}), {:.5})",
				self.sql_where_clause,
				likelihood.clamp(0.001, 0.999)
			)
		}
		return self;
	}

	/// Concatenates two assembled queries, merging:
	/// * where conditions
	/// * values for the placeholders in the where conditions.
	/// * join requests
	///
	/// The used prefix is kept from the query this method was used on.
	///
	/// Use `" OR "` or `" AND "` for the `where_joiner`.
	///
	/// Template used for where clause:
	/// 	{old_where_clause}{where_joiner}{other_where_clause}
	pub fn concat(mut self, mut other: Self, where_joiner: &str) -> Self {
		// Take care of where clause
		self.sql_where_clause = self.sql_where_clause + where_joiner + &other.sql_where_clause;
		// Take care of arguments for where clause
		self.where_values.append(&mut other.where_values);
		// take care of joins
		for join in other.sql_joins {
			self.add_join_clause(join);
		}
		return self;
	}

	/// Convenience wrapper for [concat()][RusqliteQuery::concat]
	/// with the `where_joiner` set to ` AND `
	#[inline]
	pub fn and(self, other: Self) -> Self {
		self.concat(other, " AND ")
	}

	/// Convenience wrapper for [concat()][RusqliteQuery::concat]
	/// with the `where_joiner` set to ` OR `
	#[inline]
	pub fn or(self, other: Self) -> Self {
		self.concat(other, " OR ")
	}

	/// Uses the given SQL-String as the where clause
	/// for simple queries without placeholders.
	///
	/// Also see [from_static_sql_str()][Self::from_static_sql_str].
	pub fn from_static_sql(where_clause: String) -> Self {
		Self {
			used_prefix: "".to_string(),
			sql_where_clause: where_clause,
			where_values: Vec::new(),
			sql_joins: Vec::new(),
		}
	}

	/// Like [from_static_sql()][Self::from_static_sql]
	/// but takes a `&str` making it easier to use.
	pub fn from_static_sql_str(where_clause: &str) -> Self {
		Self::from_static_sql(where_clause.to_string())
	}

	/// Convenience function for generating an SQL `1` or `0`.
	pub fn static_bool(value: bool) -> Self {
		if value {
			Self::from_static_sql_str("1")
		} else {
			Self::from_static_sql_str("0")
		}
	}

	/// Generate SQL that tests if a given field is null or not
	pub fn test_if_null(prefix: &Prefix, field: &F, invert: bool) -> Self {
		let mut where_clause = format!("{} is NULL", field.sql_safe_prefixed_field_name(prefix));
		if invert {
			where_clause = format!("NOT ({})", where_clause);
		}
		Self {
			used_prefix: prefix.to_string(),
			sql_where_clause: where_clause,
			where_values: Vec::new(),
			sql_joins: Vec::new(),
		}
	}

	/// Adds a join request to the internal list of join requests,
	/// but only if an equal request isn't already in there.
	pub fn add_join_clause(&mut self, join: Join<F>) {
		if !self.sql_joins.contains(&join) {
			self.sql_joins.push(join);
		}
	}

	/// Convenience function for adding an inner join.
	///
	/// This is an alias for the [join()] method with an inner join preset.
	///
	/// [join()]: [RusqliteQuery::join]
	#[inline]
	pub fn inner_join(
		self,
		reason: Option<&str>,
		connector_field: F,
		dock_prefix: Option<&Prefix>,
		dock_field: F,
	) -> Self {
		self.join(
			JoinType::Inner,
			reason,
			connector_field,
			dock_prefix,
			dock_field,
		)
	}

	/// Convenience function for adding an left join.
	///
	/// This is an alias for the [join()] method with an inner join preset.
	///
	/// Useful when one wants to join an incomplete table to a complete table.
	///
	/// [join()]: [RusqliteQuery::join]
	#[inline]
	pub fn left_join(
		self,
		reason: Option<&str>,
		connector_field: F,
		dock_prefix: Option<&Prefix>,
		dock_field: F,
	) -> Self {
		self.join(
			JoinType::Left,
			reason,
			connector_field,
			dock_prefix,
			dock_field,
		)
	}

	/// Helper function for adding a SQL join.
	///
	/// The `prefix` describes the namespace and is used for:
	/// * `alias = prefix+reason`
	/// * `dock_table = prefix+dock_table`
	///
	/// The `reason` is appended to the prefix to obtain the table name that
	/// will be the join result.
	///
	/// It may equal `source_table`,
	/// but choosing a conistent combination of what gets joined and why for the reason
	/// helps with better deduplication of joins, resulting in better performance.
	///
	/// Example reason could be:
	/// * `file_origin`
	/// * `file_linked_from`
	/// * `animal_in_category`
	///
	/// The prefix usually is given to you when
	/// implementing the [AssembleRusqliteQuery] trait.
	/// It should either be empty or en in an underscore (`_`).
	///
	/// **Important when joining across prefix boundaries:**
	/// Clean up after youself using `.with_prefix(&dock_prefix)`
	/// when the table you offer for public joining has a different prefix than
	/// the table the query was originally built for.
	///
	/// **Breaking Change coming in 3.0:**
	/// The internal prefix will automatically be set to the dock prefix
	/// so that it is easier to do this correctly in the most common cases.
	///
	/// Make sure that this change won't break your logic!
	///
	/// Also see: [Join][crate::sql::Join]
	///
	/// [AssembleRusqliteQuery]: crate::rusqlite::AssembleRusqliteQuery
	///
	/// Example Usage returning an [RusqliteQuery]:
	/// ```
	/// use criterium::rusqlite::AssemblyContext;
	/// use criterium::rusqlite::ToRusqliteSingleField;
	/// use criterium::sql::JoinType;
	/// use criterium::sql::Prefix;
	/// use criterium::sql::Table;
	/// use criterium::sql::Field;
	/// use criterium::StringCriterium;
	///
	/// // set up some example data:
	/// let assembly_context = AssemblyContext::new_root();
	/// let text_criterium = StringCriterium::Equals("example.html".to_string());
	///
	/// #[derive(Debug, Clone, PartialEq, Eq)]
	/// enum ExampleTable {
	/// 	Link,
	/// 	File
	/// }
	///
	/// impl Table for ExampleTable {
	/// 	fn sql_safe_table_name(&self) -> &str {
	/// 		match self {
	/// 			Self::Link => "links",
	/// 			Self::File => "files",
	/// 		}
	/// 	}
	/// }
	///
	///
	/// #[derive(Debug, Clone, PartialEq, Eq)]
	/// enum ExampleSchema {
	/// 	FileId,
	/// 	FileName,
	///
	/// 	LinkId,
	/// 	LinkInFileId,
	/// 	LinkToUrl,
	/// }
	///
	/// impl Field for ExampleSchema {
	///
	/// 	type TableType = ExampleTable;
	///
	/// 	fn sql_safe_field_name(&self) -> &str {
	/// 		match self {
	/// 			Self::FileId => "file_id",
	/// 			Self::FileName => "name",
	/// 			Self::LinkId => "link_id",
	/// 			Self::LinkInFileId => "in_file_id",
	/// 			Self::LinkToUrl => "to_url"
	/// 		}
	/// 	}
	///
	/// 	fn table(&self) -> &Self::TableType {
	/// 		match self {
	/// 			Self::FileId => &ExampleTable::File,
	/// 			Self::FileName => &ExampleTable::File,
	/// 			Self::LinkId => &ExampleTable::Link,
	/// 			Self::LinkInFileId => &ExampleTable::Link,
	/// 			Self::LinkToUrl => &ExampleTable::Link
	/// 		}
	/// 	}
	/// }
	///
	/// // Would crete a query for all links in the file called "example.html"
	/// text_criterium.assemble_query(&assembly_context, &ExampleSchema::FileName)
	/// 	.get_corrected_query()
	/// 	.join(
	/// 		JoinType::Inner,
	/// 		Some("contained_in"),
	/// 		ExampleSchema::FileId,
	/// 		Some(assembly_context.prefix()),
	/// 		ExampleSchema::LinkInFileId
	/// 	);
	/// ```
	///
	/// This will result in the following sql:
	/// ```sql
	///	inner join files
	///	on {prefix}contained_in.file_id = {prefix}links.in_file_id
	///	as {prefix}contained_in
	/// ```
	///
	/// [`assemble_query()`]: [Self::assemble_query]
	#[allow(clippy::too_many_arguments)]
	pub fn join(
		mut self,
		join_type: JoinType,
		reason: Option<&str>,
		connector_field: F,
		dock_prefix: Option<&Prefix>,
		dock_field: F,
	) -> Self {
		let dock_table = dock_field.table().sql_safe_table_name();
		let dock_table = if let Some(dock_prefix) = dock_prefix {
			dock_prefix.to_string() + dock_table
		} else {
			self.used_prefix.clone() + dock_table
		};
		let mut alias: Option<String> = None;
		if let Some(reason) = reason {
			alias = Some(self.used_prefix.clone() + reason);
		} else if !self.used_prefix.is_empty() {
			alias = Some(self.used_prefix.clone() + connector_field.table().sql_safe_table_name());
		}
		self.add_join_clause(Join {
			join_type: join_type,
			connector_field: connector_field,
			dock_table_alias: dock_table,
			dock_field: dock_field,
			alias: alias,
		});
		// Set the own prefix to the dock prefix if set.
		// This is more intuitive and in most cases the right thing to do.
		if let Some(dock_prefix) = dock_prefix {
			return self.with_prefix(dock_prefix);
		}
		return self;
	}

	/// Set the prefix of the query to help with chaing up
	/// joins after it being reqturned from a prefixed context.
	pub fn with_prefix(mut self, prefix: &Prefix) -> Self {
		self.used_prefix = prefix.to_string();
		return self;
	}

	/// Converts all joins in this assembled query to sql.
	///
	/// Example Output: `inner join foo on foo.x = bar.y as baz`
	pub fn joins_to_sql(&self) -> String {
		let mut out: String = "".to_string();
		let mut first = true;
		for join in &self.sql_joins {
			if first {
				first = false;
			} else {
				out += "\n";
			}
			out += &join.to_sql();
		}
		return out;
	}

	/// Shortcut for passing the `where_values` a params
	/// to a rusqlite query.
	///
	/// Equivalent to:
	/// ```rust,ignore
	/// use rusqlite::params_from_iter;
	///
	/// params_from_iter(example_criterum.where_values.iter()),
	/// ```
	///  
	pub fn where_values_as_params(&self) -> ParamsFromIter<Iter<'_, Value>> {
		params_from_iter(self.where_values.iter())
	}

	/// Turns self into an invertable, `AsRequested`.
	pub fn as_invertable(self) -> InvertableRusqliteQuery<F> {
		InvertableRusqliteQuery::AsRequested(self)
	}
}

/// Represents a rusqlite query that may be generated as requested by the semantic tree
/// or be the exact opposite as if prefixed by a NOT.
///
/// This is useful to be able to return an already inverted statement when the [LogicPath][crate::LogicPath] offers such an optimization.
pub enum InvertableRusqliteQuery<F: Field> {
	/// The Query is as requested and could be used without modification
	AsRequested(RusqliteQuery<F>),

	/// The query is inverted from what was requested,
	/// one should call `negate_where_clause` on it.
	///
	/// (See the [get_corrected_query()][Self::get_corrected_query] function.)
	Inverted(RusqliteQuery<F>),
}

impl<F: Field> InvertableRusqliteQuery<F> {
	/// Turns an `AsRequested` into an `Inverted` and vice versa.
	///
	/// This does not change the contained query,
	/// only how it should be interpreted.
	pub fn invert(self) -> Self {
		match self {
			Self::AsRequested(q) => Self::Inverted(q),
			Self::Inverted(q) => Self::AsRequested(q),
		}
	}

	/// Applies `invert()`, but only if `do_invert` is `true`.
	/// Returns without change if `do_invert` is `false`.
	pub fn invert_if(self, do_invert: bool) -> Self {
		if do_invert {
			self.invert()
		} else {
			self
		}
	}

	/// Returns wheter the query is inverted.
	pub fn is_inverted(&self) -> bool {
		matches!(self, Self::Inverted(_))
	}

	/// Returns a `RusqliteQuery` that behaves as requested,
	/// patching in a `NOT` statement if an inversion was requested.
	pub fn get_corrected_query(self) -> RusqliteQuery<F> {
		match self {
			Self::AsRequested(q) => q,
			Self::Inverted(q) => q.negate_where_clause(),
		}
	}

	/// Returns a mutable reference to the contained query
	/// to add joins or using `with_prefix()`.
	///
	/// This is intentionally not implemented as `DerefMut` to avoid
	/// being able to take the query out of context by accident.
	fn modify(&mut self) -> &mut RusqliteQuery<F> {
		match self {
			Self::AsRequested(ref mut q) => q,
			Self::Inverted(ref mut q) => q,
		}
	}

	/// Returns the inner query, **discarding inversion information**!
	fn inner(self) -> RusqliteQuery<F> {
		match self {
			Self::AsRequested(q) => q,
			Self::Inverted(q) => q,
		}
	}

	/// Apply an inner join tot the contained query.
	///
	/// See [RusqliteQuery::inner_join] for more information.
	pub fn inner_join(
		self,
		reason: Option<&str>,
		connector_field: F,
		dock_prefix: Option<&Prefix>,
		dock_field: F,
	) -> Self {
		let is_inverted = self.is_inverted();

		self.inner()
			.inner_join(reason, connector_field, dock_prefix, dock_field)
			.as_invertable()
			.invert_if(is_inverted)
	}

	/// Set the prefix on the query
	pub fn with_prefix(mut self, prefix: &Prefix) -> Self {
		self.modify().used_prefix = prefix.to_string();
		self
	}
}
