{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE TypeOperators       #-}

-- |
-- Module      : Authenticator.Actions
-- Description : Simple CLI actions for 'Vault's.
-- Copyright   : (c) Justin Le 2017
-- License     : MIT
-- Maintainer  : justin@jle.im
-- Stability   : unstable
-- Portability : portable
--
-- Basic actions to manipulate 'Vault's.  The @otp-auth@ executable is
-- a thin wrapper over these actions.
--
-- See 'Cmd'.
--


module Authenticator.Actions (
    viewVault
  , addSecret
  , genSecret
  , editSecret
  , deleteSecret
  ) where

import           Authenticator.Common
import           Authenticator.Vault
import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Class
import           Control.Monad.Trans.Maybe
import           Control.Monad.Trans.State
import           Control.Monad.Trans.Writer
import           Data.Char
import           Data.Dependent.Sum
import           Data.Foldable
import           Data.Functor
import           Data.Maybe
import           Data.Monoid
import           Data.Singletons
import           Data.String
import           Data.Type.Conjunction
import           Data.Witherable
import           Lens.Micro
import           Options.Applicative
import           Prelude hiding             (filter)
import           System.Exit
import           Text.Printf
import           Text.Read                  (readMaybe)
import qualified Data.Text                  as T
import qualified System.Console.Haskeline   as L

-- | View secrets, generating codes for time-based keys.
viewVault
    :: Bool                                     -- ^ List key names only; do not generate any codes.
    -> Either Int (Maybe T.Text, Maybe T.Text)  -- ^ Filter by ID or possibly by account name and issuer
    -> Vault
    -> IO ()
viewVault l filts vt = do
    (n,found) <- runWriterT . flip execStateT 1 $ vaultSecrets (\(sc :: Secret m) ms -> do
        i <- state $ \x -> (x :: Int, x + 1)
        fmap (fromMaybe ms) . runMaybeT $ do
          case filts of
            Left n -> guard (i == n)
            Right (fAcc, fIss) -> do
              traverse_ (guard . (== secAccount sc)) fAcc
              traverse_ (guard . (== secIssuer sc) . Just) fIss
          lift . lift $ tell (Any True)
          liftIO $ if l
            then printf "(%d) %s\n" i (describeSecret sc) $> ms
            else case sing @_ @m of
              SHOTP -> ms <$
                printf "(%d) %s: [ counter-based, use gen ]\n" i (describeSecret sc)
              STOTP -> do
                p <- totp sc
                printf "(%d) %s: %s\n" i (describeSecret sc) p
                return ms
      ) vt
    printf "Searched %d total entries.\n" (n - 1)
    unless (getAny found) $ case filts of
      Left i   -> printf "ID %d not found!\n" i *> exitFailure
      Right _  -> putStrLn "No matches found!"

-- | Add a secret, interactively.
addSecret
    :: Bool         -- ^ Echo back password entry
    -> Bool         -- ^ If 'True', add via otpauth protocol URI
    -> Vault
    -> IO Vault
addSecret echoPass u vt = do
    -- TODO: verify b32?
    dsc <- if u
      then do
        q <- L.runInputT hlSettings $
          fromMaybe "" <$> if echoPass
                             then L.getInputLine "URI Secret?: "
                             else L.getPassword (Just '*') "URI Secret?: "
        case parseSecretURI q of
          Left err -> do
            putStrLn "Parse error:"
            putStrLn err
            exitFailure
          Right d ->
            return d
      else mkSecret echoPass
    putStrLn "Added succesfully!"
    return $
      vt & _Vault %~ (++ [dsc])

-- | Generate a secret code, forcing a new HOTP code if it is
-- counter-based.
genSecret
    :: Int      -- ^ ID # of secret to generate
    -> Vault
    -> IO (Maybe (String, Vault))
genSecret n vt = do
    res <- runMaybeT . runWriterT . forOf (_Vault . ix (n - 1)) vt $ \case
      s :=> sc :&: ms -> 
        case s of
          SHOTP -> do
            let (p, ms') = hotp sc ms
                out = printf "(%d) %s: %s\n" n (describeSecret sc) p
            tell (First (Just out))
            return $ s :=> sc :&: ms'
          STOTP -> do
            liftIO $ do
              p <- totp sc
              printf "(%d) %s: %s\n" n (describeSecret sc) p
            empty
    forM res $ \(r, changed) ->
      case getFirst changed of
        Just msg -> return (msg, r)
        Nothing -> do
          printf "No item with ID %d found.\n" n
          exitFailure

-- | Edit a secret's metadata (account name, issuer)
editSecret
    :: Int      -- ^ ID # of secret to edit
    -> Vault
    -> IO Vault
editSecret n vt = do
    (vt', found) <- runWriterT . forOf (_Vault . ix (n - 1)) vt $ \case
      (s :=> sc :&: ms) -> do
        sc' <- liftIO $ do
          printf "Editing (%d) %s ...\n" n (describeSecret sc)
          liftIO (mkSecretFrom sc)
        tell (First (Just (describeSecret sc')))
        return $ s :=> sc' :&: ms
    case getFirst found of
      Nothing -> do
        printf "No item with ID %d found.\n" n
        exitFailure
      Just desc -> do
        printf "%s edited successfuly!\n" desc
        return vt'

-- | Delete a secret.
deleteSecret
    :: Int          -- ^ ID # of secret to delete
    -> Vault
    -> IO Vault
deleteSecret n vt = do
    (vt', found) <- runWriterT . flip evalStateT 1 . forOf (_Vault . wither) vt $ \case
      ds@(_ :=> sc :&: _) -> do
        i <- state $ \x -> (x :: Int, x + 1)
        if n == i
          then do
            lift $ tell (Any True)
            a <- liftIO . L.runInputT hlSettings $
                L.getInputChar (printf "Delete %s? y/[n]: " (describeSecret sc))
            case toLower <$> a of
              Just 'y' -> do
                liftIO $ putStrLn "Deleted!"
                return Nothing
              _     -> return (Just ds)
          else return (Just ds)
    unless (getAny found) $ do
      printf "No item with ID %d found.\n" n
      exitFailure
    return vt'

mkSecret :: Bool -> IO SomeSecretState
mkSecret echoPass = L.runInputT hlSettings $ do
    a <- (mfilter (not . null) <$> L.getInputLine "Account?: ") >>= \case
      Nothing -> liftIO $ putStrLn "Account required" >> exitFailure
      Just r  -> return r
    i <- L.getInputLine "Issuer? (optional): "
    k <- fromMaybe "" <$> if echoPass
                            then L.getInputLine "Secret?: "
                            else L.getPassword (Just '*') "Secret?: "
    m <- L.getInputChar "[t]ime- or (c)ounter-based?: "
    let i' = mfilter (not . null) i
        k' = decodePad . T.pack $ k
        s  = Sec (T.pack a) (T.pack <$> i') HASHA1 6 <$> k'
    case toLower <$> m of
      Just 'c' -> do
        n <- mfilter (not . null) <$> L.getInputLine "Initial counter? [0]: "
        n' <- case n of
          Nothing -> return 0
          Just n' -> case readMaybe n' of
            Just r -> return r
            Nothing -> liftIO $ putStrLn "Invalid initial counter.  Using 0." $> 0
        case s of
          Nothing -> liftIO $ do
            printf "Invalid secret key: %s\n" k
            exitFailure
          Just s' -> return $ SHOTP :=> s' :&: HOTPState n'
      _ -> do
        case s of
          Nothing -> liftIO $ do
            printf "Invalid secret key: %s\n" k
            exitFailure
          Just s' -> return $ STOTP :=> s' :&: TOTPState

mkSecretFrom :: Secret m -> IO (Secret m)
mkSecretFrom sc = L.runInputT hlSettings $ do
    a <- mfilter (not . null) <$> L.getInputLineWithInitial "Account?: " (T.unpack (secAccount sc), "")
    i <- mfilter (not . null) <$> case secIssuer sc of
           Nothing -> L.getInputLine "Issuer? (optional): "
           Just si -> L.getInputLineWithInitial "Issuer? (optional): " (T.unpack si, "")
    let a' = case a of
               Nothing -> secAccount sc
               Just r  -> T.pack r
        i' = case i of
               Nothing -> secIssuer sc
               Just r  -> Just $ T.pack r
    return $ sc { secAccount = a'
                , secIssuer  = i'
                }