{-# LANGUAGE QuasiQuotes #-}

{-|

Copyright:

  This file is part of the package openid-connect.  It is subject to
  the license terms in the LICENSE file found in the top-level
  directory of this distribution and at:

    https://code.devalot.com/open/openid-connect

  No part of this package, including this file, may be copied,
  modified, propagated, or distributed except according to the terms
  contained in the LICENSE file.

License: BSD-2-Clause

-}
module Options
  ( Options(..)
  , ClientDetails(..)
  , ForceAuthMethod(..)
  , parseOptions
  ) where

--------------------------------------------------------------------------------
-- Imports:
import Control.Applicative
import Data.Text (Text)
import Network.URI (URI(..), URIAuth(..), parseURI)
import Network.URI.Static (uri)
import OpenID.Connect.Client.Flow.AuthorizationCode (ClientSecret(..))
import qualified Options.Applicative as O

--------------------------------------------------------------------------------
-- | How to get client info.
data ClientDetails
  = Direct
      { optionsClientSecret  :: ClientSecret
      , optionsClientId      :: Text
      }
  | Register
      { optionsClientContact :: Text
      }

--------------------------------------------------------------------------------
-- | Flags to force a certain client authentication method.
data ForceAuthMethod
  = ForceSecretPost
  | ForceSecretJWT
  | ForcePrivateJWT
  | NoForcedAuth

--------------------------------------------------------------------------------
-- | Command line options.
data Options = Options
  { optionsPort            :: Int
  , optionsCert            :: FilePath
  , optionsKey             :: FilePath
  , optionsForceAuthMode   :: ForceAuthMethod
  , optionsProviderUri     :: URI
  , optionsClientUri       :: URI
  , optionsClientDetails   :: ClientDetails
  }

--------------------------------------------------------------------------------
readUri :: O.ReadM URI
readUri = O.eitherReader $ \s ->
  case parseURI s of
    Nothing -> Left "failed to parse URI"
    Just u  -> Right u

--------------------------------------------------------------------------------
clientDetails :: O.Parser ClientDetails
clientDetails = register <|> details
  where
    register =
      Register
        <$> O.strOption (mconcat
              [ O.long "register"
              , O.short 'r'
              , O.metavar "EMAIL"
              , O.help "Use dynamic client registration"
              ])

    details =
      Direct
        <$> clientSecretOption
        <*> O.strOption (mconcat
              [ O.long "client-id"
              , O.short 'i'
              , O.metavar "ID"
              , O.help "Provider-assigned client ID"
              ])

    clientSecretOption :: O.Parser ClientSecret
    clientSecretOption =
      AssignedSecretText
        <$> O.strOption (mconcat
              [ O.long "client-secret"
              , O.short 's'
              , O.metavar "STR"
              , O.help "Provider-assigned secret"
              ])

--------------------------------------------------------------------------------
-- | Parse the 'ForceAuthMethod' type from the command line.
forceAuthMode :: O.Parser ForceAuthMethod
forceAuthMode = forceSecretPost
            <|> forceSecretJWT
            <|> forcePrivateJWT
            <|> pure NoForcedAuth
  where
    forceSecretPost =
      O.flag' ForceSecretPost (mconcat
          [ O.long "force-secret-post"
          , O.help "Authenticate via client_secret_post"
          ])

    forceSecretJWT =
      O.flag' ForceSecretJWT (mconcat
          [ O.long "force-secret-jwt"
          , O.help "Authenticate via client_secret_jwt"
          ])

    forcePrivateJWT =
      O.flag' ForcePrivateJWT (mconcat
          [ O.long "force-private-jwt"
          , O.help "Authenticate via private_key_jwt"
          ])

--------------------------------------------------------------------------------
-- | Command line parser.
options :: O.Parser Options
options =
  Options
    <$> O.option O.auto (mconcat
          [ O.long "port"
          , O.metavar "NUM"
          , O.value 3000
          , O.help "Port number for the sever"
          ])

    <*> O.strOption (mconcat
          [ O.long "cert"
          , O.metavar "FILE"
          , O.value "example/cert.pem"
          , O.help "TLS certificate file"
          ])

    <*> O.strOption (mconcat
          [ O.long "key"
          , O.metavar "FILE"
          , O.value "example/key.pem"
          , O.help "TLS private key file"
          ])

    <*> forceAuthMode

    <*> O.option readUri (mconcat
          [ O.long "provider"
          , O.short 'p'
          , O.metavar "URI"
          , O.help "Provider discovery URI"
          ])

    <*> O.option readUri (mconcat
          [ O.long "client-uri"
          , O.short 'c'
          , O.metavar "URI"
          , O.value [uri|https://localhost:0/return|]
          , O.help "The URI for this server, including /return"
          ])

    <*> clientDetails

--------------------------------------------------------------------------------
-- | Parse the command line.
parseOptions :: IO Options
parseOptions = do
    opts <- O.execParser (O.info
      (options O.<**> O.helper)
      (mconcat [ O.fullDesc
              , O.progDesc "OpenID Connect client example"
              ]))

    pure (fixRedirUri opts)
  where
    -- Update the default client URI so the port number matches the server.
    fixRedirUri :: Options -> Options
    fixRedirUri opts =
      case uriAuthority (optionsClientUri opts) of
        Nothing -> opts
        Just auth ->
          if uriRegName auth == "localhost" && uriPort auth == ":0"
            then opts { optionsClientUri = (optionsClientUri opts)
                          { uriAuthority = Just auth
                              { uriPort = ":" <> show (optionsPort opts)
                              }
                          }
                      }
            else opts