/****************************************************************
 * 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.json

import eu.timepit.refined
import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, Preview}
import org.apache.james.jmap.core.Id.IdConstraint
import org.apache.james.jmap.core.{Properties, UuidState}
import org.apache.james.jmap.mail.{AddressesHeaderValue, BlobId, Charset, DateHeaderValue, Disposition, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailChangesRequest, EmailChangesResponse, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, GroupName, GroupedAddressesHeaderValue, HasAttachment, HeaderMessageId, HeaderURL, IsEncodingProblem, IsTruncated, Keyword, Keywords, Language, Languages, Location, MailboxIds, MessageIdsHeaderValue, Name, PartId, RawHeaderValue, Subject, TextHeaderValue, ThreadId, Type, URLsHeaderValue, UnparsedEmailId}
import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId}
import play.api.libs.functional.syntax._
import play.api.libs.json._


object EmailBodyPartToSerialize {
  def from(part: EmailBodyPart): EmailBodyPartToSerialize = EmailBodyPartToSerialize(
    partId = part.partId,
    blobId = part.blobId,
    headers = part.headers,
    size = part.size,
    `type` = part.`type`,
    charset = part.charset,
    disposition = part.disposition,
    cid = part.cid,
    language = part.language,
    location = part.location,
    name = part.name,
    subParts = part.subParts.map(list => list.map(EmailBodyPartToSerialize.from)))
}

case class EmailBodyPartToSerialize(partId: PartId,
                                    blobId: Option[BlobId],
                                    headers: List[EmailHeader],
                                    size: Size,
                                    name: Option[Name],
                                    `type`: Type,
                                    charset: Option[Charset],
                                    disposition: Option[Disposition],
                                    cid: Option[Cid],
                                    language: Option[Languages],
                                    location: Option[Location],
                                    subParts: Option[List[EmailBodyPartToSerialize]])

object EmailGetSerializer {
  private implicit val mailboxIdWrites: Writes[MailboxId] = mailboxId => JsString(mailboxId.serialize)

  private implicit val fetchTextBodyValuesReads: Reads[FetchTextBodyValues] = Json.valueReads[FetchTextBodyValues]
  private implicit val fetchHTMLBodyValuesReads: Reads[FetchHTMLBodyValues] = Json.valueReads[FetchHTMLBodyValues]
  private implicit val fetchAllBodyValuesReads: Reads[FetchAllBodyValues] = Json.valueReads[FetchAllBodyValues]
  private implicit val blobIdWrites: Writes[BlobId] = Json.valueWrites[BlobId]
  private implicit val cidWrites: Writes[Cid] = cid => JsString(cid.getValue)
  private implicit val nameWrites: Writes[Name] = Json.valueWrites[Name]
  private implicit val threadIdWrites: Writes[ThreadId] = Json.valueWrites[ThreadId]
  private implicit val mailboxIdsWrites: Writes[MailboxIds] = ids => JsObject(ids.value.map(id => (id.serialize(), JsBoolean(true))))
  private implicit val typeWrites: Writes[Type] = Json.valueWrites[Type]
  private implicit val charsetWrites: Writes[Charset] = Json.valueWrites[Charset]
  private implicit val dispositionWrites: Writes[Disposition] = Json.valueWrites[Disposition]
  private implicit val languageWrites: Format[Language] = Json.valueFormat[Language]
  private implicit val languagesFormat: Format[Languages] = Json.valueFormat[Languages]
  private implicit val locationWrites: Writes[Location] = Json.valueWrites[Location]
  private implicit val emailerNameWrites: Writes[EmailerName] = Json.valueWrites[EmailerName]
  private implicit val emailAddressWrites: Writes[EmailAddress] = Json.writes[EmailAddress]
  private implicit val headerMessageIdWrites: Writes[HeaderMessageId] = Json.valueWrites[HeaderMessageId]
  private implicit val isEncodingProblemWrites: Writes[IsEncodingProblem] = Json.valueWrites[IsEncodingProblem]
  private implicit val isTruncatedWrites: Writes[IsTruncated] = Json.valueWrites[IsTruncated]
  private implicit val partIdWrites: Writes[PartId] = partId => JsString(partId.serialize)
  private implicit val previewWrites: Writes[Preview] = preview => JsString(preview.getValue)
  private implicit val hasAttachmentWrites: Writes[HasAttachment] = Json.valueWrites[HasAttachment]
  private implicit val headerNameWrites: Writes[EmailHeaderName] = Json.valueWrites[EmailHeaderName]
  private implicit val rawHeaderWrites: Writes[RawHeaderValue] = Json.valueWrites[RawHeaderValue]
  private implicit val textHeaderWrites: Writes[TextHeaderValue] = Json.valueWrites[TextHeaderValue]
  private implicit val addressesHeaderWrites: Writes[AddressesHeaderValue] = Json.valueWrites[AddressesHeaderValue]
  private implicit val GroupNameWrites: Writes[GroupName] = Json.valueWrites[GroupName]
  private implicit val emailAddressGroupWrites: Writes[EmailAddressGroup] = (o: EmailAddressGroup) =>
    Json.obj(
      "name" -> Json.toJson(o.name),
      "addresses" -> Json.toJson(o.addresses))
  private implicit val groupedAddressesHeaderWrites: Writes[GroupedAddressesHeaderValue] = Json.valueWrites[GroupedAddressesHeaderValue]
  private implicit val messageIdsHeaderWrites: Writes[MessageIdsHeaderValue] = Json.valueWrites[MessageIdsHeaderValue]
  private implicit val dateHeaderWrites: Writes[DateHeaderValue] = Json.valueWrites[DateHeaderValue]
  private implicit val headerURLWrites: Writes[HeaderURL] = Json.valueWrites[HeaderURL]
  private implicit val urlsHeaderWrites: Writes[URLsHeaderValue] = Json.valueWrites[URLsHeaderValue]
  private implicit val emailHeaderWrites: Writes[EmailHeaderValue] = {
    case headerValue: RawHeaderValue => Json.toJson[RawHeaderValue](headerValue)
    case headerValue: TextHeaderValue => Json.toJson[TextHeaderValue](headerValue)
    case headerValue: AddressesHeaderValue => Json.toJson[AddressesHeaderValue](headerValue)
    case headerValue: GroupedAddressesHeaderValue => Json.toJson[GroupedAddressesHeaderValue](headerValue)
    case headerValue: MessageIdsHeaderValue => Json.toJson[MessageIdsHeaderValue](headerValue)
    case headerValue: DateHeaderValue => Json.toJson[DateHeaderValue](headerValue)
    case headerValue: URLsHeaderValue => Json.toJson[URLsHeaderValue](headerValue)
  }
  private implicit val headersWrites: Writes[EmailHeader] = Json.writes[EmailHeader]
  private implicit val bodyValueWrites: Writes[EmailBodyValue] = Json.writes[EmailBodyValue]
  private implicit val unparsedMessageIdWrites: Writes[UnparsedEmailId] = Json.valueWrites[UnparsedEmailId]
  private implicit val unparsedMessageIdReads: Reads[UnparsedEmailId] = {
    case JsString(string) => refined.refineV[IdConstraint](string)
      .fold(
        e => JsError(s"emailId does not match Id constraints: $e"),
        id => JsSuccess(UnparsedEmailId(id)))
    case _ => JsError("emailId needs to be represented by a JsString")
  }
  private implicit val emailIdsReads: Reads[EmailIds] = Json.valueReads[EmailIds]
  private implicit val emailGetRequestReads: Reads[EmailGetRequest] = Json.reads[EmailGetRequest]

  private implicit val emailChangesRequestReads: Reads[EmailChangesRequest] = Json.reads[EmailChangesRequest]
  private implicit val subjectWrites: Writes[Subject] = Json.valueWrites[Subject]
  private implicit val emailNotFoundWrites: Writes[EmailNotFound] = Json.valueWrites[EmailNotFound]
  private implicit val messageIdWrites: Writes[MessageId] = id => JsString(id.serialize())
  private implicit val keywordWrites: Writes[Keyword] = Json.valueWrites[Keyword]
  private implicit val keywordsWrites: Writes[Keywords] = keywords => JsObject(keywords.asMap.map {
    case (keyword, b) => (keyword.flagName, JsBoolean(b))
  })

  private implicit val bodyValueMapWrites: Writes[Map[PartId, EmailBodyValue]] =
    mapWrites[PartId, EmailBodyValue](_.value.toString(), bodyValueWrites)

  private implicit val bodyPartWritesToSerializeWrites: Writes[EmailBodyPartToSerialize] = (
      (__ \ "partId").write[PartId] and
      (__ \ "blobId").writeNullable[BlobId] and
      (__ \ "headers").write[List[EmailHeader]] and
      (__ \ "size").write[Size] and
      (__ \ "name").writeNullable[Name] and
      (__ \ "type").write[Type] and
      (__ \ "charset").writeNullable[Charset] and
      (__ \ "disposition").writeNullable[Disposition] and
      (__ \ "cid").writeNullable[Cid] and
      (__ \ "language").writeNullable[Languages] and
      (__ \ "location").writeNullable[Location] and
      (__ \ "subParts").lazyWriteNullable(implicitly[Writes[List[EmailBodyPartToSerialize]]])
    )(unlift(EmailBodyPartToSerialize.unapply))

  private implicit val bodyPartWrites: Writes[EmailBodyPart] = part => bodyPartWritesToSerializeWrites.writes(EmailBodyPartToSerialize.from(part))

  private implicit val emailMetadataWrites: OWrites[EmailMetadata] = Json.writes[EmailMetadata]
  private implicit val emailHeadersWrites: Writes[EmailHeaders] = Json.writes[EmailHeaders]
  private implicit val emailBodyMetadataWrites: Writes[EmailBodyMetadata] = Json.writes[EmailBodyMetadata]

  private val emailFastViewWrites: OWrites[EmailFastView] = (JsPath.write[EmailMetadata] and
    JsPath.write[EmailHeaders] and
    JsPath.write[EmailBodyMetadata] and
    JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFastView.unapply))
  private val emailHeaderViewWrites: OWrites[EmailHeaderView] = (JsPath.write[EmailMetadata] and
    JsPath.write[EmailHeaders] and
    JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailHeaderView.unapply))
  private val emailMetadataViewWrites: OWrites[EmailMetadataView] = view => Json.toJsObject(view.metadata)
  private implicit val emailBodyWrites: Writes[EmailBody] = Json.writes[EmailBody]
  private implicit val emailFullViewWrites: OWrites[EmailFullView] = (JsPath.write[EmailMetadata] and
    JsPath.write[EmailHeaders] and
    JsPath.write[EmailBody] and
    JsPath.write[EmailBodyMetadata] and
    JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFullView.unapply))
  private implicit val emailWrites: OWrites[EmailView] = {
    case view: EmailMetadataView => emailMetadataViewWrites.writes(view)
    case view: EmailHeaderView => emailHeaderViewWrites.writes(view)
    case view: EmailFastView => emailFastViewWrites.writes(view)
    case view: EmailFullView => emailFullViewWrites.writes(view)
  }

  private implicit val stateWrites: Writes[UuidState] = Json.valueWrites[UuidState]
  private implicit val emailGetResponseWrites: Writes[EmailGetResponse] = Json.writes[EmailGetResponse]
  private implicit val changesResponseWrites: OWrites[EmailChangesResponse] = Json.writes[EmailChangesResponse]

  def serializeChanges(changesResponse: EmailChangesResponse): JsObject = Json.toJson(changesResponse).as[JsObject]

  def serialize(emailGetResponse: EmailGetResponse, properties: Properties, bodyProperties: Properties): JsValue =
    Json.toJson(emailGetResponse)
      .transform((__ \ "list").json.update {
        case JsArray(underlying) => JsSuccess(JsArray(underlying.map(js => js.transform {
          case jsonObject: JsObject =>
           bodyPropertiesFilteringTransformation(bodyProperties)
             .reads(properties.filter(jsonObject))
          case js => JsSuccess(js)
        }.fold(_ => JsArray(underlying), o => o))))
        case jsValue => JsSuccess(jsValue)
      }).get

  private def bodyPropertiesFilteringTransformation(bodyProperties: Properties): Reads[JsValue] = {
    case serializedMailbox: JsObject =>
      val bodyPropertiesToRemove = EmailBodyPart.allowedProperties -- bodyProperties
      val noop: JsValue => JsValue = o => o

      JsSuccess(Seq(
          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "attachments"),
          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "bodyStructure"),
          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "textBody"),
          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "htmlBody"))
        .reduceLeftOption(_ compose _)
        .getOrElse(noop)
        .apply(serializedMailbox))
    case js => JsSuccess(js)
  }

  private def bodyPropertiesFilteringTransformation(properties: Properties, field: String): JsValue => JsValue =
  {
    case JsObject(underlying) =>JsObject(underlying.map {
      case (key, jsValue) if key.equals(field) => (field, removeFieldsRecursively(properties).apply(jsValue))
      case (key, jsValue) => (key, jsValue)
    })
    case jsValue => jsValue
  }

  private def removeFieldsRecursively(properties: Properties): JsValue => JsValue = {
    case JsObject(underlying) => JsObject(underlying.flatMap {
      case (key, _) if properties.containsString(key) => None
      case (key, value) => Some((key, removeFieldsRecursively(properties).apply(value)))
    })
    case JsArray(others) => JsArray(others.map(removeFieldsRecursively(properties)))
    case o: JsValue => o
  }

  def deserializeEmailGetRequest(input: JsValue): JsResult[EmailGetRequest] = Json.fromJson[EmailGetRequest](input)

  def deserializeEmailChangesRequest(input: JsValue): JsResult[EmailChangesRequest] = Json.fromJson[EmailChangesRequest](input)
}