package com.securityc4po.api.pentest

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.InvalidModelException
import com.securityc4po.api.configuration.error.handler.TransactionInterruptedException
import com.securityc4po.api.extensions.getLoggerFor
import com.securityc4po.api.project.*
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 PentestService(
    private val pentestRepository: PentestRepository,
    private val projectService: ProjectService
) {

    var logger = getLoggerFor<PentestService>()

    /**
     * Get all [Pentest]s by projectId and category
     *
     * @return list of [Pentest]
     */
    fun getPentestsForCategory(projectId: String, category: PentestCategory): Mono<List<Pentest>> {
        return pentestRepository.findPentestByProjectIdAndCategory(projectId, category).collectList().map {
            it.map { pentestEntity -> pentestEntity.toPentest() }
        }
    }

    /**
     * Save [Pentest]
     *
     * @throws [InvalidModelException] if the [Pentest] is invalid
     * @throws [TransactionInterruptedException] if the [Pentest] could not be stored
     * @return saved [Pentest]
     */
    fun savePentest(projectId: String, body: PentestRequestBody): Mono<Pentest> {
        validate(
            require = body.isValid(),
            logging = { logger.warn("Pentest not valid.") },
            mappedException = InvalidModelException(
                "Pentest not valid.", Errorcode.PentestInvalid
            )
        )
        val pentest = body.toPentest()
        val pentestEntity = PentestEntity(pentest)
        return pentestRepository.insert(pentestEntity).flatMap { newPentestEntity: PentestEntity ->
            val newPentest = newPentestEntity.toPentest()
            // After successfully saving pentest add id and status to project
            val projectPentest = ProjectPentest(pentestId = newPentest.id, status = newPentest.status)
            projectService.updateProjectTestingProgress(projectId, projectPentest).onErrorMap {
                TransactionInterruptedException(
                    "Project Pentests could not be updated in Database.",
                    Errorcode.ProjectPentestInsertionFailed
                )
            }.map {
                newPentest
            }
        }.doOnError {
            throw wrappedException(
                logging = { logger.warn("Pentest could not be stored in Database. Thrown exception: ", it) },
                mappedException = TransactionInterruptedException(
                    "Pentest could not be stored.",
                    Errorcode.PentestInsertionFailed
                )
            )
        }
    }

    /**
     * Update [Pentest]
     *
     * @throws [InvalidModelException] if the [Pentest] is invalid
     * @throws [TransactionInterruptedException] if the [Pentest] could not be updated
     * @return updated [Pentest]
     */
    fun updatePentest(pentestId: String, body: PentestRequestBody): Mono<Pentest> {
        validate(
            require = body.isValid(),
            logging = { logger.warn("Pentest not valid.") },
            mappedException = InvalidModelException(
                "Pentest not valid.", Errorcode.PentestInvalid
            )
        )
        return pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Updating not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.flatMap { currentPentestEntity: PentestEntity ->
            currentPentestEntity.lastModified = Instant.now()
            currentPentestEntity.data = buildPentest(body, currentPentestEntity)
            pentestRepository.save(currentPentestEntity).flatMap { newPentestEntity: PentestEntity ->
                val pentest = newPentestEntity.toPentest()
                // After successfully saving pentest add id and status to project
                val projectPentest = ProjectPentest(pentestId = pentest.id, status = pentest.status)
                projectService.updateProjectTestingProgress(body.projectId, projectPentest).onErrorMap {
                    TransactionInterruptedException(
                        "Project Pentest could not be updated in Database.",
                        Errorcode.ProjectPentestInsertionFailed
                    )
                }.map {
                    return@map newPentestEntity.toPentest()
                }
            }.doOnError {
                throw wrappedException(
                    logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
                    mappedException = TransactionInterruptedException(
                        "Pentest could not be updated.",
                        Errorcode.PentestInsertionFailed
                    )
                )
            }
        }
    }

    /**
     * Get all [Finding]Id's by pentestId
     *
     * @return list of [String]
     */
    fun getFindingIdsByPentestId(pentestId: String): Mono<List<String>> {
        return this.pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Collecting findings not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.map { pentestEntity -> pentestEntity.data.findingIds }
    }

    /**
     * Get all [Comment]Id's by pentestId
     *
     * @return list of [String]
     */
    fun getCommentIdsByPentestId(pentestId: String): Mono<List<String>> {
        return this.pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Collecting comments not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.map { pentestEntity -> pentestEntity.data.commentIds }
    }

    /**
     * Update [Pentest] for Finding
     *
     * @throws [InvalidModelException] if the [Pentest] is invalid
     * @throws [TransactionInterruptedException] if the [Pentest] could not be updated
     * @return updated [Pentest]
     */
    fun updatePentestFinding(pentestId: String, findingId: String): Mono<Pentest> {
        return pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Updating not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.flatMap { currentPentestEntity: PentestEntity ->
            if (currentPentestEntity.data.findingIds.find { pentestData -> pentestData == findingId } == null) {
                currentPentestEntity.data.findingIds += findingId
            }
            currentPentestEntity.lastModified = Instant.now()
            this.pentestRepository.save(currentPentestEntity).map {
                it.toPentest()
            }.doOnError {
                throw wrappedException(
                    logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
                    mappedException = TransactionInterruptedException(
                        "Pentest could not be updated.",
                        Errorcode.PentestInsertionFailed
                    )
                )
            }
        }
    }

    /**
     * Update [Pentest] for Finding
     *
     * @throws [InvalidModelException] if the [Pentest] is invalid
     * @throws [TransactionInterruptedException] if the [Pentest] could not be updated
     * @return updated [Pentest]
     */
    fun removeFindingFromPentest(pentestId: String, findingId: String): Mono<Pentest> {
        return pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Updating not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.flatMap { currentPentestEntity: PentestEntity ->
            if (currentPentestEntity.data.findingIds.find { pentestData -> pentestData == findingId } != null) {
                val findingIds = currentPentestEntity.data.findingIds.toMutableList()
                findingIds.remove(findingId)
                currentPentestEntity.data.findingIds = findingIds.toList()
            }
            currentPentestEntity.lastModified = Instant.now()
            this.pentestRepository.save(currentPentestEntity).map {
                it.toPentest()
            }.doOnError {
                throw wrappedException(
                    logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
                    mappedException = TransactionInterruptedException(
                        "Pentest could not be updated.",
                        Errorcode.PentestInsertionFailed
                    )
                )
            }
        }
    }

    /**
     * Update [Pentest] for Comment
     *
     * @throws [InvalidModelException] if the [Pentest] is invalid
     * @throws [TransactionInterruptedException] if the [Pentest] could not be updated
     * @return updated [Pentest]
     */
    fun updatePentestComment(pentestId: String, commentId: String): Mono<Pentest> {
        return pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Updating not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.flatMap { currentPentestEntity: PentestEntity ->
            if (currentPentestEntity.data.commentIds.find { pentestData -> pentestData == commentId } == null) {
                currentPentestEntity.data.commentIds += commentId
            }
            currentPentestEntity.lastModified = Instant.now()
            this.pentestRepository.save(currentPentestEntity).map {
                it.toPentest()
            }.doOnError {
                throw wrappedException(
                    logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
                    mappedException = TransactionInterruptedException(
                        "Pentest could not be updated.",
                        Errorcode.PentestInsertionFailed
                    )
                )
            }
        }
    }

    /**
     * Update [Pentest] for Comment
     *
     * @throws [InvalidModelException] if the [Pentest] is invalid
     * @throws [TransactionInterruptedException] if the [Pentest] could not be updated
     * @return updated [Pentest]
     */
    fun removeCommentFromPentest(pentestId: String, commentId: String): Mono<Pentest> {
        return pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Updating not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.flatMap { currentPentestEntity: PentestEntity ->
            if (currentPentestEntity.data.commentIds.find { pentestData -> pentestData == commentId } != null) {
                val commentIds = currentPentestEntity.data.commentIds.toMutableList()
                commentIds.remove(commentId)
                currentPentestEntity.data.commentIds = commentIds.toList()
            }
            currentPentestEntity.lastModified = Instant.now()
            this.pentestRepository.save(currentPentestEntity).map {
                it.toPentest()
            }.doOnError {
                throw wrappedException(
                    logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
                    mappedException = TransactionInterruptedException(
                        "Pentest could not be updated.",
                        Errorcode.PentestInsertionFailed
                    )
                )
            }
        }
    }

    /**
     * Enable or disable [Pentest]
     *
     * @throws [InvalidModelException] if the [Pentest] is invalid
     * @throws [TransactionInterruptedException] if the [Pentest] could not be enabled or disabled
     * @return  enabled or disabled [Pentest]
     */
    fun enableOrDisableObjectiveByPentestId(projectId: String, pentestId: String, enable: Boolean): Mono<Pentest> {
        return pentestRepository.findPentestById(pentestId).switchIfEmpty {
            logger.warn("Pentest with id $pentestId not found. Enabling not possible.")
            val msg = "Pentest with id $pentestId not found."
            val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
            throw ex
        }.flatMap { currentPentestEntity: PentestEntity ->
            if (enable) {
                // Enable Pentest
                currentPentestEntity.data.enabled = true
                if (currentPentestEntity.data.findingIds.isEmpty() && currentPentestEntity.data.commentIds.isEmpty()) {
                    currentPentestEntity.data.status = PentestStatus.NOT_STARTED
                } else {
                    currentPentestEntity.data.status = PentestStatus.PAUSED
                }
            } else {
                // Disable Pentest
                currentPentestEntity.data.enabled = false
                currentPentestEntity.data.status = PentestStatus.DISABLED
            }
            currentPentestEntity.lastModified = Instant.now()
            this.pentestRepository.save(currentPentestEntity).flatMap {updatedPentestEntity ->
                // After successfully enabling or disabling of pentest update id and status to project
                val projectPentest = ProjectPentest(pentestId = pentestId, status = currentPentestEntity.data.status)
                projectService.updateProjectTestingProgress(projectId, projectPentest).onErrorMap {
                    TransactionInterruptedException(
                        "Project Pentest could not be updated in Database.",
                        Errorcode.ProjectPentestInsertionFailed
                    )
                }.map {
                    return@map updatedPentestEntity.toPentest()
                }
            }.doOnError {
                throw wrappedException(
                    logging = { logger.warn("Pentest could not be enabled or disabled in Database. Thrown exception: ", it) },
                    mappedException = TransactionInterruptedException(
                        "Pentest could not be enabled or disabled.",
                        Errorcode.PentestInsertionFailed
                    )
                )
            }
        }
    }
}