{-# language LambdaCase #-}
{-# language OverloadedStrings #-}
{-# language ScopedTypeVariables #-}
{-# language RankNTypes #-}
{-# language DataKinds #-}
{-# language KindSignatures #-}

{-# OPTIONS_GHC -Wall #-}

module Trasa.Extra
  ( -- * Route Functions
    IsRoute (..)
  , link
  , encodeRoute
  , decodeRoute
  , redirect
    -- * Header Functions
  , getHeader
  , currentHeader
  , setHeader
  , getCookies
  , lookupCookie
    -- * Codecs and Parsing
  , setCookie
  , pathPieceCodec
  , bodyAeson
  , aeson
  , decodeInt
  , err404
  ) where

import Control.Monad.Except (throwError)
import Control.Monad.Reader
import Control.Monad.State
import Data.Aeson (ToJSON(..), FromJSON(..), encode, eitherDecode', decode')
import Data.Bifunctor (first)
import Data.CaseInsensitive
import Data.Text (Text)
import Network.HTTP.Types as HTTP
import Trasa.Core hiding (optional)
import Trasa.Server
import Web.PathPieces
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Read as TR
import qualified Trasa.Method
import qualified Web.Cookie as Cookie

-- | Meta information about your route
class IsRoute route where
  metaF :: route caps qrys req resp -> MetaCodec caps qrys req resp

link :: IsRoute route => Concealed route -> Url
link c = concealedToPrepared c (linkWith (mapMeta captureEncoding captureEncoding id id . metaF))

encodeRoute :: IsRoute route => Concealed route -> Text
encodeRoute c = encodeUrl $ link c

decodeRoute :: IsRoute route => Router route -> Text -> Maybe (Concealed route)
decodeRoute router t = do
  let url = decodeUrl t
  either (const Nothing) Just (parseWith (mapMeta id captureDecoding (mapMany bodyDecoding) id . metaF) router Trasa.Method.get url Nothing)

redirect :: IsRoute route => Prepared route response -> LBS.ByteString -> TrasaT IO a
redirect route message = do
  setHeader "Location" (encodeRoute $ conceal route)
  throwError $ TrasaErr HTTP.status302 message

getHeader :: CI BS.ByteString -> TrasaT IO (Maybe T.Text)
getHeader idt = fmap (M.lookup idt . trasaHeaders) ask

currentHeader :: CI BS.ByteString -> TrasaT IO (Maybe T.Text)
currentHeader idt = fmap (M.lookup idt) get

setHeader :: CI BS.ByteString -> T.Text -> TrasaT IO ()
setHeader idt header = modify (M.insert idt header)

getCookies :: TrasaT IO (Maybe Cookie.Cookies)
getCookies = getHeader "Cookie" >>= \case
  Nothing -> pure Nothing
  Just rawCookie -> pure $ Just $ Cookie.parseCookies $ T.encodeUtf8 rawCookie

lookupCookie :: BS.ByteString -> TrasaT IO (Maybe BS.ByteString)
lookupCookie name = do
  getCookies >>= \case
    Nothing -> pure Nothing
    Just cookies -> pure $ lookup name cookies

setCookie :: Cookie.SetCookie -> TrasaT IO ()
setCookie cookie = do
  let cookie' :: Text
      cookie' = T.decodeUtf8 $ BL.toStrict $ BB.toLazyByteString $ Cookie.renderSetCookie cookie
  setHeader "Set-Cookie" cookie'

pathPieceCodec :: PathPiece piece => CaptureCodec piece
pathPieceCodec = CaptureCodec toPathPiece fromPathPiece

bodyAeson :: (ToJSON a, FromJSON a) => BodyCodec a
bodyAeson = BodyCodec (pure "application/json") encode (first T.pack . eitherDecode')

aeson :: (ToJSON a, FromJSON a) => CaptureCodec a
aeson = CaptureCodec (T.decodeUtf8 . LBS.toStrict . encode) (decode' . LBS.fromStrict .  T.encodeUtf8)

decodeInt :: Text -> Maybe Int
decodeInt x = case TR.decimal x of
  Left _ -> Nothing
  Right (i,leftover) -> if T.null leftover then Just i else Nothing

err404 :: Monad m => TrasaT m a
err404 = throwError (TrasaErr HTTP.status404 "Not found")