{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Orion.Client where

import           Network.Wreq as W
import           Network.Wreq.Types
import           Network.HTTP.Client (HttpException)
import           Network.HTTP.Types.Method
import           Network.HTTP.Types
import           Data.Aeson as JSON hiding (Options)
import           Data.Aeson.BetterErrors as AB
import           Data.Aeson.Casing
import           Data.Text  as T hiding (head, tail, find, map, filter, singleton, empty)
import           Data.Text.Encoding as TE
import           Data.Maybe
import           Data.Aeson.BetterErrors.Internal
import           Data.Time
import           Data.Time.ISO8601
import           Data.Foldable as F
import           Data.Monoid
import           Data.Map hiding (lookup, drop)
import qualified Data.HashMap.Strict as H
import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.Lazy as BL
import           Data.String.Conversions
import           Control.Lens hiding ((.=))
import           Control.Monad.Reader
import           Control.Monad.Except (ExceptT, throwError, MonadError, catchError)
import           Control.Exception hiding (try)
import qualified Control.Monad.Catch as C
import           Orion.Types
import           System.Log.Logger
import           GHC.Generics (Generic)
import           Debug.Trace

-- * Entities

getEntities :: Maybe Text -> Orion [Entity]
getEntities mq = do
  let qq = case mq of
       Just q -> [("q", Just $ encodeUtf8 q)]
       Nothing -> []
  let (query :: Query) = qq ++ [("limit", Just $ encodeUtf8 "1000")]
  body <- orionGet (decodeUtf8 $ "/v2/entities" <> (renderQuery True query))
  case eitherDecode body of
    Right ret -> do
      debug $ "Orion success: " ++ (show ret)
      return ret
    Left (e :: String) -> do
      debug $ "Orion parse error: " ++ (show e)
      throwError $ ParseError $ pack (show e)


postEntity :: Entity -> Orion SubId
postEntity e = do
  debug $ convertString $ "Entity: " <> (JSON.encode e)
  res <- orionPost "/v2/entities" (toJSON e)
  return $ SubId $ convertString res

getEntity :: EntityId -> Orion Entity
getEntity (EntityId eid) = do
  body <- orionGet ("/v2/entities/" <> eid)
  case eitherDecode body of
    Right ret -> do
      debug $ "Orion success: " ++ (show ret)
      return ret
    Left (e :: String) -> do
      debug $ "Orion parse error: " ++ (show e)
      throwError $ ParseError $ pack (show e)

deleteEntity :: EntityId -> Orion ()
deleteEntity (EntityId eid) = orionDelete ("/v2/entities/" <> eid)

postAttribute :: EntityId -> (AttributeId, Attribute) -> Orion ()
postAttribute (EntityId eid) (attId, att) = do
  debug $ "Post attribute: " <> (convertString $ JSON.encode att)
  void $ orionPost ("/v2/entities/" <> eid <> "/attrs") (toJSON $ singleton attId att)

postTextAttributeOrion :: EntityId -> AttributeId -> Text -> Orion ()
postTextAttributeOrion (EntityId eid) attId val = do
  debug $ convertString $ "put attribute in Orion: " <> val
  void $ orionPost ("/v2/entities/" <> eid <> "/attrs") (toJSON $ fromList [getSimpleAttr attId val])

deleteAttribute :: EntityId -> AttributeId -> Orion ()
deleteAttribute (EntityId eid) (AttributeId attId) = do
  debug $ "Delete attribute"
  orionDelete ("/v2/entities/" <> eid <> "/attrs/" <> attId)


-- * Subscriptions

getSubs :: Orion [Subscription]
getSubs = do
  debug $ "Get subscriptions"
  body <- orionGet ("/v2/subscriptions/")
  debug $ "Orion body : " ++ (show body)
  case eitherDecode body of
    Right ret -> do
      debug $ "Orion success: " ++ (show ret)
      return ret
    Left (err2 :: String) -> do
      debug $ "Orion parse error: " ++ (show err2)
      throwError $ ParseError $ pack (show err2)

postSub :: Subscription -> Orion SubId
postSub e = do
  debug $ convertString $ "PostSubscription: " <> (JSON.encode e)
  res <- orionPost "/v2/subscriptions" (toJSON e)
  debug $ "Orion resp: " ++ (show res)
  return $ SubId $ convertString res

getSub :: SubId -> Orion Subscription
getSub (SubId eid) = do
  body <- orionGet ("/v2/subscriptions/" <> eid)
  debug $ "Orion success: " ++ (show body)
  case eitherDecode body of
    Right ret -> do
      debug $ "Orion success: " ++ (show ret)
      return ret
    Left (err2 :: String) -> do
      debug $ "Orion parse error: " ++ (show err2)
      throwError $ ParseError $ pack (show err2)

deleteSub :: SubId -> Orion ()
deleteSub (SubId sid) = orionDelete ("/v2/subscriptions/" <> sid)

patchSub :: SubId -> Map Text Text -> Orion ()
patchSub (SubId eid) patch = do
  orionPatch ("/v2/subscriptions/" <> eid) (toJSON patch)

-- * Requests to Orion.

-- Get Orion URI and options
getOrionDetails :: Path -> Orion (String, Options)
getOrionDetails path = do
  orionOpts@(OrionConfig baseUrl service) <- ask
  let opts = defaults &
       header "Fiware-Service" .~ [convertString service] &
       param  "attrs"          .~ ["dateModified,dateCreated,*"] &
       param  "metadata"       .~ ["dateModified,dateCreated,*"]
  let url = (unpack $ baseUrl <> path)
  return (url, opts)

orionGet :: Path -> Orion BL.ByteString
orionGet path = do
  (url, opts) <- getOrionDetails path
  info $ "Issuing ORION GET with url: " ++ (show url)
  debug $ "  headers: " ++ (show $ opts ^. W.headers)
  eRes <- C.try $ liftIO $ W.getWith opts url
  case eRes of
    Right res -> do
      return $ fromJust $ res ^? responseBody
    Left err -> do
      warn $ "Orion HTTP error: " ++ (show err)
      throwError $ HTTPError err

orionPost :: (Postable dat, Show dat) => Path -> dat -> Orion Text
orionPost path dat = do
  (url, opts) <- getOrionDetails path
  info $ "Issuing ORION POST with url: " ++ (show url)
  debug $ "  data: " ++ (show dat)
  debug $ "  headers: " ++ (show $ opts ^. W.headers)
  eRes <- C.try $ liftIO $ W.postWith opts url dat
  debug $ " resp: " ++ (show eRes)
  case eRes of
    Right res -> do
     let headers = fromJust $ res ^? responseHeaders
     return $ T.drop 18 $ convertString $ fromJust $ lookup "Location" headers
    Left err -> do
      warn $ "Orion HTTP Error: " ++ (show err)
      throwError $ HTTPError err

orionDelete :: Path -> Orion ()
orionDelete path = do
  (url, opts) <- getOrionDetails path
  info $ "Issuing ORION DELETE with url: " ++ (show url)
  debug $ "  headers: " ++ (show $ opts ^. W.headers)
  eRes <- C.try $ liftIO $ W.deleteWith opts url
  case eRes of
    Right res -> return ()
    Left err -> do
      warn $ "Orion HTTP Error: " ++ (show err)
      throwError $ HTTPError err

orionPut :: (Putable dat, Show dat) => Path -> dat -> Orion ()
orionPut path dat = do
  (url, opts) <- getOrionDetails path
  info $ "Issuing ORION PUT with url: " ++ (show url)
  debug $ "  data: " ++ (show dat)
  debug $ "  headers: " ++ (show $ opts ^. W.headers)
  eRes <- C.try $ liftIO $ W.putWith opts url dat
  case eRes of
    Right res -> return ()
    Left err -> do
      warn $ "Orion HTTP Error: " ++ (show err)
      throwError $ HTTPError err

orionPatch :: (Postable dat, Show dat) => Path -> dat -> Orion ()
orionPatch path dat = do
  (url, opts) <- getOrionDetails path
  info $ "Issuing ORION PATCH with url: " ++ (show url)
  debug $ "  data: " ++ (show dat)
  debug $ "  headers: " ++ (show $ opts ^. W.headers)
  eRes <- C.try $ liftIO $ W.customPayloadMethodWith "PATCH" opts url dat
  case eRes of
    Right res -> return ()
    Left err -> do
      warn $ "Orion HTTP Error: " ++ (show err)
      throwError $ HTTPError err

-- * Helpers

fromSimpleAttribute :: AttributeId -> Map AttributeId Attribute -> Maybe Text
fromSimpleAttribute attId attrs = do
  (Attribute _ mval _) <- attrs !? attId
  val <- mval
  getString val

fromSimpleMetadata :: MetadataId -> Map MetadataId Metadata -> Maybe Text
fromSimpleMetadata mid mets = do
  (Metadata _ mval) <- mets !? mid
  val <- mval
  getString val

getString :: Value -> Maybe Text
getString (String s) = Just s
getString _ = Nothing

getSimpleAttr :: AttributeId -> Text -> (AttributeId, Attribute)
getSimpleAttr attId val = (attId, Attribute "String" (Just $ toJSON val) empty)

getTextMetadata :: MetadataId -> Text -> (MetadataId, Metadata)
getTextMetadata metId val = (metId, Metadata (Just "String") (Just $ toJSON val))

getTimeMetadata :: MetadataId -> UTCTime -> (MetadataId, Metadata)
getTimeMetadata metId val = (metId, (Metadata (Just "DateTime") (Just $ toJSON $ formatISO8601 val)))

debug, warn, info, err :: (MonadIO m) => String -> m ()
debug s = liftIO $ debugM   "Orion" s
info s  = liftIO $ infoM    "Orion" s
warn s  = liftIO $ warningM "Orion" s
err s   = liftIO $ errorM   "Orion" s

try :: MonadError a m => m b -> m (Either a b)
try act = catchError (Right <$> act) (return . Left)