/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 ****************************************************************/

package org.apache.james.jmap.mail

import javax.inject.Inject
import org.apache.james.jmap.core.UnsignedInt
import org.apache.james.jmap.mail.MailboxName.MailboxName
import org.apache.james.jmap.utils.quotas.QuotaLoader
import org.apache.james.mailbox._
import org.apache.james.mailbox.exception.{MailboxNameException, MailboxNotFoundException}
import org.apache.james.mailbox.model.MailboxACL.EntryKey
import org.apache.james.mailbox.model.{MailboxCounters, MailboxId, MailboxMetaData, MailboxPath, MailboxACL => JavaMailboxACL}
import reactor.core.scala.publisher.SMono

import scala.jdk.CollectionConverters._
import scala.jdk.OptionConverters._

object MailboxValidation {
  private def retrieveMailboxName(mailboxPath: MailboxPath, pathDelimiter: Char): Either[MailboxNameException, MailboxName] =
    mailboxPath.getName
      .split(pathDelimiter)
      .lastOption match {
        case Some(name) => MailboxName.validate(name)
        case None => Left(new MailboxNameException("No name for the mailbox found"))
      }

  def validate(mailboxPath: MailboxPath,
               pathDelimiter: Char,
               unreadEmails: Long,
               unreadThreads: Long,
               totalEmails: Long,
               totalThreads: Long): Either[Exception, MailboxValidation] = {
    for {
      validatedName <- retrieveMailboxName(mailboxPath, pathDelimiter)
      validatedUnreadEmails <- UnsignedInt.validate(unreadEmails).map(UnreadEmails)
      validatedUnreadThreads <- UnsignedInt.validate(unreadThreads).map(UnreadThreads)
      validatedTotalEmails <- UnsignedInt.validate(totalEmails).map(TotalEmails)
      validatedTotalThreads <- UnsignedInt.validate(totalThreads).map(TotalThreads)
    } yield MailboxValidation(
      mailboxName = validatedName,
      unreadEmails = validatedUnreadEmails,
      unreadThreads = validatedUnreadThreads,
      totalEmails = validatedTotalEmails,
      totalThreads = validatedTotalThreads)
  }
}

case class MailboxValidation(mailboxName: MailboxName,
                             unreadEmails: UnreadEmails,
                             unreadThreads: UnreadThreads,
                             totalEmails: TotalEmails,
                             totalThreads: TotalThreads)

case class Subscriptions(subscribedNames: Set[MailboxPath]) {
  def isSubscribed(name: MailboxPath): IsSubscribed = IsSubscribed(subscribedNames.contains(name))

  def isSubscribed(metaData: MailboxMetaData): IsSubscribed = isSubscribed(metaData.getPath)
}

