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

module Network.HTTP.Client.Overrides.Internal.Types
    ( ConfigFile(..)
    , Config(..)
    , LogOptions(..)
    , LogFormat(..)
    , RequestOverride(..)
    , URL(..)
    , parseURL
    ) where

import Data.Either (either)
import Data.List (intercalate)
import Data.Char (toLower)
import GHC.Generics
import Text.Read (readMaybe)

import Data.Aeson (FromJSON, Value(..), (.:), (.:?), (.!=), parseJSON, withObject, withText)
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Char8 as BS
import Data.Text (Text)
import qualified Data.Text as T
import Network.URI (URI)
import qualified Network.URI as URI

--------
-- Types
--------

-- | The configuration file is versioned so that it can be changed in the
-- future and the old format gradually deprecated. The parsers uses the
-- @version@ field to decide how to parse the file.
data ConfigFile
    = V1 Config
    deriving (Show, Eq, Generic)

instance FromJSON ConfigFile where
    parseJSON = withObject "ConfigFile" $ \o -> do
        version <- o .: "version"
        case version of
            "v1" -> V1 <$> parseJSON (Object o)
            _    -> fail $ "Version '" <> version <> "' not supported"

-- | This type is used when the 'ConfigFile' version is @v1@.
data Config = Config
    { logOptions       :: !LogOptions
    -- ^ Options for logging HTTP requests and responses
    , requestOverrides :: ![RequestOverride]
    -- ^ Overrides to apply to HTTP requests
    } deriving (Show, Eq, Generic)

instance FromJSON Config where
    parseJSON = withObject "Config" $ \o -> Config
        <$> o .:? "logOptions" .!= LogOptions Nothing Nothing Nothing
        <*> o .:? "requestOverrides" .!= []

data LogOptions = LogOptions
    { logResponses        :: !(Maybe LogFormat)
    -- ^ Log HTTP responses
    , logRequests         :: !(Maybe LogFormat)
    -- ^ Log HTTP requests
    , logRequestOverrides :: !(Maybe LogFormat)
    -- ^ Log any overridden HTTP requests
    } deriving (Show, Eq, Generic)

instance FromJSON LogOptions where
    parseJSON = Aeson.genericParseJSON opts
      where
        opts = Aeson.defaultOptions { Aeson.fieldLabelModifier = toLowerHead . drop 3 }
        toLowerHead [] = []
        toLowerHead (x:xs) = (toLower x):xs

data LogFormat
    = Simple
    -- ^ Simple log format (single-line)
    | Detailed
    -- ^ Detailed log format
    deriving (Show, Eq, Generic)

instance FromJSON LogFormat where
    parseJSON = withText "LogFormat" $ \t -> case T.toLower t of
        "simple"   -> pure $ Simple
        "detailed" -> pure $ Detailed
        _          -> fail $ "Unknown LogFormat: " <> T.unpack t

data RequestOverride = RequestOverride
    { match    :: !URL
    -- ^ Match HTTP requests according to this URL
    , override :: !URL
    -- ^ Override HTTP reuests using this URL
    } deriving (Eq, Generic)

instance Show RequestOverride where
    show x = intercalate "\n" $
        [ "RequestOverride {"
        , "  match ="
        ]
        ++
        (map ("    " ++) . lines . show $ match x)
        ++
        [ "  override ="
        ]
        ++
        (map ("    " ++) . lines . show $ override x)
        ++
        [ "}"
        ]

instance FromJSON RequestOverride

data URL = URL
    { secure      :: !(Maybe Bool)
    , host        :: !(Maybe BS.ByteString)
    , port        :: !(Maybe Int)
    , path        :: !(Maybe BS.ByteString)
    } deriving (Eq, Generic)

instance Show URL where
    show x = intercalate "\n" $
        [ "URL {"
        , "  secure = " ++ show (secure x)
        , "  host   = " ++ show (host x)
        , "  port   = " ++ show (port x)
        , "  path   = " ++ show (path x)
        , "}"
        ]

instance FromJSON URL where
    parseJSON = withText "URL" $ either fail return . parseURL

--------------
-- URL parsing
--------------

parseURL :: Text -> Either String URL
parseURL url
    | "http://"  `T.isPrefixOf` url = toURL (Just False) url
    | "https://" `T.isPrefixOf` url = toURL (Just True) url
    | "://" `T.isInfixOf` url = Left $ "Unspported scheme: " ++ T.unpack url
    | otherwise = toURL Nothing $ "https://" <> url

toURL :: Maybe Bool -> Text -> Either String URL
toURL https url = do
    uri <- case URI.parseURIReference $ T.unpack url of
        Nothing -> Left $ "Failed to parse URL: " ++ T.unpack url
        Just x  -> Right x
    p <- getPort uri
    return $ URL https (getHost uri) p (getPath uri)

getHost :: URI -> Maybe BS.ByteString
getHost uri = do
    authority <- URI.uriAuthority uri
    case URI.uriRegName authority of
        "" -> Nothing
        x  -> Just $ BS.pack x

getPort :: URI -> Either String (Maybe Int)
getPort uri =
    case fmap URI.uriPort $ URI.uriAuthority uri of
        Just "" ->
            Right Nothing
        Just ":" ->
            Right Nothing
        Just (':':x) ->
            case readMaybe x of
                Nothing -> Left $ "Failed to parse Int: " ++ show x
                Just p  -> Right $ Just p
        _ ->
            Right Nothing

getPath :: URI -> Maybe BS.ByteString
getPath uri = case URI.uriPath uri of
    "" -> Nothing
    x  -> Just $ BS.pack x