{-# LANGUAGE CPP, FlexibleContexts, OverloadedStrings, QuasiQuotes, RecordWildCards, TypeFamilies #-}
-- | Add form data & handler. (The layout and js are defined in
-- Foundation so that the add form can be in the default layout for
-- all views.)

module Handler.AddForm where

import Import

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
import Control.Monad.State.Strict (evalStateT)
import Data.Either (lefts,rights)
import Data.List (sort)
import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free
import Data.Text (append, pack, unpack)
import qualified Data.Text as T
import Data.Time.Calendar
import Text.Megaparsec.Compat (digitChar, eof, some, string, runParser, ParseError, MPErr)

import Hledger.Utils
import Hledger.Data
import Hledger.Read
import Hledger.Cli.Commands.Add (appendToJournalFileOrStdout)


-- Part of the data required from the add form.
-- Don't know how to handle the variable posting fields with yesod-form yet.
data AddForm = AddForm
    { addFormDate         :: Day
    , addFormDescription  :: Maybe Text -- String
    -- , addFormPostings     :: [(AccountName, String)]
    , addFormJournalFile  :: Maybe Text -- FilePath
    }
  deriving Show

postAddForm :: Handler Html
postAddForm = do
  let showErrors errs = do
        -- error $ show errs -- XXX uncomment to prevent redirect for debugging
        setMessage [shamlet|
                    Errors:<br>
                    $forall e<-errs
                     \#{e}<br>
                   |]
  -- 1. process the fixed fields with yesod-form

  VD{..} <- getViewData
  let
      validateJournalFile :: Text -> Either FormMessage Text
      validateJournalFile f
        | unpack f `elem` journalFilePaths j = Right f
        | otherwise                          = Left $ MsgInvalidEntry $ pack "the selected journal file \"" `append` f `append` "\"is unknown"

      validateDate :: Text -> Handler (Either FormMessage Day)
      validateDate s = return $
        case fixSmartDateStrEither' today $ T.pack $ strip $ unpack s of
          Right d  -> Right d
          Left _   -> Left $ MsgInvalidEntry $ pack "could not parse date \"" `append` s `append` pack "\":" -- ++ show e)

  formresult <- runInputPostResult $ AddForm
    <$> ireq (checkMMap validateDate (pack . show) textField) "date"
    <*> iopt textField "description"
    <*> iopt (check validateJournalFile textField) "journal"
  
  ok <- case formresult of
    FormMissing      -> showErrors ["there is no form data"::String] >> return False
    FormFailure errs -> showErrors errs >> return False
    FormSuccess dat  -> do
      let AddForm{
             addFormDate       =date
            ,addFormDescription=mdesc
            ,addFormJournalFile=mjournalfile
            } = dat
          desc = maybe "" unpack mdesc
          journalfile = maybe (journalFilePath j) unpack mjournalfile

      -- 2. the fixed fields look good; now process the posting fields adhocly,
      -- getting either errors or a balanced transaction

      (params,_) <- runRequestBody
      let numberedParams s =
            reverse $ dropWhile (T.null . snd) $ reverse $ sort
            [ (n,v) | (k,v) <- params
                    , let en = parsewith (paramnamep s) k :: Either (ParseError Char MPErr) Int
                    , isRight en
                    , let Right n = en
                    ]
            where paramnamep s = do {string s; n <- some digitChar; eof; return (read n :: Int)}
          acctparams = numberedParams "account"
          amtparams  = numberedParams "amount"
          num = length acctparams
          paramErrs | num == 0 = ["at least one posting must be entered"]
                    | map fst acctparams == [1..num] &&
                      map fst amtparams `elem` [[1..num], [1..num-1]] = []
                    | otherwise = ["the posting parameters are malformed"]
          eaccts = map (runParser (accountnamep <* eof) "" . textstrip  . snd) acctparams
          eamts  = map (runParser (evalStateT (amountp <* eof) mempty) "" . textstrip . snd) amtparams
          (accts, acctErrs) = (rights eaccts, map show $ lefts eaccts)
          (amts', amtErrs)  = (rights eamts, map show $ lefts eamts)
          amts | length amts' == num = amts'
               | otherwise           = amts' ++ [missingamt]
          errs = if not (null paramErrs) then paramErrs else (acctErrs ++ amtErrs)
          etxn | not $ null errs = Left errs
               | otherwise = either (\e -> Left [L.head $ lines e]) Right
                              (balanceTransaction Nothing $ nulltransaction {
                                  tdate=date
                                 ,tdescription=T.pack desc
                                 ,tpostings=[nullposting{paccount=acct, pamount=Mixed [amt]} | (acct,amt) <- zip accts amts]
                                 })
      case etxn of
       Left errs -> showErrors errs >> return False
       Right t -> do
        -- 3. all fields look good and form a balanced transaction; append it to the file
        liftIO $ do ensureJournalFileExists journalfile
                    appendToJournalFileOrStdout journalfile $
                      showTransaction $
                      txnTieKnot -- XXX move into balanceTransaction
                      t
        setMessage [shamlet|<span>Transaction added.|]
        return True

  if ok then redirect JournalR else redirect (JournalR, [("add","1")])