class MailboxFactory @Inject() (mailboxManager: MailboxManager,
                                namespaceFactory: NamespaceFactory) {

  private def getRole(mailboxPath: MailboxPath, mailboxSession: MailboxSession): Option[Role] = Role.from(mailboxPath.getName)
    .filter(_ => mailboxPath.belongsTo(mailboxSession)).toScala

  private def getSortOrder(role: Option[Role]): SortOrder = role.map(SortOrder.getSortOrder).getOrElse(SortOrder.defaultSortOrder)

  private def getRights(resolveMailboxACL: JavaMailboxACL): Rights = Rights.fromACL(MailboxACL.fromJava(resolveMailboxACL))

  private def getNamespace(mailboxPath: MailboxPath, mailboxSession: MailboxSession): MailboxNamespace =
    namespaceFactory.from(mailboxPath, mailboxSession)

  private def getParentPath(mailboxPath: MailboxPath, mailboxSession: MailboxSession): Option[MailboxPath] = mailboxPath
    .getHierarchyLevels(mailboxSession.getPathDelimiter)
    .asScala
    .reverse
    .drop(1)
    .headOption

  private def aclEntryKey(mailboxSession: MailboxSession): EntryKey = EntryKey.createUserEntryKey(mailboxSession.getUser)

  private def getMyRights(mailboxPath: MailboxPath, resolveMailboxACL: JavaMailboxACL, mailboxSession: MailboxSession): MailboxRights = mailboxPath.belongsTo(mailboxSession) match {
    case true => MailboxRights.FULL
    case false =>
      val rights = Rfc4314Rights.fromJava(resolveMailboxACL
        .getEntries
        .getOrDefault(aclEntryKey(mailboxSession), JavaMailboxACL.NO_RIGHTS))
        .toRights
      MailboxRights(
        mayReadItems = MayReadItems(rights.contains(Right.Read)),
        mayAddItems = MayAddItems(rights.contains(Right.Insert)),
        mayRemoveItems = MayRemoveItems(rights.contains(Right.DeleteMessages)),
        maySetSeen = MaySetSeen(rights.contains(Right.Seen)),
        maySetKeywords = MaySetKeywords(rights.contains(Right.Write)),
        mayCreateChild = MayCreateChild(false),
        mayRename = MayRename(false),
        mayDelete = MayDelete(false),
        maySubmit = MaySubmit(false))
  }

  def create(mailboxMetaData: MailboxMetaData,
             mailboxSession: MailboxSession,
             subscriptions: Subscriptions,
             allMailboxesMetadata: Map[MailboxPath, MailboxMetaData],
             quotaLoader: QuotaLoader): SMono[Mailbox] = {

    val sanitizedCounters: MailboxCounters.Sanitized = mailboxMetaData.getCounters.sanitize()

    MailboxValidation.validate(mailboxMetaData.getPath, mailboxSession.getPathDelimiter, sanitizedCounters.getUnseen, sanitizedCounters.getUnseen, sanitizedCounters.getCount, sanitizedCounters.getCount) match {
      case Left(error) => SMono.error(error)
      case scala.Right(mailboxValidation) =>
        SMono.fromPublisher(quotaLoader.getQuotas(mailboxMetaData.getPath))
          .map(quotas => {
            val id: MailboxId = mailboxMetaData.getId
            val role: Option[Role] = getRole(mailboxMetaData.getPath, mailboxSession)
            val sortOrder: SortOrder = getSortOrder(role)
            val rights: Rights = getRights(mailboxMetaData.getResolvedAcls)
            val namespace: MailboxNamespace = getNamespace(mailboxMetaData.getPath, mailboxSession)
            val parentPath: Option[MailboxPath] = getParentPath(mailboxMetaData.getPath, mailboxSession)
            val parentId: Option[MailboxId] = parentPath.flatMap(path => allMailboxesMetadata.get(path)).map(_.getId)
            val myRights: MailboxRights = getMyRights(mailboxMetaData.getPath, mailboxMetaData.getResolvedAcls, mailboxSession)
            val isSubscribed: IsSubscribed = subscriptions.isSubscribed(mailboxMetaData)

            Mailbox(
              id = id,
              name = mailboxValidation.mailboxName,
              parentId = parentId,
              role = role,
              sortOrder = sortOrder,
              unreadEmails = mailboxValidation.unreadEmails,
              totalEmails = mailboxValidation.totalEmails,
              unreadThreads = mailboxValidation.unreadThreads,
              totalThreads = mailboxValidation.totalThreads,
              myRights = myRights,
              namespace = namespace,
              rights = rights,
              quotas = quotas,
              isSubscribed = isSubscribed)
          })
    }
  }

  def create(id: MailboxId, mailboxSession: MailboxSession, quotaLoader: QuotaLoader, subscriptions: Subscriptions): SMono[Mailbox] =
    for {
      messageManager <- SMono(mailboxManager.getMailboxReactive(id, mailboxSession))
      counters <- SMono(messageManager.getMailboxCountersReactive(mailboxSession))
      sanitizedCounters = counters.sanitize()
      mailbox <- instanciateMailbox(id, mailboxSession, quotaLoader, messageManager, sanitizedCounters, subscriptions)
    } yield {
      mailbox
    }

  private def instanciateMailbox(id: MailboxId, mailboxSession: MailboxSession, quotaLoader: QuotaLoader, messageManager: MessageManager, sanitizedCounters: MailboxCounters.Sanitized, subscriptions: Subscriptions): SMono[Mailbox] = {
    try {
      MailboxValidation.validate(messageManager.getMailboxPath, mailboxSession.getPathDelimiter, sanitizedCounters.getUnseen, sanitizedCounters.getUnseen, sanitizedCounters.getCount, sanitizedCounters.getCount) match {
        case Left(error) => SMono.error(error)
        case scala.Right(mailboxValidation) =>

          SMono.zip(array => (array(0).asInstanceOf[Option[MailboxId]],
              array(1).asInstanceOf[Quotas]),
              getParentId(messageManager, mailboxSession),
              SMono.fromPublisher(quotaLoader.getQuotas(messageManager.getMailboxPath)))
            .map(tuple2 => {
              val resolvedACL = messageManager.getResolvedAcl(mailboxSession)
              val role: Option[Role] = getRole(messageManager.getMailboxPath, mailboxSession)
              val sortOrder: SortOrder = getSortOrder(role)
              val rights: Rights = getRights(resolvedACL)
              val namespace: MailboxNamespace = getNamespace(messageManager.getMailboxPath, mailboxSession)
              val myRights: MailboxRights = getMyRights(messageManager.getMailboxPath, resolvedACL, mailboxSession)
              val isSubscribed: IsSubscribed = subscriptions.isSubscribed(messageManager.getMailboxPath)

              Mailbox(
                id = id,
                name = mailboxValidation.mailboxName,
                parentId = tuple2._1,
                role = role,
                sortOrder = sortOrder,
                unreadEmails = mailboxValidation.unreadEmails,
                totalEmails = mailboxValidation.totalEmails,
                unreadThreads = mailboxValidation.unreadThreads,
                totalThreads = mailboxValidation.totalThreads,
                myRights = myRights,
                namespace = namespace,
                rights = rights,
                quotas = tuple2._2,
                isSubscribed = isSubscribed)
            })
      }
    } catch {
      case error: Exception => SMono.error(error)
    }
  }

  private def getParentId(messageManager: MessageManager, mailboxSession: MailboxSession): SMono[Option[MailboxId]] =
    getParentPath(messageManager.getMailboxPath, mailboxSession)
      .map(parentPath => SMono(mailboxManager.getMailboxReactive(parentPath, mailboxSession))
        .map(_.getId)
        .map(Some(_))
        .onErrorResume {
          case _: MailboxNotFoundException => SMono.just(None)
          case e => SMono.error(e)
        })
      .getOrElse(SMono.just(None))
}