/**
 * SCIM 2.0 Query Builder
 *
 * Type-safe query builder for SCIM 2.0 filtering.
 *
 * @example
 * ```typescript
 * // Array-based (recommended for UI)
 * const filters: QueryConditions<Person> = [
 *   { field: 'first_name', op: 'eq', value: 'John' },
 *   { field: 'age', op: 'gt', value: 18, connector: 'and' },
 * ]
 * buildQuery(filters) // → 'first_name eq "John" and age gt 18'
 *
 * // Range query (same field twice)
 * const rangeFilters: QueryConditions<Donation> = [
 *   { field: 'amount', op: 'ge', value: 100 },
 *   { field: 'amount', op: 'le', value: 500, connector: 'and' },
 * ]
 * buildQuery(rangeFilters) // → 'amount ge 100 and amount le 500'
 * ```
 */

type Primitive = string | number | boolean | null

type DeepKeyOf<T> = T extends Primitive
	? never
	: {
		[K in keyof T & string]: K | `${K}.${DeepKeyOf<T[K]>}`;
	}[keyof T & string]

/**
 * SCIM 2.0 comparison operators
 */
export type ComparisonOperator
	= | 'eq' // equal
		| 'ne' // not equal
		| 'gt' // greater than
		| 'ge' // greater than or equal
		| 'lt' // less than
		| 'le' // less than or equal
		| 'co' // contains
		| 'sw' // starts with
		| 'ew' // ends with
		| 'pr' // present (has value)
		| 'in' // in list — expands to (field eq "a" or field eq "b" …)
		| 'nin' // not in list — expands to (field ne "a" and field ne "b" …)

/**
 * SCIM 2.0 logical operators
 */
export type LogicalOperator = 'and' | 'or'

/**
 * A single filter condition
 */
export interface FilterCondition<T extends Record<string, any> = Record<string, any>> {
	field: DeepKeyOf<T> | string
	op: ComparisonOperator
	value?: Primitive | Primitive[] // array only used with 'in'/'nin'
	connector?: LogicalOperator // before this condition, ignored for first
}

/**
 * Array of filter conditions - the primary way to define queries
 */
export type QueryConditions<T extends Record<string, any> = Record<string, any>> = FilterCondition<T>[]

/**
 * Format a value for SCIM 2.0 query
 */
