{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}

-- TBD: basic-auth, gzip

module HttpBin.Server (serve) where

import Control.Applicative ((<$>))
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (Value(..), eitherDecode, object, toJSON)
import Data.Aeson.Encode.Pretty (Config(..), Indent(Spaces), defConfig, encodePretty')
import Data.ByteString.Char8 (pack)
import Data.CaseInsensitive (original)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Monoid ((<>))
import Data.Text.Encoding (decodeUtf8)
import Data.Text.Read (decimal)
import Data.Time.Clock (UTCTime(..))
import Data.UUID (toASCIIBytes)
import Data.UUID.V4 (nextRandom)
import Snap.Core
import Snap.Http.Server as Snap
import Snap.Util.GZip (withCompression)
import System.PosixCompat.Time (epochTime)
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as B
import qualified Data.Map as Map
import qualified Data.Text.Lazy.Encoding as Lazy

get = respond return

post = respond $ \obj -> do
  body <- readRequestBody 65536
  return $ obj <> [("data", toJSON (Lazy.decodeUtf8 body))] <>
           case eitherDecode body of
             Left _    -> [("json", Null)]
             Right val -> [("json", val)]

put = post

delete = respond return

status = do
  val <- (fromMaybe 200 . rqIntParam "val") <$> getRequest
  let code | val >= 200 && val <= 505 = val
           | otherwise                = 400
  modifyResponse $ setResponseCode code

gzip =
  localRequest (setHeader "Accept-Encoding" "gzip") . withCompression .
  respond $ \obj -> return $ obj <> [("gzipped", Bool True)]

deleteCookies = do
  req <- getRequest
  let expire name = Cookie name "" (Just mcfly) Nothing (Just "/") False False
      mcfly = UTCTime (read "1985-10-26") 4800
  modifyResponse . foldr (.) id $ [
      addResponseCookie (expire name) . deleteResponseCookie name
      | name <- Map.keys (rqQueryParams req)
    ]
  redirect "/cookies"

setCookies = do
  params <- rqQueryParams <$> getRequest
  modifyResponse . foldr (.) id . map addResponseCookie $
    [Cookie k v Nothing Nothing (Just "/") False False
     | (k,vs) <- Map.toList params, v <- vs]
  redirect "/cookies"

listCookies = do
  cks <- rqCookies <$> getRequest
  let cs = [(decodeUtf8 (cookieName c),
             toJSON (decodeUtf8 (cookieValue c))) | c <- cks]
  respond $ \obj -> return $ obj <> [("cookies", object cs)]

redirect_ = do
  req <- getRequest
  let n   = fromMaybe (-1::Int) . rqIntParam "n" $ req
      prefix = B.reverse . B.dropWhile (/='/') . B.reverse . rqURI $ req
  case undefined of
    _| n > 1     -> redirect $ prefix <> pack (show (n-1))
     | n == 1    -> redirect "/get"
     | otherwise -> modifyResponse $ setResponseCode 400

unauthorized = modifyResponse $
               setHeader "WWW-Authenticate" "Basic realm=\"Fake Realm\"" .
               setResponseCode 401

simpleAuth expect = do
  req <- getRequest
  case expect req of
    Nothing -> modifyResponse $ setResponseCode 400
    Just (expected, resp) ->
      case getHeader "Authorization" (headers req) of
        Nothing -> unauthorized
        Just auth | auth == expected -> writeJSON $
                                        resp <> [("authenticated", Bool True)]
                  | otherwise -> unauthorized

basicAuth = simpleAuth $ \req ->
  case (rqParam "user" req, rqParam "pass" req) of
    (Just [user], Just [passwd]) | not (':' `B.elem` user) ->
      Just ("Basic " <> B64.encode (user <> ":" <> passwd),
            [("user", toJSON (B.unpack user))])
    _ -> Nothing

oauth2token = simpleAuth $ \req ->
  case (rqParam "kind" req, rqParam "token" req) of
    (Just [kind], Just [token]) ->
      Just (kind <> " " <> token,
            [("token", toJSON (B.unpack token))])
    _ -> Nothing

cache = do
  hdrs <- headers <$> getRequest
  let cond = not . null . catMaybes . map (flip getHeader hdrs) $
             ["If-Modified-Since", "If-None-Match"]
  if cond
    then modifyResponse $ setResponseCode 304
    else do
      now <- liftIO $ formatHttpTime =<< epochTime
      uuid <- liftIO nextRandom
      modifyResponse $ setHeader "Last-Modified" now .
                       setHeader "ETag" (toASCIIBytes uuid)
      respond return

rqIntParam name req =
  case rqParam name req of
    Just (str:_) -> case decimal (decodeUtf8 str) of
                      Right (n, "") -> Just n
                      _             -> Nothing
    _            -> Nothing

writeJSON obj = do
  modifyResponse $ setContentType "application/json"
  writeLBS . (<> "\n") . encodePretty' defConfig { confIndent = Spaces 2, confCompare = compare } . object $ obj

respond act = do
  req <- getRequest
  let step m k v = Map.insert (decodeUtf8 k) (decodeUtf8 (head v)) m
      params = Map.foldlWithKey' step Map.empty .
               rqQueryParams $ req
      wibble (k,v) = (decodeUtf8 (original k), decodeUtf8 v)
      rqHdrs = headers req
      hdrs = Map.fromList . map wibble . listHeaders $ rqHdrs
      url = case getHeader "Host" rqHdrs of
              Nothing   -> []
              Just host -> [("url", toJSON . decodeUtf8 $
                                    "http://" <> host <> rqURI req)]
  writeJSON =<< act ([ ("args", toJSON params)
                     , ("headers", toJSON hdrs)
                     , ("origin", toJSON . decodeUtf8 . rqClientAddr $ req)
                     ] <> url)

meths ms h = methods ms (path "" h)
meth m h   = method m (path "" h)

serve mkConfig = do
  cfg <- mkConfig
       . setAccessLog ConfigNoLog
       . setErrorLog ConfigNoLog
       $ defaultConfig
  httpServe cfg $ route [
      ("/get", meths [GET,HEAD] get)
    , ("/post", meth POST post)
    , ("/put", meth PUT put)
    , ("/delete", meth DELETE delete)
    , ("/redirect/:n", redirect_)
    , ("/status/:val", status)
    , ("/gzip", meths [GET,HEAD] gzip)
    , ("/cookies/delete", meths [GET,HEAD] deleteCookies)
    , ("/cookies/set", meths [GET,HEAD] setCookies)
    , ("/cookies", meths [GET,HEAD] listCookies)
    , ("/basic-auth/:user/:pass", meths [GET,HEAD] basicAuth)
    , ("/oauth2/:kind/:token", meths [GET,HEAD] oauth2token)
    , ("/cache", meths [GET,HEAD] cache)
    ]
