package com.nisemoe.nise.search

import com.nisemoe.generated.tables.references.BEATMAPS
import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.nise.Format
import com.nisemoe.nise.service.AuthService
import org.jooq.*
import org.jooq.impl.DSL
import org.springframework.stereotype.Service
import kotlin.math.roundToInt

@Service
class SearchService(
    private val dslContext: DSLContext,
    private val authService: AuthService
) {

    fun search(request: SearchController.SearchRequest): SearchController.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<*>>(
            // Common fields list
            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,

            // Scores fields
            SCORES.ID,
            SCORES.BEATMAP_ID,
            SCORES.COUNT_300,
            SCORES.COUNT_100,
            SCORES.COUNT_50,
            SCORES.COUNT_MISS,
            SCORES.DATE,
            SCORES.MAX_COMBO,
            SCORES.MODS,
            SCORES.PERFECT,
            SCORES.PP,
            SCORES.RANK,
            SCORES.REPLAY_ID,
            SCORES.SCORE,
            SCORES.UR,
            SCORES.FRAMETIME,
            SCORES.EDGE_HITS,
            SCORES.SNAPS,
            SCORES.ADJUSTED_UR,
            SCORES.MEAN_ERROR,
            SCORES.ERROR_VARIANCE,
            SCORES.ERROR_STANDARD_DEVIATION,
            SCORES.MINIMUM_ERROR,
            SCORES.MAXIMUM_ERROR,
            SCORES.ERROR_RANGE,
            SCORES.ERROR_COEFFICIENT_OF_VARIATION,
            SCORES.ERROR_KURTOSIS,
            SCORES.ERROR_SKEWNESS,

            // Beatmaps fields
            BEATMAPS.ARTIST,
            BEATMAPS.BEATMAPSET_ID,
            BEATMAPS.CREATOR,
            BEATMAPS.SOURCE,
            BEATMAPS.STAR_RATING,
            BEATMAPS.TITLE,
            BEATMAPS.VERSION
        )

        // Conditionally add admin-specific fields
        if (authService.isAdmin()) {
            selectFields.addAll(
                listOf(
                    SCORES.KEYPRESSES_MEDIAN_ADJUSTED,
                    SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED,
                    SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED,
                    SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED
                )
            )
        }

        val query = dslContext
            .select(selectFields)
            .from(SCORES)
            .join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
            .join(BEATMAPS).on(SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
            .where(baseQuery)
            .apply {
                if (request.sorting.field.isNotBlank())
                    orderBy(buildSorting(request.sorting))
            }
            .offset((request.page - 1) * SearchController.RESULTS_PER_PAGE)
            .limit(SearchController.RESULTS_PER_PAGE)

        val results = query
            .fetch()

        // Get total results
        val totalResults = dslContext.selectCount()
            .from(SCORES)
            .join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
            .join(BEATMAPS).on(SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
            .where(baseQuery)
            .fetchOne(0, Int::class.java) ?: 0

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

    private fun mapRecordToScores(results: Result<Record>): MutableList<SearchController.SearchResponseEntry> =
        results.map {
            SearchController.SearchResponseEntry(
                // User fields
                user_id = it.get(SCORES.USER_ID),
                user_username = it.get(USERS.USERNAME),
                user_join_date = it.get(USERS.JOIN_DATE)?.let { it1 -> Format.formatLocalDateTime(it1) },
                user_country = it.get(USERS.COUNTRY),
                user_country_rank = it.get(USERS.COUNTRY_RANK),
                user_rank = it.get(USERS.RANK),
                user_pp_raw = it.get(USERS.PP_RAW)?.roundToInt()?.toDouble(),
                user_accuracy = it.get(USERS.ACCURACY),
                user_playcount = it.get(USERS.PLAYCOUNT),
                user_total_score = it.get(USERS.TOTAL_SCORE),
                user_ranked_score = it.get(USERS.RANKED_SCORE),
                user_seconds_played = it.get(USERS.SECONDS_PLAYED),
                user_count_300 = it.get(USERS.COUNT_300),
                user_count_100 = it.get(USERS.COUNT_100),
                user_count_50 = it.get(USERS.COUNT_50),
                user_count_miss = it.get(SCORES.COUNT_MISS),

                // Score fields
                id = it.get(SCORES.ID),
                beatmap_id = it.get(SCORES.BEATMAP_ID),
                count_300 = it.get(SCORES.COUNT_300),
                count_100 = it.get(SCORES.COUNT_100),
                count_50 = it.get(SCORES.COUNT_50),
                count_miss = it.get(SCORES.COUNT_MISS),
                date = it.get(SCORES.DATE)?.let { it1 -> Format.formatLocalDateTime(it1) },
                max_combo = it.get(SCORES.MAX_COMBO),
                mods = it.get(SCORES.MODS),
                perfect = it.get(SCORES.PERFECT),
                pp = it.get(SCORES.PP)?.roundToInt()?.toDouble(),
                rank = it.get(SCORES.RANK),
                replay_id = it.get(SCORES.REPLAY_ID),
                score = it.get(SCORES.SCORE),
                ur = it.get(SCORES.UR),
                frametime = it.get(SCORES.FRAMETIME),
                edge_hits = it.get(SCORES.EDGE_HITS),
                snaps = it.get(SCORES.SNAPS),
                adjusted_ur = it.get(SCORES.ADJUSTED_UR),
                mean_error = it.get(SCORES.MEAN_ERROR),
                error_variance = it.get(SCORES.ERROR_VARIANCE),
                error_standard_deviation = it.get(SCORES.ERROR_STANDARD_DEVIATION),
                minimum_error = it.get(SCORES.MINIMUM_ERROR),
                maximum_error = it.get(SCORES.MAXIMUM_ERROR),
                error_range = it.get(SCORES.ERROR_RANGE),
                error_coefficient_of_variation = it.get(SCORES.ERROR_COEFFICIENT_OF_VARIATION),
                error_kurtosis = it.get(SCORES.ERROR_KURTOSIS),
                error_skewness = it.get(SCORES.ERROR_SKEWNESS),

                // Admin-specific fields, conditional based on isAdmin
                keypresses_median_adjusted = if (authService.isAdmin()) it.get(SCORES.KEYPRESSES_MEDIAN_ADJUSTED) else null,
                keypresses_standard_deviation_adjusted = if (authService.isAdmin()) it.get(SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED) else null,
                sliderend_release_median_adjusted = if (authService.isAdmin()) it.get(SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED) else null,
                sliderend_release_standard_deviation_adjusted = if (authService.isAdmin()) it.get(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED) else null,

                // Beatmap fields
                beatmap_artist = it.get(BEATMAPS.ARTIST),
                beatmap_beatmapset_id = it.get(BEATMAPS.BEATMAPSET_ID),
                beatmap_creator = it.get(BEATMAPS.CREATOR),
                beatmap_source = it.get(BEATMAPS.SOURCE),
                beatmap_star_rating = it.get(BEATMAPS.STAR_RATING),
                beatmap_title = it.get(BEATMAPS.TITLE),
                beatmap_version = it.get(BEATMAPS.VERSION)
            )
        }

    fun buildCondition(query: SearchController.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: SearchController.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: SearchController.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 = SearchSchemaController.internalFields.first {
            it.name == predicateName && it.databaseField != null
        }
        return databaseField.databaseField!!
    }

    private fun buildBooleanCondition(field: Field<Boolean>, operator: String, value: Boolean): Condition {
        return when (operator) {
            "=" -> field.eq(value)
            "!=" -> field.ne(value)
            else -> throw IllegalArgumentException("Invalid operator")
        }
    }

    private fun buildGradeCondition(field: Field<String>, operator: String, value: String): Condition {
        return when (value) {
            "SS", "S", "A", "B", "C", "D" -> {
                val valuesToMatch = when (value) {
                    "SS" -> listOf("Grade.SS", "Grade.SSH")
                    "S" -> listOf("Grade.S", "Grade.SH")
                    else -> listOf("Grade.$value")
                }
                when (operator) {
                    "=" -> field.`in`(valuesToMatch)
                    "!=" -> field.notIn(valuesToMatch)
                    else -> throw IllegalArgumentException("Invalid operator")
                }
            }
            else -> throw IllegalArgumentException("Invalid grade value")
        }
    }

    private fun buildNumberCondition(field: Field<Double>, operator: String, value: Double): Condition {
        return when (operator) {
            "=" -> field.eq(value)
            ">" -> field.gt(value)
            "<" -> field.lt(value)
            ">=" -> field.ge(value)
            "<=" -> field.le(value)
            "!=" -> field.ne(value)
            else -> throw IllegalArgumentException("Invalid operator")
        }
    }

    private fun buildStringCondition(field: Field<String>, operator: String, value: String): Condition {
        return when (operator.lowercase()) {
            "=" -> field.eq(value)
            "contains" -> field.containsIgnoreCase(value)
            "like" -> field.likeIgnoreCase(
                // Escape special characters for LIKE if needed
                value.replace("%", "\\%").replace("_", "\\_")
            )

            else -> throw IllegalArgumentException("Invalid operator")
        }
    }

    private fun buildDatetimeCondition(field: Field<String>, operator: String, value: String): Condition {
        return when (operator.lowercase()) {
            "before" -> field.lessThan(value)
            "after" -> field.greaterThan(value)
            else -> throw IllegalArgumentException("Invalid operator")
        }
    }

}