package dev.inmo.postssystem.services.posts.server

import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.ktor.common.decodeHex
import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
import dev.inmo.postssystem.features.common.common.FileBasedInputProvider
import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.files.common.FileId
import dev.inmo.postssystem.features.posts.common.*
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.streamProvider
import io.ktor.request.isMultipart
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.routing.*
import io.ktor.util.asStream
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.use
import io.ktor.utils.io.streams.asInput
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.builtins.*
import kotlinx.serialization.modules.polymorphic
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.FileTime
import java.util.concurrent.TimeUnit

class ServerPostsServiceRoutingConfigurator(
    private val readPostsService: ReadPostsService,
    private val writePostsService: WritePostsService? = readPostsService as? WritePostsService,
    private val scope: CoroutineScope,
    unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element {
    private val unifiedRouter = UnifiedRouter(
        serialFormat = unifiedRouter.serialFormat.createWithSerializerModuleExtension {
            polymorphic(Content::class) {
                subclass(BinaryContent::class, BinaryContentSerializer(TempFileIdentifierInputProvider::class, TempFileIdentifierInputProvider.serializer()))
            }
        }
    )
    private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
    private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
    private val contentsSerializer = ListSerializer(ContentSerializer)

    private val temporalFilesMap = mutableMapOf<FileId, MPPFile>()
    private val temporalFilesMutex = Mutex()
    private val removingJob = scope.launchSafelyWithoutExceptions {
        while (isActive) {
            val filesWithCreationInfo = temporalFilesMap.mapNotNull { (fileId, file) ->
                fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
            }
            if (filesWithCreationInfo.isEmpty()) {
                delay(TimeUnit.HOURS.toMillis(1L))
                continue
            }
            var min = filesWithCreationInfo.first()
            for (fileWithCreationInfo in filesWithCreationInfo) {
                if (fileWithCreationInfo.second < min.second) {
                    min = fileWithCreationInfo
                }
            }
            delay(System.currentTimeMillis() - (min.second + TimeUnit.HOURS.toMillis(1)))
            temporalFilesMutex.withLock {
                temporalFilesMap.remove(min.first)
            } ?.delete()
        }
    }

    private suspend fun mapBinary(content: BinaryContent): BinaryContent? {
        val provider = content.inputProvider
        if (provider is TempFileIdentifierInputProvider) {
            val newProvider = temporalFilesMutex.withLock {
                temporalFilesMap.remove(provider.tempFile)
            } ?.let(::FileBasedInputProvider)

            if (newProvider != null) {
                return content.copy(
                    inputProvider = newProvider
                )
            }
        }
        return null
    }

    private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContents(): List<Content> {
        return unifiedRouter.run {
            uniload(contentsSerializer).mapNotNull {
                mapBinary(it as? BinaryContent ?: return@mapNotNull it)
            }
        }
    }

    private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
        return unifiedRouter.run {
            uniload(contentsEitherSerializer).mapNotNull {
                it.mapOnSecond {
                    mapBinary(it as? BinaryContent ?: return@mapOnSecond null) ?.either()
                } ?: it
            }
        }
    }



    override fun Route.invoke() {
        authenticate {
            route(postsRootPath) {
                configureReadStandardCrudRepoRoutes(
                    readPostsService,
                    RegisteredPost.serializer(),
                    RegisteredPost.serializer().nullable,
                    PostId.serializer(),
                    unifiedRouter
                )

                writePostsService ?.let {

                    unifiedRouter.apply {

                        post(createRouting) {
                            val data = receiveContents()

                            unianswer(
                                RegisteredPost.serializer().nullable,
                                writePostsService.create(FullNewPost(data))
                            )
                        }

                        post(updateRouting) {
                            val postId = call.decodeUrlQueryValueOrSendError(postsPostIdParameter, PostId.serializer()) ?: return@post
                            val data = receiveContentsEithers()

                            unianswer(
                                RegisteredPost.serializer().nullable,
                                writePostsService.update(
                                    postId,
                                    data
                                )
                            )
                        }

                        post(removeRoute) {
                            val postId = uniload(PostId.serializer())

                            unianswer(
                                Unit.serializer(),
                                writePostsService.remove(postId)
                            )
                        }

                        post(postsCreateTempPathPart) {
                            val multipart = call.receiveMultipart()

                            var fileInfo: Pair<FileId, MPPFile>? = null
                            var part = multipart.readPart()
                            while (part != null) {
                                if (part is PartData.FileItem) {
                                    break
                                }
                                part = multipart.readPart()
                            }
                            part ?.let {
                                if (it is PartData.FileItem) {
                                    val fileId = FileId(uuid4().toString())
                                    val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
                                    fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
                                        outputStream().use { outputStream ->
                                            it.streamProvider().use {
                                                it.copyTo(outputStream)
                                            }
                                        }
                                        deleteOnExit()
                                    }
                                }
                            }

                            fileInfo ?.also { (fileId, file) ->
                                temporalFilesMutex.withLock {
                                    temporalFilesMap[fileId] = file
                                }
                                call.respond(fileId.string)
                            } ?: call.respond(HttpStatusCode.BadRequest)

                        }

                    }

                }
            }
        }
    }
}
