package com.insanusmokrassar.postssystem.core.exposed

import com.insanusmokrassar.postssystem.core.content.ContentId
import com.insanusmokrassar.postssystem.core.post.*
import com.insanusmokrassar.postssystem.core.post.api.PostsAPI
import com.insanusmokrassar.postssystem.core.utils.generatePostId
import com.insanusmokrassar.postssystem.core.utils.pagination.*
import com.soywiz.klock.DateTime
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import org.jetbrains.exposed.sql.*

private class PostsAPIContentRelations(
    private val database: Database
) : Table() {
    private val postIdColumn = text("postId")
    private val contentIdColumn = text("contentId")

    private inline fun <T> transaction(noinline body: Transaction.() -> T): T = database.transaction(body)

    init {
        transaction {
            SchemaUtils.createMissingTablesAndColumns(this@PostsAPIContentRelations)
        }
    }

    fun getPostContents(postId: PostId): List<ContentId> {
        return transaction {
            select { postIdColumn.eq(postId) }.map { it[contentIdColumn] }
        }
    }

    fun getContentPosts(contentId: ContentId): List<PostId> {
        return transaction {
            select { contentIdColumn.eq(contentId) }.map { it[postIdColumn] }
        }
    }

    fun linkPostAndContents(postId: PostId, vararg contentIds: ContentId) {
        transaction {
            val leftToPut = contentIds.toSet() - getPostContents(postId)
            leftToPut.forEach { contentId ->
                insert {
                    it[postIdColumn] = postId
                    it[contentIdColumn] = contentId
                }
            }
        }
    }
    fun unlinkPostAndContents(postId: PostId, vararg contentIds: ContentId): Boolean {
        return transaction {
            deleteWhere {
                postIdColumn.eq(postId).and(contentIdColumn.inList(contentIds.toList()))
            } > 0
        }
    }
}

private class PostsAPIDatabaseTable(
    private val database: Database
) : PostsAPI, Table() {
    private val contentsTable = PostsAPIContentRelations(database)

    private val idColumn = text("postId")
    private val creationDateColumn = datetime("creationDate").default(org.joda.time.DateTime.now())


    private val postCreatedBroadcastChannel = BroadcastChannel<RegisteredPost>(Channel.BUFFERED)
    override val postCreatedFlow: Flow<RegisteredPost> = postCreatedBroadcastChannel.asFlow()

    private val postDeletedBroadcastChannel = BroadcastChannel<RegisteredPost>(Channel.BUFFERED)
    override val postDeletedFlow: Flow<RegisteredPost> = postDeletedBroadcastChannel.asFlow()

    private val postUpdatedBroadcastChannel = BroadcastChannel<RegisteredPost>(Channel.BUFFERED)
    override val postUpdatedFlow: Flow<RegisteredPost> = postUpdatedBroadcastChannel.asFlow()

    private inline fun <T> transaction(noinline body: Transaction.() -> T): T = database.transaction(body)

    init {
        transaction {
            SchemaUtils.createMissingTablesAndColumns(this@PostsAPIDatabaseTable)
        }
    }

    private fun ResultRow.toRegisteredPost(): RegisteredPost = get(idColumn).let { id ->
        SimpleRegisteredPost(
            id,
            contentsTable.getPostContents(id),
            DateTime(get(creationDateColumn).millis)
        )
    }

    override suspend fun createPost(post: Post): RegisteredPost? {
        val id = generatePostId()
        return transaction {
            insert {
                it[idColumn] = id
            }
            contentsTable.linkPostAndContents(id, *post.content.toTypedArray())
            select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost()
        } ?.also {
            postCreatedBroadcastChannel.send(it)
        }
    }

    override suspend fun deletePost(id: PostId): Boolean {
        val post = getPostById(id) ?: return false
        return (transaction {
            deleteWhere { idColumn.eq(id) }
        } > 0).also {
            if (it) {
                postDeletedBroadcastChannel.send(post)
                contentsTable.unlinkPostAndContents(id, *post.content.toTypedArray())
            }
        }
    }

    override suspend fun updatePostContent(postId: PostId, post: Post): Boolean {
        return transaction {
            val alreadyLinked = contentsTable.getPostContents(postId)
            val toRemove = alreadyLinked - post.content
            val toInsert = post.content - alreadyLinked
            val updated = (toRemove.isNotEmpty() && contentsTable.unlinkPostAndContents(postId, *toRemove.toTypedArray())) || toInsert.isNotEmpty()
            if (toInsert.isNotEmpty()) {
                contentsTable.linkPostAndContents(postId, *toInsert.toTypedArray())
            }
            updated
        }.also {
            if (it) {
                getPostById(postId) ?.also { updatedPost -> postUpdatedBroadcastChannel.send(updatedPost) }
            }
        }
    }
    override suspend fun getPostsIds(): Set<PostId> {
        return transaction {
            selectAll().map { it[idColumn] }.toSet()
        }
    }

    override suspend fun getPostById(id: PostId): RegisteredPost? {
        return transaction {
            select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost()
        }
    }

    override suspend fun getPostsByContent(id: ContentId): List<RegisteredPost> {
        return transaction {
            val postsIds = contentsTable.getContentPosts(id)
            select { idColumn.inList(postsIds) }.map { it.toRegisteredPost() }
        }
    }

    override suspend fun getPostsByCreatingDates(from: DateTime, to: DateTime): List<RegisteredPost> {
        return transaction {
            select { creationDateColumn.between(from, to) }.map { it.toRegisteredPost() }
        }
    }

    override suspend fun getPostsByPagination(pagination: Pagination): PaginationResult<out RegisteredPost> {
        return transaction {
            val posts = selectAll().limit(pagination.size, pagination.firstIndex).orderBy(creationDateColumn).map {
                it.toRegisteredPost()
            }
            val postsNumber = selectAll().count()
            pagination.createResult(postsNumber, posts)
        }
    }

}

class ExposedPostsAPI (
    database: Database
) : PostsAPI by PostsAPIDatabaseTable(database)