{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE LambdaCase        #-}
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}

import           Control.Monad.IO.Class   (liftIO)
import           Control.Monad.Reader
import           Data.Aeson               as A
import qualified Data.ByteString.Lazy     as BSL
import           Data.Maybe               (fromJust)
import           Data.Monoid              ((<>))
import           Data.String              (fromString)
import qualified Data.Vector              as V
import           Database.Redis           hiding (String)
import           Network.HTTP.Types       hiding (noContent204)
import           Network.IPv6DB.Types
import           Network.Wai
import           Network.Wai.Handler.Warp
import           Network.Wai.Logger
import           Options.Applicative      (execParser)
import           Prelude                  hiding (error)
import           System.Log.FastLogger
import           Text.IPv6Addr

import           Options
import           Queries
import           Types

main :: IO ()
main = do
  Options{..} <- execParser opts
  timeCache <- newTimeCache simpleTimeFormat
  apLog <- apacheLogger <$>
             initLogger
               FromSocket
               (LogFileNoRotate logFile defaultBufSize)
               timeCache
  runSettings
    (setPort appPort $ setHost (fromString appHost) defaultSettings)
    (ipv6db apLog)

ipv6db :: ApacheLogger -> Application
ipv6db logger req res = do
  Options{..} <- execParser opts
  conn <- checkedConnect $
    defaultConnectInfo
      { connectHost     = redisHost
      , connectPort     = PortNumber (fromInteger redisPort)
      , connectAuth     = redisAuth
      , connectDatabase = redisDatabase }
  withEnv Env { redisConn = conn } $
    case parseMethod (requestMethod req) of
      Right mtd ->
        case pathInfo req of

          ["ipv6db", "v1", "batch"] ->
            batchHandler mtd

          ["ipv6db", "v1", "list", list, "addresses"] ->
            listHandler mtd list

          ["ipv6db", "v1", "list", list, "addresses", addr] ->
            listAddressHandler mtd list addr

          _ -> liftIO (jsonError "Bad URI Request")
      Left _ -> liftIO (jsonError "Bad HTTP Method")
    where

      withEnv = flip runReaderT

      maybeJSONBody :: FromJSON a => IO (Maybe a)
      maybeJSONBody = A.decode <$> strictRequestBody req

      logWith status = liftIO (logger req status Nothing)

      -- -------------------------------------------------------------------- --
      -- Endpoint handlers                                                    --
      -- -------------------------------------------------------------------- --

      batchHandler mtd = do
        env@Env{..} <- ask
        liftIO $ case mtd of

          mtd' | mtd' == PUT || mtd' == POST ->
            maybeJSONBody >>= maybe badJSONRequest answer
            where
              answer (Resources rsrcs) = do
                  results <- mapM (setSource redisConn mtd') rsrcs
                  if all (== RedisOk) results
                    then noContent204
                    else jsonRes400 (encode results)

          GET -> maybeJSONBody >>= maybe badJSONRequest answer
                 where
                   answer ents =
                     runRedis redisConn (getByEntries ents) >>= \case
                       Right srcs -> withEnv env (fromEntries ents srcs) >>= jsonOk
                       Left _  -> jsonError "Backend Error"

          DELETE -> maybeJSONBody >>= maybe badJSONRequest answer
                    where
                      answer ents =
                        runRedis redisConn (delByEntries ents) >>= \case
                          Right d ->
                            case d of
                              0 -> jsonRes404 (justError "No Deletion Performed")
                              _ -> noContent204
                          Left _  -> jsonError "Backend Error"

          _      -> methodNotAllowed

      listHandler mtd list = do
        env@Env{..} <- ask
        liftIO $ case mtd of

          mtd' | mtd' == PUT || mtd' == POST ->
            maybeJSONBody >>= maybe badJSONRequest answer
            where
              answer (Array v) = do
                let rsrcs =
                      (\o -> maybeResource o [("list",String list)]) <$> V.toList v
                if Nothing `notElem` rsrcs
                  then do
                    results <- mapM (setSource redisConn mtd' . fromJust) rsrcs
                    if all (== RedisOk) results
                      then noContent204
                      else jsonRes400 (encode $ filter (/= RedisOk) results)
                  else badJSONRequest

          GET -> maybeJSONBody >>= maybe badJSONRequest answer
                 where
                   answer addrs =
                     runRedis redisConn (getByAddresses list addrs) >>= \case
                       Right msrcs ->
                         withEnv env (fromAddresses list addrs msrcs) >>= jsonOk
                       Left  _     -> jsonServerError "Backend Error"

          DELETE ->
            maybeJSONBody >>= maybe badJSONRequest answer
            where
              answer addrs =
                runRedis redisConn (delByAddresses list addrs) >>= \case
                  Right d ->
                    case d of
                      0 -> jsonRes404 (justError "Resource To Delete Not Found")
                      _ -> noContent204
                  Left _  -> jsonServerError "Backend Error"

          _      -> methodNotAllowed

      listAddressHandler mtd list addr = do
        Env{..} <- ask
        liftIO $ case mtd of

          mtd' | mtd' == PUT || mtd' == POST ->
            maybeJSONBody >>= maybe badJSONRequest answer
            where
              answer o =
                case maybeResource o [("list", String list),("address", String addr)] of
                  Just rsrc ->
                    setSource redisConn mtd' rsrc >>= \case
                      RedisOk -> noContent204
                      error   -> jsonRes400 (encode error)
                  Nothing   -> jsonRes400 (justError "Bad JSON Request")

          GET ->
            case maybeIPv6Addr addr of
              Just (IPv6Addr addr') ->
                liftIO (runRedis redisConn $ getSource list addr') >>= \case
                  Right msrc ->
                    case msrc of
                      Just src -> do
                        ttls <- ttlSource redisConn list addr'
                        case toResource list addr' ttls src of
                          Just rsrc -> jsonOk (A.encode rsrc)
                          Nothing   -> jsonError "Can't Build Resource"
                      Nothing  ->
                        jsonRes404 $
                          encode $
                            ResourceError
                              list
                              (IPv6Addr addr')
                              "Resource Not Found"
                  Left _     -> jsonError "Backend Error"
              Nothing -> jsonError "Not IPv6 Address in URI"

          DELETE ->
            case maybeIPv6Addr addr of
              Just (IPv6Addr addr') ->
                liftIO (runRedis redisConn $ delSource list addr') >>= \case
                  Right i ->
                    case i of
                      1 -> noContent204
                      _ ->
                        jsonRes404 $
                          encode $
                            ResourceError
                              list
                              (IPv6Addr addr')
                              "The Resource Doesn't Exist"
                  Left _ -> jsonError "Backend Error"
              Nothing -> jsonError "Not an IPv6 Address in URI"

          _      -> methodNotAllowed

      -- -------------------------------------------------------------------- --
      -- JSON Responses                                                       --
      -- -------------------------------------------------------------------- --

      jsonOk bs = logWith status200 >> res (jsonRes status200 bs)

      noContent204 = logWith status204 >> res (responseLBS status204 [] BSL.empty)

      jsonRes400 bs = logWith status400 >> res (jsonRes status400 bs)

      badJSONRequest = logWith status400 >> jsonError "Bad JSON Request"

      jsonError err = logWith status400 >> res (jsonRes status400 $ justError err)

      jsonRes404 bs = logWith status404 >> res (jsonRes status404 bs)

      methodNotAllowed = logWith status405 >> res (jsonRes status405 $ justError "Method Not Allowed")

      jsonServerError err = logWith status500 >> res (jsonRes status500 $ justError err)

      jsonRes status =
        responseLBS
          status
          [ ("Content-Type", "application/json; charset=utf-8") ]

      justError err = "{\"error\":\"" <> err <> "\"}"

