-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later

{-# LANGUAGE UndecidableInstances #-}

{-|
Description: Support for partial and conditional http requests (Range, ETag, If-Match, ...)
-}

module Utils.HttpConditional
  ( ByteRangesSpecifier(..), ByteRangeSpecification(..)
  , ByteContentRangeSpecification(..), ByteRangeResponseSpecification(..)
  , IsRangeUnit(..)
  , ETag(..)
  , RepresentationConditionalInformation(..)
  , mkResponseConditional
  , respondSourceConditional, respondSourceDBConditional
  ) where

import ClassyPrelude hiding (Builder)
import Yesod.Core
import Yesod.Persist.Core
import Data.Conduit
import qualified Data.Conduit.Combinators as C
import Data.Binary.Builder (Builder)

import Web.HttpApiData

import qualified Data.Attoparsec.Text as A

import Data.Char (chr, ord)

import qualified Data.Set as Set
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI

import Data.Time

import Network.HTTP.Types
import Network.HTTP.Types.Header

import Control.Lens
import Control.Lens.Extras

import Data.Kind (Type)
import Data.Coerce
import Data.Proxy

import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Fail (MonadFail(..))
import Control.Monad.Trans.Resource (ResourceT)

import Network.Wai

import Control.Monad.Random.Class

import qualified Data.ByteString.Base64.URL as Base64
import qualified Data.ByteString as BS

import Data.List.NonEmpty (NonEmpty(..))


ows :: A.Parser ()
ows = A.skipMany $ A.satisfy (`elem` [chr 0x20, chr 0x09])

httpList :: A.Parser a -> A.Parser [a]
httpList itemParser = do
  let sep = A.many1 $ ows *> A.char ',' <* ows
  A.skipMany sep
  xs <- itemParser `A.sepBy1` sep
  A.skipMany sep
  return xs

parseUrlPiece' :: A.Parser a -> (Text -> Either Text a)
parseUrlPiece' p = first pack . A.parseOnly (p <* A.endOfInput)


newtype ByteRangesSpecifier = ByteRangesSpecifier (NonNull (Set ByteRangeSpecification))
  deriving (Eq, Ord, Read, Show, Generic)
data ByteRangeSpecification
  = ByteRangeSpecification
      { byteRangeSpecFirstPosition :: Word64
      , byteRangeSpecLastPosition :: Maybe Word64
      }
  | ByteRangeSuffixSpecification
      { byteRangeSpecSuffixLength :: Word64
      }
  deriving (Eq, Ord, Read, Show, Generic)

instance FromHttpApiData ByteRangesSpecifier where
  parseUrlPiece = parseUrlPiece' parser
    where parser :: A.Parser ByteRangesSpecifier
          parser = do
            ranges <- httpList brSpecParser
            ByteRangesSpecifier <$> maybe (fail "Parser definition error: empty list of ByteRangeSpecifications") return (fromNullable $ Set.fromList ranges)
          brSpecParser :: A.Parser ByteRangeSpecification
          brSpecParser = brSpecParser' <|> brSuffixParser
            where brSpecParser' = do
                    byteRangeSpecFirstPosition <- A.decimal
                    void $ A.char '-'
                    byteRangeSpecLastPosition <- optional A.decimal
                    return ByteRangeSpecification{..}
                  brSuffixParser = do
                    void $ A.char '-'
                    byteRangeSpecSuffixLength <- A.decimal
                    return ByteRangeSuffixSpecification{..}

data ByteContentRangeSpecification
  = ByteContentRangeSpecification
      { byteRangeResponse :: Maybe ByteRangeResponseSpecification
      , byteRangeInstanceLength :: Maybe Word64
      } deriving (Eq, Ord, Read, Show, Generic)
data ByteRangeResponseSpecification
  = ByteRangeResponseSpecification
      { byteRangeResponseSpecFirstPosition
      , byteRangeResponseSpecLastPosition :: Word64
      } deriving (Eq, Ord, Read, Show, Generic)

instance ToHttpApiData ByteContentRangeSpecification where
  toUrlPiece ByteContentRangeSpecification{..} = maybe "*" encByteRangeResponse byteRangeResponse <> "/" <> maybe "*" encByteRangeInstanceLength byteRangeInstanceLength
    where
      encByteRangeInstanceLength = toUrlPiece
      encByteRangeResponse ByteRangeResponseSpecification{..} = toUrlPiece byteRangeResponseSpecFirstPosition <> "-" <> toUrlPiece byteRangeResponseSpecLastPosition


class (FromHttpApiData req, ToHttpApiData resp, Ord (SingularRangeSpecification req), Show resp) => IsRangeUnit req resp | req -> resp, resp -> req where
  type SingularRangeSpecification req :: Type
  rangeUnit :: forall p1 p2. p1 req -> p2 resp -> Text
  rangeRequestAll :: forall p. p req -> SingularRangeSpecification req
  _RangeSpecifications :: Iso' req (NonNull (Set (SingularRangeSpecification req)))
  default _RangeSpecifications :: Coercible req (NonNull (Set (SingularRangeSpecification req)))
                               => Iso' req (NonNull (Set (SingularRangeSpecification req)))
  _RangeSpecifications = coerced
  rangeInstanceLength :: resp -> Maybe Word64
  rangeInstanceLength _ = Nothing

instance IsRangeUnit ByteRangesSpecifier ByteContentRangeSpecification where
  type SingularRangeSpecification ByteRangesSpecifier = ByteRangeSpecification
  rangeUnit _ _ = "bytes"
  rangeRequestAll _ = ByteRangeSpecification 0 Nothing
  rangeInstanceLength = byteRangeInstanceLength


data ETag = WeakETag { unETag :: Text } | StrongETag { unETag :: Text }
  deriving (Eq, Ord, Read, Show, Generic)

parseETag :: A.Parser ETag
parseETag = do
  isWeak <- is _Just <$> optional (A.string "W/")
  void $ A.char '"'
  tag <- pack <$> many (A.satisfy isETagChar)
  void $ A.char '"'
  return $ bool StrongETag WeakETag isWeak tag
  where
    isETagChar c = c == '!'
                || (0x23 <= ord c && ord c <= 0x7e)
                || (0x80 <= ord c && ord c <= 0xff)

instance FromHttpApiData ETag where
  parseUrlPiece = parseUrlPiece' parseETag
instance ToHttpApiData ETag where
  toUrlPiece (WeakETag t) = "W/\"" <> t <> "\"" 
  toUrlPiece (StrongETag t) = "\"" <> t <> "\"" 

strongETagEq, weakETagEq :: ETag -> ETag -> Bool
strongETagEq (StrongETag a) (StrongETag b) = a == b
strongETagEq _ _ = False
weakETagEq = (==) `on` unETag
  
data RepresentationConditionalInformation = RepresentationConditionalInformation
  { representationETag :: Maybe ETag
  , representationLastModified :: Maybe UTCTime
  , representationExists :: Bool
  , requestedActionAlreadySucceeded :: Maybe Status
  } deriving (Eq, Ord, Show, Generic)

newtype ETagMatch = ETagMatch (Set ETag)
  deriving (Eq, Ord, Read, Show, Generic)
  deriving newtype (Semigroup, Monoid)

instance FromHttpApiData ETagMatch where
  parseUrlPiece = parseUrlPiece' parseIfMatch
    where parseIfMatch :: A.Parser ETagMatch
          parseIfMatch = parseEmptyIfMatch <|> parseNonEmptyIfMatch
          parseEmptyIfMatch = mempty <* A.char '*'
          parseNonEmptyIfMatch = ETagMatch . Set.fromList <$> httpList parseETag

parseHTTPTime :: A.Parser UTCTime
parseHTTPTime = do
  inpT <- A.takeText
  maybe (fail "Could not parse time specification") return . parseTimeM True defaultTimeLocale "%a, %d %b %Y %X %Z" $ unpack inpT

newtype ModifiedMatch = ModifiedMatch UTCTime
  deriving (Eq, Ord, Read, Show, Generic)

instance FromHttpApiData ModifiedMatch where
  parseUrlPiece = parseUrlPiece' $ ModifiedMatch <$> parseHTTPTime

data IfRange = IfRangeETag ETag | IfRangeModified UTCTime
  deriving (Eq, Ord, Read, Show, Generic)

instance FromHttpApiData IfRange where
  parseUrlPiece = parseUrlPiece' parseIfRange
    where parseIfRange = parseIfRangeETag <|> parseIfRangeModified
          parseIfRangeETag = IfRangeETag <$> parseETag
          parseIfRangeModified = IfRangeModified <$> parseHTTPTime

newtype RangeRequest req = RangeRequest { unRangeRequest :: req }
  deriving (Generic)
  deriving newtype (Eq, Ord, Read, Show)

instance IsRangeUnit req resp => FromHttpApiData (RangeRequest req) where
  parseUrlPiece = parseUrlPiece' parseRangeRequest
    where parseRangeRequest :: A.Parser (RangeRequest req)
          parseRangeRequest = do
            void . A.string $ rangeUnit (Proxy @req) (Proxy @resp)
            void $ A.char '='
            t <- A.takeText
            either (fail . unpack) (return . RangeRequest) $ parseUrlPiece t

newtype RangeResponse resp = RangeResponse resp
  deriving (Generic)
  deriving newtype (Eq, Ord, Read, Show)

instance IsRangeUnit req resp => ToHttpApiData (RangeResponse resp) where
  toUrlPiece (RangeResponse r) = rangeUnit (Proxy @req) (Proxy @resp) <> " " <> toUrlPiece r

newtype MultipartBoundary = MultipartBoundary ByteString
  deriving (Eq, Ord, Read, Show, Generic)

instance ToHttpApiData MultipartBoundary where
  toUrlPiece (MultipartBoundary bs) = decodeUtf8 $ Base64.encodeUnpadded bs
  
mkResponseConditional :: forall rangeReq rangeResp builder m m'.
                         ( MonadHandler m, Monad m'
                         , IsRangeUnit rangeReq rangeResp
                         , ToFlushBuilder builder
                         )
                      => RepresentationConditionalInformation
                      -> ContentType
                      -> Either (ConduitT () builder m' ()) (SingularRangeSpecification rangeReq -> (ConduitT () builder m' (), rangeResp))
                      -> m (Status, ContentType, ConduitT () (Flush Builder) m' ())
-- ^ Implementes https://tools.ietf.org/html/rfc7232#section-6
--
-- Assumes we are the origin server
mkResponseConditional RepresentationConditionalInformation{..} cType cont = liftHandler $ do
  isSafeMethod <- (`elem` safeMethods) . requestMethod <$> waiRequest

  for_ representationETag $ \etag ->
    replaceOrAddHeader (decodeUtf8 $ CI.original hETag) . decodeUtf8 $ toHeader etag
  for_ representationLastModified $ \lModified ->
    replaceOrAddHeader (decodeUtf8 $ CI.original hLastModified) $ formatRFC1123 lModified
  
  ifMatch <- lookupHeader' hIfMatch
  for_ ifMatch $ \(ETagMatch ps) -> if
    | null ps, representationExists -> return ()
    | Just etag <- representationETag
    , any (`strongETagEq` etag) ps -> return ()
    | Just retCode <- requestedActionAlreadySucceeded -> sendResponseStatus retCode ()
    | otherwise -> preconditionFailed

  ifUnmodifiedSince <- lookupHeader' hIfUnmodifiedSince
  for_ (guard (is _Nothing ifMatch) *> ifUnmodifiedSince) $ \(ModifiedMatch ts) -> if
    | Just lModified <- representationLastModified
    , lModified < addUTCTime (-precision) ts -> return ()
    | Just retCode <- requestedActionAlreadySucceeded -> sendResponseStatus retCode ()
    | otherwise -> preconditionFailed
    
  ifNoneMatch <- lookupHeader' hIfNoneMatch
  for_ ifNoneMatch $ \(ETagMatch ps) -> if
    | null ps, representationExists -> bool preconditionFailed notModified isSafeMethod
    | Just etag <- representationETag
    , any (`weakETagEq` etag) ps -> bool preconditionFailed notModified isSafeMethod
    | otherwise -> return ()

  ifModifiedSince <- lookupHeader' hIfModifiedSince
  for_ (guard (isSafeMethod && is _Nothing ifNoneMatch) *> ifModifiedSince) $ \(ModifiedMatch ts) -> if
    | Just lModified <- representationLastModified
    , lModified <= addUTCTime precision ts -> notModified
    | otherwise -> return ()

  case cont of
    Left evalNoRanges -> do
      replaceOrAddHeader (decodeUtf8 $ CI.original hAcceptRanges) "none"
      return (ok200, cType, evalNoRanges .| C.map toFlushBuilder)
    Right evalRange -> do
      replaceOrAddHeader (decodeUtf8 $ CI.original hAcceptRanges) $ rangeUnit (Proxy @rangeReq) (Proxy @rangeResp)

      mRanges <- do
        ifRange <- lookupHeader' hIfRange
        range <- lookupHeader' @(RangeRequest rangeReq) hRange
        case ifRange of
          Just (IfRangeETag p)
            | Just etag <- representationETag
            , p `strongETagEq` etag -> return range
          Just (IfRangeModified ts)
            | Just lModified <- representationLastModified
            , lModified < addUTCTime (-precision) ts -> return range
          Just _ -> return Nothing
          Nothing -> return range

      let ranges = maybe (rangeRequestAll (Proxy @rangeReq) :| []) (toNonEmpty . view _RangeSpecifications . unRangeRequest) mRanges

      when (length ranges > 5) $ do
        invalidArgs ["Too many ranges"]

      case ranges of
        r :| [] -> do
          let (respSrc, rResp) = evalRange r
          when (is _Just mRanges) $
            replaceOrAddHeader (decodeUtf8 $ CI.original hContentRange) . decodeUtf8 . toHeader $ RangeResponse rResp
          return (bool partialContent206 ok200 $ r == rangeRequestAll (Proxy @rangeReq), cType, respSrc .| C.map toFlushBuilder)
        (toList -> rs) -> do
          boundary <- liftIO $ MultipartBoundary . BS.pack <$> replicateM 12 getRandom
          let cType' = "multipart/byteranges; boundary=" <> toHeader boundary
              bodySrc = do
                forM_ rs $ \r -> do
                  let (respSrc, rResp) = evalRange r
                  sendChunkBS $ "--" <> toHeader boundary <> "\r\n"
                  sendChunkBS $ CI.original hContentType <> ": " <> cType <> "\r\n"
                  sendChunkBS $ CI.original hContentRange <> ": " <> toHeader (RangeResponse rResp) <> "\r\n"
                  sendChunkBS   "\r\n"
                  respSrc .| C.map toFlushBuilder
                  sendChunkBS   "\r\n"
                  sendFlush
                sendChunkBS $ "--" <> toHeader boundary <> "--\r\n"
          return (partialContent206, cType', bodySrc)
  
  where
    lookupHeader' :: forall hdr n. (MonadHandler n, FromHttpApiData hdr) => CI ByteString -> n (Maybe hdr)
    lookupHeader' hdrName = liftHandler . runMaybeT $ do
      hdrBS <- MaybeT $ lookupHeader hdrName
      case parseHeader hdrBS of
        Left errMsg -> do
          $logInfoS "lookupHeader'" $ "Could not parse value for request header “" <> decodeUtf8 (CI.original hdrName) <> "”, “" <> tshow hdrBS <> "”: " <> errMsg
          mzero
        Right val -> return val

    precision :: NominalDiffTime
    precision = 1

    safeMethods = [ methodGet, methodHead, methodOptions ]

    preconditionFailed = sendResponseStatus preconditionFailed412 ()

respondSourceConditional :: forall rangeReq rangeResp builder m a.
                            ( MonadHandler m
                            , IsRangeUnit rangeReq rangeResp
                            , ToFlushBuilder builder
                            )
                         => RepresentationConditionalInformation
                         -> ContentType
                         -> Either (ConduitT () builder (HandlerFor (HandlerSite m)) ()) (SingularRangeSpecification rangeReq -> (ConduitT () builder (HandlerFor (HandlerSite m)) (), rangeResp))
                         -> m a
respondSourceConditional cInfo cType cont = liftHandler $ do
  (rStatus, cType', cont') <- mkResponseConditional cInfo cType cont
  UnliftIO{..} <- askUnliftIO
  sendResponseStatus rStatus ( cType'
                             , toContent $
                                 transPipe (lift @ResourceT . unliftIO) cont'
                             )
  
respondSourceDBConditional :: forall rangeReq rangeResp builder m a.
                              ( MonadHandler m, YesodPersistRunner (HandlerSite m)
                              , IsRangeUnit rangeReq rangeResp
                              , ToFlushBuilder builder
                              )
                           => RepresentationConditionalInformation
                           -> ContentType
                           -> Either (ConduitT () builder (YesodDB (HandlerSite m)) ()) (SingularRangeSpecification rangeReq -> (ConduitT () builder (YesodDB (HandlerSite m)) (), rangeResp))
                           -> m a
respondSourceDBConditional cInfo cType cont = liftHandler $ do
  (rStatus, cType', cont') <- mkResponseConditional cInfo cType cont
  UnliftIO{..} <- askUnliftIO
  sendResponseStatus rStatus ( cType'
                             , toContent . transPipe (lift @ResourceT . unliftIO) $ runDBSource cont'
                             )
