package inn.ocsf.bee.freigeld.serve.rest

import com.oblac.nomen.Nomen
import dev.samstevens.totp.code.*
import dev.samstevens.totp.qr.QrData
import dev.samstevens.totp.qr.ZxingPngQrGenerator
import dev.samstevens.totp.recovery.RecoveryCodeGenerator
import dev.samstevens.totp.secret.DefaultSecretGenerator
import dev.samstevens.totp.time.SystemTimeProvider
import dev.samstevens.totp.time.TimeProvider
import dev.samstevens.totp.util.Utils.getDataUriForImage
import inn.ocsf.bee.freigeld.core.data.CentralBankQueueLevel
import inn.ocsf.bee.freigeld.core.data.ExchangeFailedEvent
import inn.ocsf.bee.freigeld.core.data.ExchangeSuccessEvent
import inn.ocsf.bee.freigeld.core.demo.getSelfAccount
import inn.ocsf.bee.freigeld.core.model.*
import inn.ocsf.bee.freigeld.core.model.data.PersonData
import inn.ocsf.bee.freigeld.core.repo.PersonRepository
import inn.ocsf.bee.freigeld.core.repo.PublicLinkRepository
import inn.ocsf.bee.freigeld.core.service.TicketService
import inn.ocsf.bee.freigeld.core.service.WorldService
import inn.ocsf.bee.freigeld.serve.JwtAuthenticationController
import inn.ocsf.bee.freigeld.utils.KeyValueBucket.*
import inn.ocsf.bee.freigeld.utils.KeyValueStorage
import org.apache.commons.lang3.RandomStringUtils
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.*
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.streams.toList

@RestController
class PersonController {

    @Inject
    private lateinit var world: GlobalWorld

    @Inject
    private lateinit var bank: CentralBank

    @Inject
    private lateinit var ticketService: TicketService

    @Inject
    private lateinit var linkRepo: PublicLinkRepository

    @Inject
    private lateinit var totp: PersonTotpService

    @Inject
    private lateinit var storage: KeyValueStorage

    private val log = LoggerFactory.getLogger(javaClass)

