package com.nisemoe.nise.search

import com.fasterxml.jackson.annotation.JsonInclude
import jakarta.validation.Valid
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.util.StopWatch
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RestController

@RestController
class SearchController(
    private val searchService: SearchService
) {

    private val logger = LoggerFactory.getLogger(javaClass)

    companion object {

        const val RESULTS_PER_PAGE = 50

    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    data class SearchResponse(
        val scores: List<SearchResponseEntry>,
        val pagination: SearchResponsePagination
    )

    data class SearchResponsePagination(
        val currentPage: Int,
        val pageSize: Int,
        val totalResults: Int
    ) {

        val totalPages: Int
            get() = if (totalResults % pageSize == 0) totalResults / pageSize else totalResults / pageSize + 1

    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    data class SearchResponseEntry(
        // User fields
        val user_id: Long?,
        val user_username: String?,
        val user_join_date: String?,
        val user_country: String?,
        val user_country_rank: Long?,
        val user_rank: Long?,
        val user_pp_raw: Double?,
        val user_accuracy: Double?,
        val user_playcount: Long?,
        val user_total_score: Long?,
        val user_ranked_score: Long?,
        val user_seconds_played: Long?,
        val user_count_300: Long?,
        val user_count_100: Long?,
        val user_count_50: Long?,
        val user_count_miss: Int?,

        // Score fields
        val id: Int?,
        val beatmap_id: Int?,
        val count_300: Int?,
        val count_100: Int?,
        val count_50: Int?,
        val count_miss: Int?,
        val date: String?,
        val max_combo: Int?,
        val mods: Int?,
        val perfect: Boolean?,
        val pp: Double?,
        val rank: String?,
        val replay_id: Long?,
        val score: Long?,
        val ur: Double?,
        val frametime: Double?,
        val edge_hits: Int?,
        val snaps: Int?,
        val adjusted_ur: Double?,
        val mean_error: Double?,
        val error_variance: Double?,
        val error_standard_deviation: Double?,
        val minimum_error: Double?,
        val maximum_error: Double?,
        val error_range: Double?,
        val error_coefficient_of_variation: Double?,
        val error_kurtosis: Double?,
        val error_skewness: Double?,
        val keypresses_median_adjusted: Double?,
        val keypresses_standard_deviation_adjusted: Double?,
        val sliderend_release_median_adjusted: Double?,
        val sliderend_release_standard_deviation_adjusted: Double?,

        // Beatmap fields
        val beatmap_artist: String?,
        val beatmap_beatmapset_id: Int?,
        val beatmap_creator: String?,
        val beatmap_source: String?,
        val beatmap_star_rating: Double?,
        val beatmap_title: String?,
        val beatmap_version: String?
    )

    data class SearchRequest(
        @Valid @field:ValidChildQueriesDepth @field:Size(max = 10) val queries: List<SearchQuery>,
        @Valid val sorting: SearchSorting,
        @field:Min(1) val page: Int
    )

    data class SearchSorting(
        @field:NotBlank @field:Size(max = 300) val field: String,
        @field:NotBlank @field:Size(max = 300) val order: String
    )

    data class SearchQuery(
        @field:NotBlank @field:Size(max = 300) val logicalOperator: String,
        @Valid @field:Size(max = 10) val predicates: List<SearchPredicate>,
        @Valid @field:ValidChildQueriesDepth @field:Size(max = 10) val childQueries: List<SearchQuery>?
    )

    data class SearchPredicate(
        @Valid val field: SearchField,
        @Valid val operator: SearchPredicateOperator,
        @field:NotBlank @field:Size(max = 300) val value: String
    )

    data class SearchPredicateOperator(
        @field:NotBlank @field:Size(max = 300) val operatorType: String,
        @field:NotBlank @field:Size(max = 300) val acceptsValues: String
    )

    data class SearchField(
        @field:NotBlank @field:Size(max = 300) val name: String,
        @field:NotBlank @field:Size(max = 300) val type: String
    )

    @PostMapping("search")
    fun doSearch(@RequestBody @Valid request: SearchRequest, @RequestHeader("X-NISE-API") apiVersion: String): ResponseEntity<SearchResponse> {
        if (apiVersion.isBlank())
            return ResponseEntity.badRequest().build()

        // TODO: CSRF

        val stopwatch = StopWatch()
        stopwatch.start()
        try {
            val response = this.searchService.search(request)
            return ResponseEntity.ok(response)
        } catch (e: Exception) {
            this.logger.error("Error while searching: {}", e.stackTraceToString())
            return ResponseEntity.status(500).build()
        } finally {
            stopwatch.stop()
            this.logger.info("Search took {} seconds", String.format("%.2f", stopwatch.totalTimeSeconds))
        }
    }

}