/****************************************************************
 * 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.api.model

import java.net.URL
import java.security.interfaces.ECPublicKey
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.{Base64, UUID}

import com.google.crypto.tink.HybridEncrypt
import com.google.crypto.tink.apps.webpush.WebPushHybridEncrypt
import com.google.crypto.tink.subtle.EllipticCurves
import org.apache.james.jmap.api.model.ExpireTimeInvalidException.TIME_FORMATTER

import scala.util.Try

object PushSubscriptionId {
  def generate(): PushSubscriptionId = PushSubscriptionId(UUID.randomUUID)

  def parse(string: String): Either[IllegalArgumentException, PushSubscriptionId] = Try(UUID.fromString(string)).toEither
    .map(PushSubscriptionId(_))
    .left.map {
    case e: IllegalArgumentException => e
    case e => new IllegalArgumentException(e)
  }
}

case class PushSubscriptionId(value: UUID) {
  def serialise: String = value.toString
}

case class DeviceClientId(value: String) extends AnyVal

object VerificationCode {
  def generate(): VerificationCode = VerificationCode(UUID.randomUUID().toString)
}

case class VerificationCode(value: String) extends AnyVal

object PushSubscriptionServerURL {
  def from(value: String): Try[PushSubscriptionServerURL] = Try(PushSubscriptionServerURL(new URL(value)))
}

case class PushSubscriptionServerURL(value: URL)

case class PushSubscriptionExpiredTime(value: ZonedDateTime) {
  def isAfter(date: ZonedDateTime): Boolean = value.isAfter(date)
  def isBefore(date: ZonedDateTime): Boolean = value.isBefore(date)
}

case class PushSubscriptionKeys(p256dh: String, auth: String) {
  def validate: Either[IllegalArgumentException, PushSubscriptionKeys] =
    Try(asHybridEncrypt()).map(_ => this)
      .toEither
      .left.map {
      case e: IllegalArgumentException => e
      case e => new IllegalArgumentException(e)
    }

  // Follows https://datatracker.ietf.org/doc/html/rfc8291
  // Message Encryption for Web Push
  def encrypt(payload: Array[Byte]): Array[Byte] = asHybridEncrypt()
    .encrypt(payload, null)

  private def asHybridEncrypt(): HybridEncrypt =  new WebPushHybridEncrypt.Builder()
    .withAuthSecret(Base64.getUrlDecoder().decode(auth))
    .withRecipientPublicKey(asECPublicKey())
    .build()

  private def asECPublicKey(): ECPublicKey = EllipticCurves.getEcPublicKey(Base64.getUrlDecoder.decode(p256dh))
}

case class PushSubscriptionCreationRequest(deviceClientId: DeviceClientId,
                                           url: PushSubscriptionServerURL,
                                           keys: Option[PushSubscriptionKeys] = None,
                                           expires: Option[PushSubscriptionExpiredTime] = None,
                                           types: Seq[TypeName]) {

  def validate: Either[IllegalArgumentException, PushSubscriptionCreationRequest] =
    for {
      _ <- validateTypes
      _ <- validateKeys
    } yield {
      this
    }

  private def validateTypes: Either[IllegalArgumentException, PushSubscriptionCreationRequest] =
    if (types.isEmpty) {
      scala.Left(new IllegalArgumentException("types must not be empty"))
    } else {
      Right(this)
    }

  private def validateKeys: Either[IllegalArgumentException, PushSubscriptionCreationRequest] =
    keys.map(_.validate.map(_ => this))
      .getOrElse(Right(this))
}

object PushSubscription {
  val VALIDATED: Boolean = true
  val EXPIRES_TIME_MAX_DAY: Int = 7

  def from(creationRequest: PushSubscriptionCreationRequest,
           expireTime: PushSubscriptionExpiredTime): PushSubscription =
    PushSubscription(id = PushSubscriptionId.generate(),
      deviceClientId = creationRequest.deviceClientId,
      url = creationRequest.url,
      keys = creationRequest.keys,
      verificationCode = VerificationCode.generate(),
      validated = !VALIDATED,
      expires = expireTime,
      types = creationRequest.types)
}

case class PushSubscription(id: PushSubscriptionId,
                            deviceClientId: DeviceClientId,
                            url: PushSubscriptionServerURL,
                            keys: Option[PushSubscriptionKeys],
                            verificationCode: VerificationCode,
                            validated: Boolean,
                            expires: PushSubscriptionExpiredTime,
                            types: Seq[TypeName]) {
  def withTypes(types: Seq[TypeName]): PushSubscription = copy(types = types)

  def verified(): PushSubscription = copy(validated = true)

  def withExpires(expires: PushSubscriptionExpiredTime): PushSubscription = copy(expires = expires)
}

case class PushSubscriptionNotFoundException(id: PushSubscriptionId) extends RuntimeException

object ExpireTimeInvalidException {
  val TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX")
}
case class ExpireTimeInvalidException(expires: ZonedDateTime, message: String) extends IllegalStateException(s"`${expires.format(TIME_FORMATTER)}` $message")

case class DeviceClientIdInvalidException(deviceClientId: DeviceClientId, message: String) extends IllegalArgumentException(s"`${deviceClientId.value}` $message")
case class InvalidPushSubscriptionKeys(keys: PushSubscriptionKeys) extends IllegalArgumentException