package com.nisemoe.nise.search.user

import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.nise.Format
import com.nisemoe.nise.search.PredicateBuilder.Companion.buildBooleanCondition
import com.nisemoe.nise.search.PredicateBuilder.Companion.buildDatetimeCondition
import com.nisemoe.nise.search.PredicateBuilder.Companion.buildGradeCondition
import com.nisemoe.nise.search.PredicateBuilder.Companion.buildNumberCondition
import com.nisemoe.nise.search.PredicateBuilder.Companion.buildStringCondition
import org.jooq.*
import org.jooq.impl.DSL
import org.springframework.stereotype.Service
import kotlin.math.roundToInt


@Service
class UserSearchService(
    private val dslContext: DSLContext
) {

    fun search(request: UserSearchController.SearchRequest): UserSearchController.SearchResponse {
        var baseQuery = DSL.noCondition()
        for (query in request.queries.filter { it.predicates.isNotEmpty() }) {
            val condition = buildCondition(query)
            baseQuery = when (query.logicalOperator.lowercase()) {
                "and" -> baseQuery.and(condition)
                "or" -> baseQuery.or(condition)
                else -> throw IllegalArgumentException("Invalid logical operator")
            }
        }

        val selectFields = mutableListOf<Field<*>>(
            USERS.USERNAME,
            USERS.USER_ID,
            USERS.JOIN_DATE,
            USERS.COUNTRY,
            USERS.COUNTRY_RANK,
            USERS.RANK,
            USERS.PP_RAW,
            USERS.ACCURACY,
            USERS.PLAYCOUNT,
            USERS.TOTAL_SCORE,
            USERS.RANKED_SCORE,
            USERS.SECONDS_PLAYED,
            USERS.COUNT_300,
            USERS.COUNT_100,
            USERS.COUNT_50,
            USERS.COUNT_MISS,
            USERS.IS_BANNED,
            USERS.APPROX_BAN_DATE
        )

        val query = dslContext
            .select(selectFields)
            .from(USERS)
            .where(baseQuery)
            .apply {
                if (request.sorting.field.isNotBlank())
                    orderBy(buildSorting(request.sorting))
            }
            .offset((request.page - 1) * UserSearchController.RESULTS_PER_PAGE)
            .limit(UserSearchController.RESULTS_PER_PAGE)

        val results = query
            .fetch()

        // Get total results
        val totalResults = dslContext.selectCount()
            .from(USERS)
            .where(baseQuery)
            .fetchOne(0, Int::class.java) ?: 0

        return UserSearchController.SearchResponse(
            results = mapRecordToScores(results),
            pagination = UserSearchController.SearchResponsePagination(
                currentPage = request.page,
                pageSize = UserSearchController.RESULTS_PER_PAGE,
                totalResults = totalResults
            )
        )
    }

    private fun mapRecordToScores(results: Result<Record>): MutableList<UserSearchController.SearchResponseEntry> =
        results.map {
            UserSearchController.SearchResponseEntry(
                user_id = it.get(SCORES.USER_ID),
                username = it.get(USERS.USERNAME),
                join_date = it.get(USERS.JOIN_DATE)?.let { it1 -> Format.formatLocalDateTime(it1) },
                country = it.get(USERS.COUNTRY),
                country_rank = it.get(USERS.COUNTRY_RANK),
                rank = it.get(USERS.RANK),
                pp_raw = it.get(USERS.PP_RAW)?.roundToInt()?.toDouble(),
                accuracy = it.get(USERS.ACCURACY),
                playcount = it.get(USERS.PLAYCOUNT),
                total_score = it.get(USERS.TOTAL_SCORE),
                ranked_score = it.get(USERS.RANKED_SCORE),
                seconds_played = it.get(USERS.SECONDS_PLAYED),
                count_300 = it.get(USERS.COUNT_300),
                count_100 = it.get(USERS.COUNT_100),
                count_50 = it.get(USERS.COUNT_50),
                count_miss = it.get(USERS.COUNT_MISS),
                is_banned = it.get(USERS.IS_BANNED),
                approx_ban_date = it.get(USERS.APPROX_BAN_DATE)?.let { it1 -> Format.formatOffsetDateTime(it1) }
            )
        }

    fun buildCondition(query: UserSearchController.SearchQuery): Condition {
        // Handle base predicates
        var baseCondition = buildPredicateCondition(query.predicates.first())
        query.predicates.drop(1).forEach { predicate ->
            baseCondition = when (query.logicalOperator.lowercase()) {
                "and" -> baseCondition.and(buildPredicateCondition(predicate))
                "or" -> baseCondition.or(buildPredicateCondition(predicate))
                else -> throw IllegalArgumentException("Invalid logical operator")
            }
        }

        // Handle child queries
        if(query.childQueries.isNullOrEmpty())
            return baseCondition

        query.childQueries.forEach { childQuery ->
            val childCondition = buildCondition(childQuery) // Recursively build condition for child queries
            baseCondition = when (childQuery.logicalOperator.lowercase()) {
                "and" -> baseCondition.and(childCondition)
                "or" -> baseCondition.or(childCondition)
                else -> throw IllegalArgumentException("Invalid logical operator")
            }
        }

        return baseCondition
    }

    private fun buildSorting(sorting: UserSearchController.SearchSorting): OrderField<*> {
        val field = mapPredicateFieldToDatabaseField(sorting.field)
        return when (sorting.order.lowercase()) {
            "asc" -> field.asc()
            "desc" -> field.desc()
            else -> throw IllegalArgumentException("Invalid sorting order")
        }
    }

    private fun buildPredicateCondition(predicate: UserSearchController.SearchPredicate): Condition {
        val field = mapPredicateFieldToDatabaseField(predicate.field.name)
        return when (predicate.field.type.lowercase()) {
            "number" -> buildNumberCondition(field as Field<Double>, predicate.operator.operatorType, predicate.value.toDouble())
            "string" -> buildStringCondition(field as Field<String>, predicate.operator.operatorType, predicate.value)
            "boolean" -> buildBooleanCondition(field as Field<Boolean>, predicate.operator.operatorType, predicate.value.toBoolean())
            "flag" -> buildStringCondition(field as Field<String>, predicate.operator.operatorType, predicate.value)
            "grade" -> buildGradeCondition(field as Field<String>, predicate.operator.operatorType, predicate.value)
            "datetime" -> buildDatetimeCondition(field as Field<String>, predicate.operator.operatorType, predicate.value)
            "playtime" -> buildNumberCondition(field as Field<Double>, predicate.operator.operatorType, predicate.value.toDouble())
            else -> throw IllegalArgumentException("Invalid field type")
        }
    }

    private fun mapPredicateFieldToDatabaseField(predicateName: String): Field<*> {
        val databaseField = UserSearchSchemaController.internalFields.first {
            it.name == predicateName && it.databaseField != null
        }
        return databaseField.databaseField!!
    }

}