{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}

-- | A dropbox client
--
module Dropbox
  ( createClient
  , dropboxProxy
  , Dropbox(..)
  , defListFolderRequest
  , FileTag(..)
  , Entry(..)
  , ListFolderResponse(..)
  , ListFolderRequest
  , path
  , LinkResponse(..)
  , LinkRequest(..)
  ) where

import Control.Exception(throwIO)
import Data.Proxy
import Data.Text(Text)
import Network.HTTP.Client.TLS
import Servant.API
import Servant.Client.Generic
import Servant.API.Generic
import Data.Aeson
import Servant.Auth
import Servant.Auth.Client
import Servant.Client
import Web.FormUrlEncoded hiding (fieldLabelModifier)
import Data.Char (isLower)

toSnake :: String -> String
toSnake = camelTo2 '_' . dropWhile isLower

data ListFolderRequest = ListFolderRequest
  { path :: String
  , recursive :: Bool
  , include_media_info :: Bool
  , include_deleted :: Bool
  , include_has_explicit_shared_members :: Bool
  , include_mounted_folders :: Bool
  , include_non_downloadable_files :: Bool
} deriving (Generic, ToJSON)

newtype LinkRequest = LinkRequest
  { linkPath :: String
  } deriving (Generic)

instance ToJSON LinkRequest where
    -- this generates a Value
    toJSON (LinkRequest lpath) =
        object ["path" .= lpath]

defListFolderRequest :: ListFolderRequest
defListFolderRequest = ListFolderRequest
  { path = "" -- root is empty string in dropbox api
  , recursive = False
  , include_media_info = False
  , include_deleted = False
  , include_has_explicit_shared_members = False
  , include_mounted_folders = True
  , include_non_downloadable_files = False
  }

data FileTag = File | Folder
  deriving (Generic, FromJSON, Show, Eq)

data Entry = Entry
  { eTag :: FileTag
  , eName :: Text
  , ePathDisplay :: Text
  , eId :: Text
  } deriving (Generic, Show, Eq)

instance FromJSON Entry where
  parseJSON = withObject "Entry" $ \v -> do
    tag' :: Text <- v .: ".tag"
    let eTag = if tag' == "file" then File else Folder
    eName <- v .: "name"
    ePathDisplay <- v .: "path_display"
    eId <- v .: "id"
    pure $ Entry {..}

newtype ListFolderResponse = ListFolderResponse
  { entries :: [Entry]
  } deriving (Generic, FromJSON, Show, Eq)

-- https://www.dropbox.com/developers/documentation/http/documentation#oauth2-token
data TokenRequest = TokenRequest
  { trAccessToken :: Text
  , trExpiresIn :: Maybe Int -- seconds
  , trTokenType :: Text
  , trRefreshToken :: Maybe Text
  , trScope :: Text
  , trAccountId :: Text
  , trUid :: Text
  } deriving Generic

snakeConstructor :: Options
snakeConstructor = defaultOptions { constructorTagModifier = camelTo2 '_' }

snakeLabel :: Options
snakeLabel = snakeConstructor { fieldLabelModifier = toSnake }

instance FromJSON TokenRequest where
  parseJSON = genericParseJSON snakeLabel

data TokenBody = TokenBody
  { code         :: Maybe Text
  , refresh_token :: Maybe Text
  , grant_type    :: Text
  , redirect_uri  :: Text
  , client_id     :: Text
  , client_secret :: Text
  } deriving (Generic, Show, ToForm)

newtype LinkResponse = LinkResponse {
  link :: Text
  } deriving (Generic, Show, FromJSON)

-- | To use the various endpoints make sure you have the right
-- scope in your 'app' on dropbox: https://www.dropbox.com/developers/apps/info/t282kls5wbrtofs#permissions
-- then regenerate your token (because it's attached to that)
data Dropbox route = Dropbox
  -- TODO: implement list folder
  {
  -- | https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
    _dropbox_list_folder :: route :- "2" :> "files" :> "list_folder" :> Auth '[Bearer] Token :> ReqBody '[JSON] ListFolderRequest :> Post '[JSON] ListFolderResponse
  -- | https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link
  , _dropbox_get_temporary_link :: route :- "2" :> "files" :> "get_temporary_link" :> Auth '[Bearer] Token :> ReqBody '[JSON] LinkRequest :> Post '[JSON] LinkResponse
  , _dropbox_token :: route :- "oauth2" :> "token" :> ReqBody '[FormUrlEncoded] TokenBody :> Post '[JSON] TokenRequest
  } deriving Generic

-- does the generic to type level compute
dropboxProxy :: Proxy (ToServant Dropbox AsApi)
dropboxProxy = genericApi (Proxy @Dropbox)

-- | gives an adhoc client. This throws exceptions
cliRoutes ::  ClientEnv -> Dropbox (AsClientT IO)
cliRoutes env = genericClientHoist
    (\x -> runClientM x env >>= either throwIO return)

createClient :: IO (Dropbox (AsClientT IO))
createClient = do
  baseUri <- parseBaseUrl "https://api.dropboxapi.com/"
  manager <- newTlsManager
  pure $ cliRoutes $ mkClientEnv manager baseUri