/*
 * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
 *
 * Licensed 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 zio.http

import java.security.MessageDigest
import java.util.Base64.getEncoder
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

import scala.collection.mutable

import zio._

import zio.http.internal.CookieEncoding

/**
 * Cookie is an immutable and type-safe representation of an HTTP cookie. There
 * are two types of cookies: request cookies and response cookies. These can be
 * created with the constructors in the companion object of Cookie.
 */
sealed trait Cookie { self =>
  def name: String

  def content: String

  /**
   * Encodes the cookie into a string, or fails with an exception if there was a
   * problem during encoding. This encoding method performs no validation.
   */
  final def encode: Either[Exception, String] = encodeValidate(false)

  /**
   * Encodes the cookie into a string, or fails with an exception if there was a
   * problem during encoding. This encoding method performs validation as
   * specified.
   */
  def encodeValidate(validate: Boolean): Either[Exception, String]

  /**
   * Converts the cookie to a request cookie. If the cookie is a response
   * cookie, then it is converted into a request cookie by discarding everything
   * except the name and content.
   */
  def toRequest: Cookie.Request = Cookie.Request(name, content)

  /**
   * Converts the cookie to a response cookie, using, if necessary, default
   * values for all the parameters of a response cookie.
   */
  def toResponse: Cookie.Response = toResponse()

  /**
   * Converts the cookie to a response cookie, using the specified values only
   * if this cookie is a request cookie.
   */
  def toResponse(
    domain: Option[String] = None,
    path: Option[Path] = None,
    isSecure: Boolean = false,
    isHttpOnly: Boolean = false,
    maxAge: Option[Duration] = None,
    sameSite: Option[Cookie.SameSite] = None,
  ): Cookie.Response = {
    self match {
      case Cookie.Request(_, _) =>
        Cookie.Response(name, content, domain, path, isSecure, isHttpOnly, maxAge, sameSite)
      case x: Cookie.Response   => x
    }
  }

  /**
   * Returns a new cookie derived from this one, but where the content of the
   * cookie is set to the specified value.
   */
  def content(content: String): Cookie

  /**
   * Returns a new cookie derived from this one, but where the name of the
   * cookie is set to the specified value.
   */
  def name(name: String): Cookie
}

object Cookie {

  /**
   * Creates a cookie with an expired maxAge
   */
  def clear(name: String): Cookie.Response = Cookie.Response(name, "", maxAge = Some(Duration.Zero))

  def decodeRequest(header: String, validate: Boolean = false): Either[Exception, Chunk[Cookie.Request]] =
    Cookie.Request.decode(header, validate)

  def decodeResponse(header: String, validate: Boolean = false): Either[Exception, Cookie.Response] =
    Cookie.Response.decode(header, validate)

  private def signature(secret: String, content: String): String = {
    val sha256    = Mac.getInstance("HmacSHA256")
    val secretKey = new SecretKeySpec(secret.getBytes(), "RSA")

    sha256.init(secretKey)

    val signed = sha256.doFinal(content.getBytes())
    val mda    = MessageDigest.getInstance("SHA-512")
    getEncoder.encodeToString(mda.digest(signed))
  }

  final case class Response(
    name: String,
    content: String,
    domain: Option[String] = None,
    path: Option[Path] = None,
    isSecure: Boolean = false,
    isHttpOnly: Boolean = false,
    maxAge: Option[Duration] = None,
    sameSite: Option[SameSite] = None,
  ) extends Cookie { self =>
    override def encodeValidate(validate: Boolean): Either[Exception, String] =
      try {
        Right(CookieEncoding.encodeResponseCookie(self, validate))
      } catch {
        case e: Exception => Left(e)
      }

    /**
     * Signs cookie content with a secret and returns a signed cookie.
     */
    def sign(secret: String): Cookie.Response =
      content(
        new mutable.StringBuilder()
          .append(content)
          .append('.')
          .append(Cookie.signature(secret, content))
          .result(),
      )

    override def name(name: String): Cookie.Response = copy(name = name)

    override def content(content: String): Cookie.Response = copy(content = content)
  }
  object Response {

    /**
     * Decodes a response cookie from a string.
     */
    def decode(header: String, validate: Boolean = false): Either[Exception, Cookie.Response] = {
      try {
        Right(CookieEncoding.decodeResponseCookie(header, validate))
      } catch {
        case e: Exception =>
          Left(e)
      }
    }
  }

  final case class Request(name: String, content: String) extends Cookie { self =>
    override def encodeValidate(validate: Boolean): Either[Exception, String] =
      try {
        Right(CookieEncoding.encodeRequestCookie(self, validate))
      } catch {
        case e: Exception => Left(e)
      }

    /**
     * Un-signs cookie content with a secret and returns an unsigned cookie.
     */
    def unSign(secret: String): Option[Cookie.Request] = {
      val index     = content.lastIndexOf('.')
      val signature = content.slice(index + 1, content.length)
      val value     = content.slice(0, index)
      if (Cookie.signature(secret, value) == signature) Some(self.content(value)) else None
    }

    override def name(name: String): Cookie.Request = copy(name = name)

    override def content(content: String): Cookie.Request = copy(content = content)

  }
  object Request {

    /**
     * Decodes a request cookie from a string.
     */
    def decode(header: String, validate: Boolean = false): Either[Exception, Chunk[Cookie.Request]] = {
      try {
        Right(CookieEncoding.decodeRequestCookie(header, validate))
      } catch {
        case e: Exception =>
          Left(e)
      }
    }
  }

  sealed trait SameSite
  object SameSite {
    case object Strict extends SameSite
    case object Lax    extends SameSite
    case object None   extends SameSite

    def values: List[SameSite] = List(Strict, Lax, None)
  }
}