    @RequestMapping("/api/old/person/{personId}/get-credit-link/{linkId}", method = [RequestMethod.GET])
    fun getCreditLink(@PathVariable("personId") personId: UUID, @PathVariable("linkId") linkId: String): ResponseEntity<Map<String, Any?>> {
        val link = linkRepo.findById(linkId).filter { it is PublicLinkDebitAccount }.map { it as PublicLinkDebitAccount }.orElse(null)
        if (link != null) {
            if (!link.isWasted()) {
                return ResponseEntity.ok(mapOf("ok" to true, "amount" to link.amount))
            } else {
                return ResponseEntity(HttpStatus.GONE)
            }
        } else {
            return ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @RequestMapping("api/old/person/{personId}/account/{accountId}/debit-by-link", method = [RequestMethod.POST])
    fun getDebitLink(@PathVariable("personId") personId: UUID, @PathVariable("accountId") accountId: UUID, @RequestBody lreq: DebitLinkReq): ResponseEntity<Map<String, Any>> {
        val link = linkRepo.getFreeIdAndSave { id -> PublicLinkDebitAccount(id, personId, accountId, lreq.amount, lreq.once, lreq.limited, lreq.time) }
        return ResponseEntity.ok(mapOf("code" to link.id)) //p7do xfay
    }

    @RequestMapping("api/old/person/{personId}/accounts", method = [RequestMethod.GET])
    fun getAccountsInfo(@PathVariable("personId") personId: UUID): ResponseEntity<Map<String, Any?>> {
        val accounts = bank.getAccounts(personId).map { AccountInfo(it.id, it.overall) }
        return ResponseEntity.ok(mapOf("accounts" to accounts))
    }

    @RequestMapping("api/old/person/{personId}/events", method = [RequestMethod.GET])
    fun getEvents(@PathVariable("personId") personId: UUID, @RequestParam("limit", defaultValue = "25") limit: Long, @RequestParam("after", required = false) eventId: UUID?): ResponseEntity<List<AccountEvent>> {
        val accounts = bank.getAccounts(personId).map { it.id }
        var skipping = eventId != null
        val events = ticketService.findAll(CentralBankQueueLevel.bankTicketChannelName, setOf(TicketStatus.completed)).filter {
            val skippingThis = skipping
            if (skipping) if (it.ticket.id == eventId) skipping = false
            !skippingThis
        }.filter {
            val ticket = it.ticket
            val ids = when (ticket) {
                is ExchangeFailedEvent -> setOf(ticket.parentEvent?.from, ticket.parentEvent?.to)
                is ExchangeSuccessEvent -> setOf(ticket.parentEvent?.from, ticket.parentEvent?.to)
                else -> setOf()
            }
            accounts.intersect(ids).isNotEmpty()
        }.map {
            val ticket = it.ticket as BankEvent
            AccountEvent(it.ticket.id, it.ticket::class.java.typeName, it.ts.time, ticket)
        }.limit(limit).toList().sortedByDescending { it.ts }
        return ResponseEntity.ok(events)
    }

    @RequestMapping("api/old/person/{personId}/first-run", method = [RequestMethod.GET])
    fun firstRun(@PathVariable("personId") personId: UUID): ResponseEntity<PersonFirstRunInfoResp> {
        val firstRun = storage.get<Boolean>(personFirstRun, personId) ?: false
        val codes = if (firstRun) {
            storage.put(personFirstRun, personId, false)
            world.getPerson(personId)?.let { person -> world.getPersonIdentitySet(person)?.filter { it is PersonIdentitySecret }?.map { it as PersonIdentitySecret }?.firstOrNull()?.codes }
        } else {
            null
        }
        return ResponseEntity.ok(PersonFirstRunInfoResp(firstRun, codes))
    }

    @RequestMapping("api/old/person/{personId}/accounts/add", method = [RequestMethod.POST])
    fun addAccount(@PathVariable("personId") personId: UUID): ResponseEntity<Map<String, Any?>> {
        val account = bank.addAccount(personId)
        return ResponseEntity.ok(mapOf("id" to account.id))
    }


    @ExperimentalStdlibApi
    @Profile("dev")
    @RequestMapping("api/old/person/{personId}/givemoney", method = [RequestMethod.POST])
    fun giveMoney(@PathVariable("personId") personId: UUID): ResponseEntity<Map<String, Any?>> {
        val account = bank.getAccounts(personId).sortedBy { it.overall }.firstOrNull()
        if (account != null) {
            bank.exchange(account, bank.getSelfAccount(), (100 * Math.random()).toLong(), BankExchangeDetails.BankExchangeDetailsBuilder.aBankExchangeDetails().withDescription("basic income").build()).get(5, TimeUnit.SECONDS)
        }
        return ResponseEntity.ok(mapOf())
    }

    @RequestMapping("api/old/person/{personId}/account/{accountId}/credit/by-link/{linkId}")
    fun creditMoney(@PathVariable("personId") personId: UUID, @PathVariable("accountId") accountId: UUID, @PathVariable("linkId") linkId: String, @RequestBody req: CreditLinkReq): ResponseEntity<Map<String, Any?>> {
        val verifier = totp.getVerifier()
        val secret = world.getPerson(personId)?.let { world.getPersonIdentitySet(it) }?.filter { it is PersonIdentitySecret }?.map { it as PersonIdentitySecret }?.firstOrNull()
        if (secret != null) {
            if (!verifier.isValidCode(secret.key, req.code)) {
                return ResponseEntity(HttpStatus.FORBIDDEN)
            }
        }
        val fromAccount = bank.getAccounts(personId).first { it.id == accountId }
        val link = linkRepo.findById(linkId).filter { it is PublicLinkDebitAccount }.map { it as PublicLinkDebitAccount }.orElse(null)
        val ret = if (fromAccount != null) {
            if (link != null && !link.isWasted()) {
                val toAccount = bank.getAccount(link.accountId!!)
                if (toAccount != null) {
                    if (req.amount != null && req.amount > 0) {
                        val e = bank.exchange(toAccount, fromAccount, req.amount, BankExchangeDetails.BankExchangeDetailsBuilder.aBankExchangeDetails().withDescription("by link ${linkId}").withExtraFields(mapOf("linkId" to linkId)).build()).get(5, TimeUnit.SECONDS)
                        if (link.once == true) {
                            link.status = PublicLinkStatus.done
                            linkRepo.save(link)
                        }
                        e
                    } else {
                        null
                    }
                } else {
                    null
                }
            } else {
                null
            }
        } else {
            null
        }
        if (ret != null) {
            return ResponseEntity.ok(mapOf("id" to ret.id))
        } else {
            return ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }
}

data class PersonFirstRunInfoResp(val firstRun: Boolean, val codes: List<String>?)

data class CreditLinkReq(val code: String?, val amount: Long?)

data class DebitLinkReq(val amount: Long?, val once: Boolean?, val limited: Boolean?, val time: Long?)

data class AccountInfo(val id: UUID, val overall: Long)

data class AccountEvent(val id: UUID, val type: String, val ts: Long, val info: BankEvent)

class PublicLinkDebitAccount() : PublicLink() {

    var personId: UUID? = null
    var accountId: UUID? = null
    var amount: Long? = null
    var once: Boolean? = null
    var time: Long? = null
    var limited: Boolean? = null
    var dt: Date? = null

    constructor(id: String, personId: UUID, accountId: UUID, amount: Long?, once: Boolean?, limited: Boolean?, time: Long?) : this() {
        this.id = id
        this.personId = personId
        this.accountId = accountId
        this.amount = amount
        this.once = once
        this.limited = limited
        this.time = time
        this.dt = Date()
        this.status = PublicLinkStatus.active
    }

    fun isWasted(): Boolean {
        val dt = LocalDateTime.from((this.dt ?: Date()).toInstant().atZone(ZoneId.systemDefault()))
        return (this.once != true || this.status != PublicLinkStatus.done) && (this.limited != true || !(dt.plusHours(this.time!!).isBefore(LocalDateTime.now())))
    }
}

@Service
class PersonTotpService {

    fun getVerifier(): CodeVerifier {
        val timeProvider: TimeProvider = SystemTimeProvider()
        val codeGenerator: CodeGenerator = DefaultCodeGenerator(HashingAlgorithm.SHA1)
        val verifier = DefaultCodeVerifier(codeGenerator, timeProvider)
        verifier.setTimePeriod(30)
        verifier.setAllowedTimePeriodDiscrepancy(4)
        return verifier
    }

    fun getSecret(): String {
        val secretGenerator = DefaultSecretGenerator()
        val secret = secretGenerator.generate()
        return secret
    }

    fun getImage(secret: String): String {
        val data = QrData.Builder().secret(secret).algorithm(HashingAlgorithm.SHA1).label("personal kalita").issuer("FreiGeld").digits(6).period(30).build()
        val generator = ZxingPngQrGenerator()
        val imageData = generator.generate(data)
        val mimeType = generator.imageMimeType
        val dataUri = getDataUriForImage(imageData, mimeType)
        return dataUri
    }


}

@RestController
@CrossOrigin
class PersonAuthController : JwtAuthenticationController() {

    @Inject
    private lateinit var storage: KeyValueStorage

    @Inject
    private lateinit var personRepo: PersonRepository

    @Inject
    private lateinit var world: GlobalWorld

    @Inject
    private lateinit var worldService: WorldService

    @Inject
    private lateinit var totp: PersonTotpService

    @RequestMapping("api/new/person/qr", method = [RequestMethod.GET])
    fun newqr(@RequestParam("siteId") siteId: UUID): ResponseEntity<QrDataResponse> {
        val secret = totp.getSecret()

        storage.put(secretToSite, secret, siteId)

        val dataUri = totp.getImage(secret)

        return ResponseEntity.ok(QrDataResponse(secret, dataUri))
    }

    @RequestMapping("api/new/person/check16", method = [RequestMethod.POST])
    fun checkrecover(@RequestBody req: RecoverOldReq): ResponseEntity<RecoverOldResp> {
        val person = worldService.findOneByRecoveryCode(req.code, req.num)
        return if (person != null) {
            storage.put(siteToPerson, req.siteId, person.id)
            ResponseEntity.ok(RecoverOldResp(req.siteId))
        } else {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @RequestMapping("api/old/person/{personId}/check6", method = [RequestMethod.POST])
    fun checkqrnow(@PathVariable("personId") personId: UUID, @RequestBody rq: QrOldSixRequest): ResponseEntity<QrOldSixResponse> {
        val verifier = totp.getVerifier()
        val person = world.getPerson(personId)
        if (person != null) {
            val secret = world.getPersonIdentitySet(person)?.filter { it is PersonIdentitySecret }?.map { it as PersonIdentitySecret }?.firstOrNull()
            if (secret != null) {
                return ResponseEntity.ok(QrOldSixResponse(verifier.isValidCode(secret.key, rq.key)))
            } else {
                throw RuntimeException("this should not happen")
            }
        } else {
            return ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @RequestMapping("api/new/person/check6", method = [RequestMethod.POST])
    fun checkqr(@RequestBody req: QrSixRequest): ResponseEntity<QrSixResponse> {
        val verifier = totp.getVerifier()

        var register = false

        val thisPerson = if (verifier.isValidCode(req.key, req.code)) { //new registration
            val person = PersonData.NaturalPersonImpl("${Nomen.randomName()}_${RandomStringUtils.randomAlphanumeric(5)}")
            world.addPerson(person)
            world.setPersonIdentity(person, PersonIdentityFullName(person.fullName))
            val recoveryCodeGenerator = RecoveryCodeGenerator()
            val codes = recoveryCodeGenerator.generateCodes(16)
            world.setPersonIdentity(person, PersonIdentitySecret(req.key, codes.toList()))
            storage.put(siteToPerson, req.siteId, person.id)
            storage.put(personFirstRun, person.id, true)
            register = true
            person
        } else {
            storage.get<UUID>(siteToPerson, req.siteId).let { id -> personRepo.findOneByPersonId(id).orElseThrow { throw RuntimeException("strange person ${id} not found") } }.let {
                val secret = it.identities.values.filter { it is PersonIdentitySecret }.first()
                if (verifier.isValidCode(secret.key, req.code)) {
                    it.person
                } else {
                    null
                }
            }
        }
        val token = thisPerson?.let { createAuthenticationToken(it.id.toString()) }
        return ResponseEntity.ok(QrSixResponse(thisPerson != null, register, token))
    }

}

data class RecoverOldReq(val num: Int, val siteId: UUID, val code: String)

data class RecoverOldResp(val siteId: UUID)

data class QrDataResponse(val key: String, val img: String)

data class QrSixRequest(val siteId: UUID, val code: String, val key: String)

data class QrOldSixRequest(val key: String)

data class QrOldSixResponse(val ok: Boolean)

data class QrSixResponse(val ok: Boolean, val register: Boolean, val token: String?)