function formatValue(value: Primitive): string {
	if (value === null) return 'null'
	if (typeof value === 'string') {
		const escaped = value.replace(/"/g, '\\"')
		return `"${escaped}"`
	}
	if (typeof value === 'boolean') return value.toString()
	return String(value)
}

/**
 * Build a SCIM 2.0 query string from conditions array
 *
 * @example
 * ```typescript
 * buildQuery<Person>([
 *   { field: 'name', op: 'eq', value: 'John' },
 *   { field: 'age', op: 'gt', value: 18, connector: 'and' },
 * ])
 * // → 'name eq "John" and age gt 18'
 * ```
 */
export function buildQuery<T extends Record<string, any>>(
	conditions?: QueryConditions<T>
): string {
	if (!conditions || conditions.length === 0) return ''

	const parts: string[] = []

	for (let i = 0; i < conditions.length; i++) {
		const { field, op, value, connector } = conditions[i]

		// Add connector (except for first condition)
		if (i > 0 && connector) {
			parts.push(connector)
		}

		// Build the condition
		if (op === 'pr') {
			parts.push(`${field} pr`)
		} else if (op === 'in' || op === 'nin') {
			const values = Array.isArray(value) ? value : (value != null ? [value] : [])
			if (values.length === 0) continue
			const innerOp = op === 'in' ? 'eq' : 'ne'
			const innerConn = op === 'in' ? 'or' : 'and'
			const inner = values.map(v => `${field} ${innerOp} ${formatValue(v)}`).join(` ${innerConn} `)
			parts.push(values.length > 1 ? `(${inner})` : inner)
		} else {
			parts.push(`${field} ${op} ${formatValue(value as Primitive ?? null)}`)
		}
	}

	return parts.join(' ')
}

/**
 * Query Builder for SCIM 2.0 filtering
 */
export class QueryFilter<T extends Record<string, any>> {
	private parts: string[] = []

	/**
	 * Equal comparison
	 */
	eq<K extends DeepKeyOf<T>>(field: K, value: Primitive): this {
		this.parts.push(`${field as string} eq ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Not equal comparison
	 */
	ne<K extends DeepKeyOf<T>>(field: K, value: Primitive): this {
		this.parts.push(`${field as string} ne ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Greater than comparison
	 */
	gt<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
		this.parts.push(`${field as string} gt ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Greater than or equal comparison
	 */
	ge<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
		this.parts.push(`${field as string} ge ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Less than comparison
	 */
	lt<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
		this.parts.push(`${field as string} lt ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Less than or equal comparison
	 */
	le<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
		this.parts.push(`${field as string} le ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Contains comparison (for strings)
	 */
	co<K extends DeepKeyOf<T>>(field: K, value: string): this {
		this.parts.push(`${field as string} co ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Starts with comparison (for strings)
	 */
	sw<K extends DeepKeyOf<T>>(field: K, value: string): this {
		this.parts.push(`${field as string} sw ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Ends with comparison (for strings)
	 */
	ew<K extends DeepKeyOf<T>>(field: K, value: string): this {
		this.parts.push(`${field as string} ew ${QueryFilter.formatValue(value)}`)
		return this
	}

	/**
	 * Present check (field has a value)
	 */
	pr<K extends DeepKeyOf<T>>(field: K): this {
		this.parts.push(`${field as string} pr`)
		return this
	}

	/**
	 * AND logical operator
	 */
	and(): this {
		this.parts.push('and')
		return this
	}

	/**
	 * OR logical operator
	 */
	or(): this {
		this.parts.push('or')
		return this
	}

	/**
	 * NOT logical operator
	 */
	not(): this {
		this.parts.push('not')
		return this
	}

	/**
	 * Group expressions with parentheses
	 */
	group(builderFn: (qb: QueryFilter<T>) => QueryFilter<T>): this {
		const groupBuilder = new QueryFilter<T>()
		builderFn(groupBuilder)
		const groupQuery = groupBuilder.build()
		if (groupQuery) {
			this.parts.push(`(${groupQuery})`)
		}
		return this
	}

	/**
	 * Add raw query string (use with caution)
	 */
	raw(query: string): this {
		this.parts.push(query)
		return this
	}

	/**
	 * Clear the query
	 */
	clear(): this {
		this.parts = []
		return this
	}

	/**
	 * Build the final query string
	 */
	build(): string {
		return this.parts.join(' ')
	}

	/**
	 * Convert to string automatically (allows using builder without .build())
	 */
	toString(): string {
		return this.build()
	}

	/**
	 * Allow implicit string conversion
	 */
	[Symbol.toPrimitive](hint: string): string | number {
		if (hint === 'string') {
			return this.build()
		}
		return this.parts.length
	}

	/**
	 * Format value for SCIM 2.0 query
	 */
	private static formatValue(value: Primitive): string {
		if (value === null) {
			return 'null'
		}
		if (typeof value === 'string') {
			// Escape quotes in strings
			const escaped = value.replace(/"/g, '\\"')
			return `"${escaped}"`
		}
		if (typeof value === 'boolean') {
			return value.toString()
		}
		return String(value)
	}
}

/**
 * Build a query string from conditions or create a chainable query builder
 *
 * @example
 * ```typescript
 * // From conditions array
 * queryFilter<Person>([
 *   { field: 'first_name', op: 'eq', value: 'John' },
 *   { field: 'age', op: 'gt', value: 18, connector: 'and' },
 * ]).build()
 * // → 'first_name eq "John" and age gt 18'
 *
 * // Chainable builder
 * queryFilter<Person>().eq('first_name', 'John').and().gt('age', 18).build()
 * // → 'first_name eq "John" and age gt 18'
 *
 * // Empty returns empty builder
 * queryFilter() // → QueryFilter instance
 * ```
 */
export function queryFilter<T extends Record<string, any>>(
	conditions?: QueryConditions<T>
): QueryFilter<T> {
	const builder = new QueryFilter<T>()
	if (conditions && conditions.length > 0) {
		builder.raw(buildQuery(conditions))
	}
	return builder
}

/**
 * Create a query that checks if any of the fields match the value
 */
export function anyOf<T extends Record<string, any>>(
	fields: Array<DeepKeyOf<T> | string>,
	value: Primitive
): string {
	const conditions: QueryConditions<T> = fields.map((field, i) => ({
		field,
		op: 'eq' as const,
		value,
		connector: i > 0 ? 'or' as const : undefined,
	}))
	return buildQuery(conditions)
}

/**
 * Create a range query (field >= min and field <= max)
 */
export function range<T extends Record<string, any>>(
	field: DeepKeyOf<T> | string,
	min: number | string,
	max: number | string
): string {
	return buildQuery<T>([
		{ field, op: 'ge', value: min },
		{ field, op: 'le', value: max, connector: 'and' },
	])
}

/**
 * Create a search query across multiple string fields (OR)
 */
export function search<T extends Record<string, any>>(
	fields: Array<DeepKeyOf<T> | string>,
	searchTerm: string
): string {
	const conditions: QueryConditions<T> = fields.map((field, i) => ({
		field,
		op: 'co' as const,
		value: searchTerm,
		connector: i > 0 ? 'or' as const : undefined,
	}))
	return buildQuery(conditions)
}

/**
 * Evaluate a SCIM 2.0 query string against a data object.
 * Returns true if the data matches, or if the query is empty/invalid.
 *
 * @example
 * ```typescript
 * evaluateQuery('status eq "active" and age gt 18', { status: 'active', age: 25 }) // → true
 * evaluateQuery('status eq "active" and age gt 18', { status: 'inactive', age: 25 }) // → false
 * ```
 */
export function evaluateQuery(query: string, data: Record<string, any>): boolean {
	if (!query) return true

	try {
		const conditions = parseQuery(query)
		if (conditions.length === 0) return true

		let result = true
		let prev: boolean | null = null

		for (let i = 0; i < conditions.length; i++) {
			const { field, op, value, connector } = conditions[i]
			const fieldValue = getNestedValue(data, field as string)

			let match = false
			switch (op) {
				case 'eq': match = fieldValue === value; break
				case 'ne': match = fieldValue !== value; break
				case 'gt': match = value != null && fieldValue > value; break
				case 'ge': match = value != null && fieldValue >= value; break
				case 'lt': match = value != null && fieldValue < value; break
				case 'le': match = value != null && fieldValue <= value; break
				case 'co': match = value != null && String(fieldValue).includes(String(value)); break
				case 'sw': match = value != null && String(fieldValue).startsWith(String(value)); break
				case 'ew': match = value != null && String(fieldValue).endsWith(String(value)); break
				case 'pr': match = fieldValue != null; break
				default: break
			}

			if (i === 0) {
				result = match
				prev = match
			} else {
				result = connector === 'or' ? (prev ?? false) || match : (prev ?? false) && match
				prev = result
			}
		}

		return result
	} catch {
		return true
	}
}

function getNestedValue(obj: Record<string, any>, path: string): any {
	return path.split('.').reduce<any>((acc, key) => acc?.[key], obj)
}

const OPERATORS: ComparisonOperator[] = ['eq', 'ne', 'gt', 'ge', 'lt', 'le', 'co', 'sw', 'ew', 'pr', 'in', 'nin']
const CONNECTORS: LogicalOperator[] = ['and', 'or']

/**
 * Parse a value string into a Primitive
 */
function parseValue(valueStr: string): Primitive {
	const trimmed = valueStr.trim()
	if (trimmed === 'null') return null
	if (trimmed === 'true') return true
	if (trimmed === 'false') return false
	if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
		// Unescape quotes in strings
		return trimmed.slice(1, -1).replace(/\\"/g, '"')
	}
	const num = Number(trimmed)
	if (!Number.isNaN(num) && trimmed !== '') return num
	return trimmed
}

/**
 * Parse a SCIM 2.0 query string into conditions array
 * Supports grouping with parentheses: (a eq 1 or b eq 2) and c eq 3
 *
 * @example
 * ```typescript
 * parseQuery('first_name eq "John" and age gt 18')
 * // → [
 * //   { field: 'first_name', op: 'eq', value: 'John' },
 * //   { field: 'age', op: 'gt', value: 18, connector: 'and' },
 * // ]
 *
 * parseQuery('(item eq "A" or item eq "B") and status eq "active"')
 * // → [
 * //   { field: 'item', op: 'eq', value: 'A' },
 * //   { field: 'item', op: 'eq', value: 'B', connector: 'or' },
 * //   { field: 'status', op: 'eq', value: 'active', connector: 'and' },
 * // ]
 * ```
 */
export function parseQuery<T extends Record<string, any> = Record<string, any>>(
	queryString?: string
): QueryConditions<T> {
	if (queryString == null || queryString.trim() === '') return []

	const tokens = tokenize(queryString)
	return parseTokens<T>(tokens, 0, tokens.length).conditions
}

interface ParseResult<T extends Record<string, any>> {
	conditions: QueryConditions<T>
	endIndex: number
}

function parseTokens<T extends Record<string, any>>(
	tokens: string[],
	start: number,
	end: number
): ParseResult<T> {
	const conditions: QueryConditions<T> = []
	let i = start

	while (i < end) {
		let connector: LogicalOperator | undefined

		// Check for connector (and/or)
		if (conditions.length > 0 && CONNECTORS.includes(tokens[i]?.toLowerCase() as LogicalOperator)) {
			connector = tokens[i].toLowerCase() as LogicalOperator
			i++
		}

		if (i >= end) break

		// Handle opening parenthesis - parse grouped expression
		if (tokens[i] === '(') {
			// Find matching closing parenthesis
			let depth = 1
			let closeIndex = i + 1
			while (closeIndex < end && depth > 0) {
				if (tokens[closeIndex] === '(') depth++
				else if (tokens[closeIndex] === ')') depth--
				closeIndex++
			}

			// Parse the group recursively
			const groupResult = parseTokens<T>(tokens, i + 1, closeIndex - 1)

			// Apply connector to first condition in group
			if (connector && groupResult.conditions.length > 0) {
				groupResult.conditions[0].connector = connector
			}

			conditions.push(...groupResult.conditions)
			i = closeIndex
			continue
		}

		// Handle closing parenthesis - should not happen at top level
		if (tokens[i] === ')') {
			i++
			continue
		}

		const field = tokens[i++]
		if (i >= end) break

		const op = tokens[i++]?.toLowerCase() as ComparisonOperator
		if (!OPERATORS.includes(op)) {
			throw new Error(`Invalid operator: ${op}`)
		}

		let value: Primitive | undefined
		if (op !== 'pr') {
			if (i >= end) {
				throw new Error(`Missing value for operator ${op}`)
			}
			value = parseValue(tokens[i++])
		}

		const condition: FilterCondition<T> = { field, op, value }
		if (connector) condition.connector = connector
		conditions.push(condition)
	}

	return { conditions, endIndex: i }
}

/**
 * Tokenize a SCIM query string, handling quoted strings and parentheses
 */
function tokenize(query: string): string[] {
	const tokens: string[] = []
	let current = ''
	let inQuote = false
	let i = 0

	while (i < query.length) {
		const char = query[i]

		if (inQuote) {
			if (char === '\\' && query[i + 1] === '"') {
				current += '\\"'
				i += 2
				continue
			}
			if (char === '"') {
				current += '"'
				tokens.push(current)
				current = ''
				inQuote = false
				i++
				continue
			}
			current += char
			i++
		} else {
			if (char === '"') {
				if (current) tokens.push(current)
				current = '"'
				inQuote = true
				i++
				continue
			}
			// Handle parentheses as separate tokens
			if (char === '(' || char === ')') {
				if (current) {
					tokens.push(current)
					current = ''
				}
				tokens.push(char)
				i++
				continue
			}
			if (char === ' ' || char === '\t') {
				if (current) {
					tokens.push(current)
					current = ''
				}
				i++
				continue
			}
			current += char
			i++
		}
	}

	if (current) tokens.push(current)
	return tokens
}
