package icu.samnyan.aqua.net

import ext.*
import icu.samnyan.aqua.net.components.*
import icu.samnyan.aqua.net.db.*
import icu.samnyan.aqua.net.db.AquaUserServices.Companion.SETTING_FIELDS
import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.time.Instant
import java.time.LocalDateTime
import kotlin.io.path.writeBytes

@RestController
@API("/api/v2/user")
class UserRegistrar(
    val userRepo: AquaNetUserRepo,
    val hasher: PasswordEncoder,
    val turnstileService: TurnstileService,
    val emailService: EmailService,
    val geoIP: GeoIP,
    val jwt: JWT,
    val confirmationRepo: EmailConfirmationRepo,
    val cardRepo: CardRepository,
    val cardService: CardService,
    val validator: AquaUserServices,
    val emailProps: EmailProperties,
    final val paths: PathProps
) {
    val portraitPath = paths.aquaNetPortrait.path()

    companion object {
        // Random long with length 9-10
        // We chose 1e9 as the start because normal cards took 0...1e9-1
        // This is because games can only take uint32 for card ID, which is at max 10 digits (4294967295)
        const val cardExtIdStart = 1e9.toLong()
        const val cardExtIdEnd = 4294967295

        val log = LoggerFactory.getLogger(UserRegistrar::class.java)
    }

    @API("/register")
    @Doc("Register a new user. This will also create a ghost card for the user and send a confirmation email.", "Success message")
    suspend fun register(
        @RP username: Str, @RP email: Str, @RP password: Str, @RP turnstile: Str,
        request: HttpServletRequest
    ): Any {
        val ip = geoIP.getIP(request)
        log.info("Net: /user/register from $ip : $username")

        // Check captcha
        if (!turnstileService.validate(turnstile, ip)) 400 - "Invalid captcha"

        // GeoIP check to infer country
        val country = geoIP.getCountry(ip)

        // Create user
        val u = async { AquaNetUser(
            username = validator.checkUsername(username),
            email = validator.checkEmail(email),
            pwHash = validator.checkPwHash(password),
            regTime = millis(), lastLogin = millis(), country = country,
        ) }

        // Create a ghost card
        val card = Card().apply {
            extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd)
            luid = extId.toString()
            registerTime = LocalDateTime.now()
            accessTime = registerTime
            aquaUser = u
            isGhost = true
        }
        u.ghostCard = card

        // Save the user
        async {
            userRepo.save(u)
            cardRepo.save(card)
        }

        // Send confirmation email
        emailService.sendConfirmation(u)

        return mapOf("success" to true)
    }

    @API("/login")
    @Doc("Login with email/username and password. This will also check if the email is verified and send another confirmation", "JWT token")
    suspend fun login(
        @RP email: Str, @RP password: Str, @RP turnstile: Str,
        request: HttpServletRequest
    ): Any {
        // Check captcha
        val ip = geoIP.getIP(request)
        log.info("Net: /user/login from $ip : $email")
        if (!turnstileService.validate(turnstile, ip)) 400 - "Invalid captcha"

        // Treat email as email / username
        val user = async { userRepo.findByEmailIgnoreCase(email) ?: userRepo.findByUsernameIgnoreCase(email) }
            ?: (400 - "User not found")
        if (!hasher.matches(password, user.pwHash)) 400 - "Invalid password"

        // Check if email is verified
        if (!user.emailConfirmed && emailProps.enable) {
            // Check if last confirmation email was sent within a minute
            val confirmations = async { confirmationRepo.findByAquaNetUserAuId(user.auId) }
            val lastConfirmation = confirmations.maxByOrNull { it.createdAt }

            if (lastConfirmation?.createdAt?.plusSeconds(60)?.isAfter(Instant.now()) == true) {
                400 - "Email not verified - STATE_0"
            }

            // Check if we have sent more than 3 confirmation emails in the last 24 hours
            if (confirmations.count { it.createdAt.plusSeconds(60 * 60 * 24).isAfter(Instant.now()) } > 3) {
                400 - "Email not verified - STATE_1"
            }

            // Send another confirmation email
            emailService.sendConfirmation(user)
            400 - "Email not verified - STATE_2"
        }

        // Generate JWT token
        val token = jwt.gen(user)

        // Set last login time
        async { userRepo.save(user.apply { lastLogin = millis() }) }
        log.info("> Login success: ${user.username} ${user.auId}")

        return mapOf("token" to token)
    }

    @API("/confirm-email")
    @Doc("Confirm email address with a token sent through email to the user.", "Success message")
    suspend fun confirmEmail(@RP token: Str): Any {
        log.info("Net: /user/confirm-email with token $token")

        // Find the confirmation
        val confirmation = async { confirmationRepo.findByToken(token) }

        // Check if the token is valid
        if (confirmation == null) 400 - "Invalid token"

        // Check if the token is expired
        if (confirmation.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired"

        // Confirm the email
        async { userRepo.save(confirmation.aquaNetUser.apply { emailConfirmed = true }) }

        return SUCCESS
    }

    @API("/me")
    @Doc("Get the information of the current logged-in user.", "User information")
    suspend fun getUser(@RP token: Str) = jwt.auth(token)

    @API("/setting")
    @Doc("Validate and set a user setting field.", "Success message")
    suspend fun setting(@RP token: Str, @RP key: Str, @RP value: Str) = jwt.auth(token) { u ->
        // Check if the key is a settable field
        val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")

        async {
            // Set the validated field
            field.setter.call(u, field.checker.call(validator, value))

            // Save the user
            userRepo.save(u)
        }

        SUCCESS
    }

    val keychipRange = 1e9.toULong()..1e10.toULong() - 1UL

    @API("/keychip")
    @Doc("Get a Keychip ID so that the user can connect to the server.", "Success message")
    suspend fun setupConnection(@RP token: Str) = jwt.auth(token) { u ->
        if (u.keychip != null) return mapOf("keychip" to u.keychip)
        log.info("Net: /user/keychip setup: ${u.auId} for ${u.username}")

        // Generate a keychip id with 10 digits (e.g. A1234567890)
        var new = "A" + keychipRange.random()
        while (async { userRepo.findByKeychip(new) != null }) new = "A" + keychipRange.random()
        async { userRepo.save(u.apply { keychip = new }) }

        mapOf("keychip" to new)
    }

    @API("/upload-pfp", consumes = ["multipart/form-data"])
    @Doc("Upload a profile picture for the user.", "Success message")
    suspend fun uploadPfp(@RP token: Str, @RP file: MultipartFile) = jwt.auth(token) { u ->
        // Processing the image would lead to many open factors for attack
        // (e.g. the JFIF Pixel Flood attack that ImageIO is vulnerable to)
        // So we check file magic, then store the image without any processing
        val bytes = file.bytes
        val mime = TIKA.detect(bytes) ?: (400 - "Invalid file type")

        // Check if the file is an image
        if (!mime.startsWith("image/")) 400 - "Invalid file type"

        // Save the image
        val name = "${u.auId}${MIMES.forName(mime)?.extension ?: ".jpg"}"
        async {
            (portraitPath / name).writeBytes(bytes)
            userRepo.save(u.apply { profilePicture = name })
        }

        SUCCESS
    }
}