package com.securityc4po.api.pentest.comment

import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.error.handler.*
import com.securityc4po.api.configuration.error.handler.EntityNotFoundException
import com.securityc4po.api.configuration.error.handler.InvalidModelException
import com.securityc4po.api.configuration.error.handler.TransactionInterruptedException
import com.securityc4po.api.extensions.getLoggerFor
import com.securityc4po.api.pentest.PentestService
import com.securityc4po.api.pentest.finding.*
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
import java.time.Instant

@Service
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
class CommentService(private val commentRepository: CommentRepository, private val pentestService: PentestService) {

    var logger = getLoggerFor<CommentService>()

    /**
     * Get all [Comments]s by commentId's
     *
     * @return list of [Comment]s
     */
    fun getCommentsByIds(commentIds: List<String>): Mono<List<Comment>> {
        return commentRepository.findCommentsByIds(commentIds).collectList().map {
            it.map { commentEntity -> commentEntity.toComment() }
        }.switchIfEmpty {
            val msg = "Comment not found."
            val ex = EntityNotFoundException(msg, Errorcode.CommentsNotFound)
            logger.warn(msg, ex)
            throw ex
        }
    }

    /**
     * Get [Comment] by commentId
     *
     * @return of [Comment]
     */
    fun getCommentById(commentId: String): Mono<Comment> {
        return this.commentRepository.findCommentById(commentId).switchIfEmpty {
            logger.warn("Comment with id $commentId not found.")
            val msg = "Comment with id $commentId not found."
            val ex = EntityNotFoundException(msg, Errorcode.CommentNotFound)
            throw ex
        }.map { it.toComment() }
    }

    /**
     * Save [Comment]
     *
     * @throws [InvalidModelException] if the [Comment] is invalid
     * @throws [TransactionInterruptedException] if the [Comment] could not be stored
     * @return saved [Comment]
     */
    fun saveComment(pentestId: String, body: CommentRequestBody): Mono<Comment> {
        validate(
            require = body.isValid(),
            logging = { logger.warn("Comment not valid.") },
            mappedException = InvalidModelException(
                "Comment not valid.", Errorcode.CommentInvalid
            )
        )
        val comment = body.toComment()
        val commentEntity = CommentEntity(comment)
        return commentRepository.insert(commentEntity).flatMap { newCommentEntity: CommentEntity ->
            val comment = newCommentEntity.toComment()
            // After successfully saving comment add id to pentest
            pentestService.updatePentestComment(pentestId, comment.id).onErrorMap {
                TransactionInterruptedException(
                    "Pentest could not be updated in Database.",
                    Errorcode.PentestInsertionFailed
                )
            }.map {
                comment
            }
        }.doOnError {
            throw wrappedException(
                logging = { logger.warn("Comment could not be stored in Database. Thrown exception: ", it) },
                mappedException = TransactionInterruptedException(
                    "Comment could not be stored.",
                    Errorcode.CommentInsertionFailed
                )
            )
        }
    }

    /**
     * Update [Comment]
     *
     * @throws [InvalidModelException] if the [Comment] is invalid
     * @throws [TransactionInterruptedException] if the [Comment] could not be stored
     * @return saved [Comment]
     */
    fun updateComment(commentId: String, body: CommentRequestBody): Mono<Comment> {
        validate(
            require = body.isValid(),
            logging = { logger.warn("Comment not valid.") },
            mappedException = InvalidModelException(
                "Comment not valid.", Errorcode.CommentInvalid
            )
        )
        return this.commentRepository.findCommentById(commentId).switchIfEmpty {
            logger.warn("Comment with id $commentId not found.")
            val msg = "Comment with id $commentId not found."
            val ex = EntityNotFoundException(msg, Errorcode.CommentNotFound)
            throw ex
        }.flatMap { currentCommentEntity: CommentEntity ->
            currentCommentEntity.lastModified = Instant.now()
            currentCommentEntity.data = buildComment(body, currentCommentEntity)
            commentRepository.save(currentCommentEntity).map {
                it.toComment()
            }
        }.doOnError {
            throw wrappedException(
                logging = { logger.warn("Comment could not be updated in Database. Thrown exception: ", it) },
                mappedException = TransactionInterruptedException(
                    "Comment could not be stored.",
                    Errorcode.CommentInsertionFailed
                )
            )
        }
    }

    /**
     * Delete [Comment]
     *
     * @throws [TransactionInterruptedException] if the [Comment] could not be deleted
     * @return deleted [Comment]
     */
    fun deleteCommentByPentestAndCommentId(pentestId: String, commentId: String): Mono<Comment> {
        return commentRepository.findCommentById(commentId).switchIfEmpty {
            logger.info("Comment with id $commentId not found. Deletion not necessary.")
            Mono.empty()
        }.flatMap { commentEntity: CommentEntity ->
            val comment = commentEntity.toComment()
            commentRepository.deleteCommentById(commentId).flatMap {
                // After successfully deleting comment remove its id from pentest
                pentestService.removeCommentFromPentest(pentestId, commentId).onErrorMap {
                    TransactionInterruptedException(
                        "Pentest could not be updated in Database.",
                        Errorcode.PentestInsertionFailed
                    )
                }.map {
                    comment
                }
            }.doOnError {
                throw wrappedException(
                    logging = { logger.warn("Comment could not be deleted from Database. Thrown exception: ", it) },
                    mappedException = TransactionInterruptedException(
                        "Comment could not be deleted.",
                        Errorcode.CommentDeletionFailed
                    )
                )
            }
        }
    }
}
