{- Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

module CmdLine where

import Types
import Types.Storage
import Types.Server (HostName, Port)
import Types.Cost (Seconds(..))
import Storage.Local
import Storage.Network
import Tunables
import qualified Gpg
import Options.Applicative
import qualified Data.ByteString.UTF8 as BU8
import qualified Data.Text as T
import System.Directory

data CmdLine = CmdLine
	{ mode :: Maybe Mode
	, secretkeysource :: Maybe SecretKeySource
	, localstoragedirectory :: Maybe LocalStorageDirectory
	, gui :: Bool
	, testMode :: Bool
	, customShareParams :: Maybe ShareParams
	, name :: Maybe Name
	, othername :: Maybe Name
	, preferredStorage :: [Maybe LocalStorageDirectory -> IO (Maybe Storage)]
	, serverConfig :: ServerConfig
	, chaffMaxDelay :: Maybe Seconds
	}

data Mode = Backup | Restore | UploadQueued | AutoStart | Server | BackupServer FilePath | RestoreServer FilePath | Chaff HostName | CheckServers | Benchmark | Test
	deriving (Show)

data ServerConfig = ServerConfig
	{ serverPort :: Port
	, serverAddress :: String
	, monthsToFillHalfDisk :: Integer
	, serverMotd :: Maybe T.Text
	}

parse :: Parser CmdLine
parse = CmdLine
	<$> optional parseMode
	<*> optional (gpgswitch <|> fileswitch)
	<*> optional localstoragedirectoryopt
	<*> guiswitch
	<*> testmodeswitch
	<*> optional parseShareParams
	<*> optional nameopt
	<*> optional othernameopt
	<*> many (addstoragedirectory <|> addserver)
	<*> parseServerConfig
	<*> optional chaffmaxdelayopt
  where
	gpgswitch = GpgKey . KeyId . T.pack <$> strOption
		( long "gpgkeyid"
		<> metavar "KEYID"
		<> help "Specify keyid of gpg key to back up or restore. (When this option is used to back up a key, it must also be used at restore time.)"
		)
	fileswitch = KeyFile <$> strOption
		( long "keyfile"
		<> metavar "FILE"
		<> help "Specify secret key file to back up or restore. (The same filename must be used to restore a key as was used to back it up.)"
		)
	localstoragedirectoryopt = LocalStorageDirectory <$> option str
		( long "store-directory"
		<> metavar "DIR"
		<> help "Where to store data locally. For the client, data is stored here before it is uploaded to the server. For the server, this is where it stores its data. (default: ~/.keysafe/objects/)"
		)
	testmodeswitch = switch
		( long "testmode"
		<> help "Avoid using expensive cryptographic operations to secure data. Use for testing only, not with real secret keys."
		)
	guiswitch = switch
		( long "gui"
		<> help "Use GUI interface for interaction. Default is to use readline interface when run in a terminal, and GUI otherwise."
		)
	nameopt = option nameOption
		( long "name"
		<> metavar "N"
		<> help "Specify name used for key backup/restore, avoiding the usual prompt."
		)
	othernameopt = option nameOption
		( long "othername"
		<> metavar "N"
		<> help "Specify other name used for key backup/restore, avoiding the usual prompt."
		)
	chaffmaxdelayopt = option secondsOption
		( long "chaff-max-delay"
		<> metavar "SECONDS"
		<> help "Specify a delay between chaff uploads. Will delay a random amount between 0 and this many seconds."
		)
	addstoragedirectory = (\d _lsd -> localStorageOverride d) 
		<$> strOption
			( long "add-storage-directory"
			<> metavar "DIR"
			<> help "Add the directory to the list of locations keysafe will use for backup/restore of keys. Keysafe will use the directory first, before any of its built-in servers."
			)
	addserver = (\(h, p) lsd -> networkStorageOverride lsd h p) 
		<$> option hostPortOption
			( long "add-server"
			<> metavar "HOST[:PORT]"
			<> help "Add the server to the server list which keysafe will use for backup/restore of keys. Keysafe will use the server first before any of its built-in servers."
			)

parseMode :: Parser Mode
parseMode = 
	flag' Backup
		( long "backup"
		<> help "Store a secret key in keysafe."
		)
	<|> flag' Restore
		( long "restore"
		<> help "Retrieve a secret key from keysafe."
		)
	<|> flag' UploadQueued
		( long "uploadqueued"
		<> help "Upload any data to servers that was queued by a previous --backup run."
		)
	<|> flag' AutoStart
		( long "autostart"
		<> help "Run automatically on login by desktop autostart file."
		)
	<|> flag' Server
		( long "server"
		<> help "Run as a keysafe server, accepting objects and storing them to ~/.keysafe/objects/local/"
		)
	<|> BackupServer <$> strOption
		( long "backup-server"
		<> metavar "BACKUPDIR"
		<> help "Run on a server, populates the directory with a gpg encrypted backup of all objects stored in the --store-directory. This is designed to be rsynced offsite (with --delete) to back up a keysafe server with minimal information leakage."
		)
	<|> RestoreServer <$> strOption
		( long "restore-server"
		<> metavar "BACKUPDIR"
		<> help "Restore all objects present in the gpg-encrypted backups in the specified directory."
		)
	<|> Chaff <$> strOption
		( long "chaff"
		<> metavar "HOSTNAME"
		<> help "Upload random data to a keysafe server."
		)
	<|> flag' CheckServers
		( long "check-servers"
		<> help "Tries to connect to each server in the server list. Displays the server's MOTD, and the amount of data stored on it. Prints message to stderr and exits nonzero if any of the servers are not accessible."
		)
	<|> flag' Benchmark
		( long "benchmark"
		<> help "Benchmark speed of keysafe's cryptographic primitives."
		)
	<|> flag' Test
		( long "test"
		<> help "Run test suite."
		)

parseShareParams :: Parser ShareParams
parseShareParams = ShareParams <$> totalobjects <*> neededobjects
  where
	totalobjects = option auto 
		( long "totalshares"
		<> metavar "M"
		<> help ("Configure the number of shares to split encrypted secret key into. "
			++ showdefault totalObjects ++ neededboth)
		)
	neededobjects = option auto 
		( long "neededshares"
		<> metavar "N"
		<> help ("Configure the number of shares needed to restore. " 
			++ showdefault neededObjects ++ neededboth)
		)
	showdefault f = "(default: " ++ show (f (shareParams defaultTunables)) ++ ")"
	neededboth = " (When this option is used to back up a key, it must also be provided at restore time.)"
	

parseServerConfig :: Parser ServerConfig
parseServerConfig = ServerConfig
	<$> option auto
		( long "port"
		<> metavar "P"
		<> value 4242
		<> showDefault
		<> help "Port for server to listen on."
		)
	<*> option str
		( long "address"
		<> metavar "A"
		<> value "127.0.0.1"
		<> showDefault
		<> help "Address for server to bind to. (Use \"*\" to bind to all addresses.)"
		)
	<*> option auto
		( long "months-to-fill-half-disk"
		<> metavar "N"
		<> value 12
		<> showDefault
		<> help "Server rate-limits requests and requires proof of work, to avoid too many objects being stored. This is an lower bound on how long it could possibly take for half of the current disk space to be filled."
		)
	<*> optional (T.pack <$> strOption 
		( long "motd"
		<> metavar "MESSAGE"
		<> help "The server's Message Of The Day."
		))

get :: IO CmdLine
get = execParser opts
  where
	opts = info (helper <*> parse)
		( fullDesc
		<> header "keysafe - securely back up secret keys"
		)

-- | When a mode is not specified on the command line,
-- default to backing up if a secret key exists, and otherwise restoring.
selectMode :: CmdLine -> IO Mode
selectMode cmdline = case mode cmdline of
	Just m -> return m
	Nothing -> case secretkeysource cmdline of
		Just (KeyFile f) -> present <$> doesFileExist f
		_ -> present . not . null <$> Gpg.listSecretKeys
  where
	present True = Backup
	present False = Restore

customizeShareParams :: CmdLine -> Tunables -> Tunables
customizeShareParams cmdline t = case customShareParams cmdline of
	Nothing -> t
	Just ps -> t { shareParams = ps }

secondsOption :: ReadM Seconds
secondsOption = Seconds . toRational <$> (auto :: ReadM Double)

nameOption :: ReadM Name
nameOption = Name . BU8.fromString <$> auto

hostPortOption :: ReadM (HostName, Port)
hostPortOption = eitherReader $ \s ->
	case break (== ':') s of
		([], []) -> Left "need a hostname"
		(h, ':':ps) -> case reads ps of
			[(p, "")] -> Right (h, p)
			_ -> Left $ "unable to parse port \"" ++ ps ++ "\""
		(h, _) -> Right (h, 4242)
