module Penny.Brenner.Types
  ( Date(..)
  , IncDec(..)
  , UNumber(..)
  , FitId(..)
  , Payee(..)
  , Desc(..)
  , Amount(unAmount)
  , mkAmount
  , translate
  , DbMap
  , DbList
  , Posting(..)
  , DbLocation(..)
  , FitAcctName(..)
  , FitAcctDesc(..)
  , ParserDesc(..)
  , PennyAcct(..)
  , Translator(..)
  , DefaultAcct(..)
  , Currency(..)
  , FitAcct(..)
  , Config(..)
  , FitFileLocation(..)
  , AllowNew(..)
  , ParserFn
  , Mode
  ) where

import Control.Applicative ((<$>), (<*>))
import qualified Data.Map as M
import qualified Data.Time as Time
import qualified Penny.Lincoln as L
import Data.Text (Text, pack, unpack)
import qualified Data.Text.Encoding as E
import qualified Data.Serialize as S
import qualified Data.Sums as Su
import qualified System.Console.MultiArg as MA

-- | The type of all Brenner MultiArg modes.
type Mode = Maybe FitAcct -> MA.Mode (MA.ProgName -> String) (IO ())

-- | The date reported by the financial institution.
newtype Date = Date { unDate :: Time.Day }
  deriving (Eq, Show, Ord, Read)

instance S.Serialize Date where
  put = S.put . show . unDate
  get = Date <$> (read <$> S.get)

-- | Reports changes in account balances. Avoids using /debit/ and
-- /credit/ as these terms are used differently by the bank than in
-- your ledger (that is, the bank reports it from their perspective,
-- not yours) so instead the terms /increase/ and /decrease/ are
-- used. IncDec is used to record the bank's transactions so
-- /increase/ and /decrease/ are used in the same way you would see
-- them on a bank statement, whether it's a credit card, loan,
-- checking account, etc.
data IncDec
  = Increase
  -- ^ Increases the account balance. For a checking or savings
  -- account, this is a deposit. For a credit card, this is a purchase.

  | Decrease
  -- ^ Decreases the account balance. On a credit card, this is a
  -- payment. On a checking account, this is a withdrawal.
  deriving (Eq, Show, Read)

instance S.Serialize IncDec where
  put x = case x of
    Increase -> S.putWord8 0
    Decrease -> S.putWord8 1
  get = S.getWord8 >>= f
    where
      f x = case x of
        0 -> return Increase
        1 -> return Decrease
        _ -> fail "read IncDec error"

-- | A unique number assigned by Brenner to identify each
-- posting. This is unique within a particular financial institution
-- account only.
newtype UNumber = UNumber { unUNumber :: Integer }
  deriving (Eq, Show, Ord, Read)

instance S.Serialize UNumber where
  put = S.put . unUNumber
  get = UNumber <$> S.get

putText :: Text -> S.Put
putText = S.put . E.encodeUtf8

getText :: S.Get Text
getText = S.get >>= f
  where
    f bs = case E.decodeUtf8' bs of
      Left _ -> fail "text reading failed"
      Right x -> return x


-- | For Brenner to work, the bank has to assign unique identifiers to
-- each transaction that it gives you for download. This is the
-- easiest reliable way to ensure duplicates are not processed
-- multiple times. (There are other ways to accomplish this, but they
-- are much harder and less reliable.) If the bank does not do this,
-- you can't use Brenner.
newtype FitId = FitId { unFitId :: Text }
  deriving (Eq, Show, Ord, Read)

instance S.Serialize FitId where
  put = putText . unFitId
  get = FitId <$> getText

-- | Some financial institutions assign a separate Payee in addition
-- to a description. Others just have a single Description field. If
-- this institution uses both, put something here. Brenner will prefer
-- the Payee if it is not zero length; then it will use the Desc.
newtype Payee = Payee { unPayee :: Text }
  deriving (Eq, Show, Ord, Read)

instance S.Serialize Payee where
  put = putText . unPayee
  get = Payee <$> getText

-- | The transaction description. Some institutions assign only a
-- description (sometimes muddling a payee with long codes, some
-- dates, etc). Brenner prefers the Payee if there is one, and uses a
-- Desc otherwise.
newtype Desc =
  Desc { unDesc :: Text }
  deriving (Eq, Show, Ord, Read)

instance S.Serialize Desc where
  put = putText . unDesc
  get = Desc <$> getText

-- | The amount of the transaction. Do not include any leading plus or
-- minus signs; this should be only digits and a decimal point.
newtype Amount = Amount { unAmount :: Text }
  deriving (Eq, Show, Ord, Read)

instance S.Serialize Amount where
  put = putText . unAmount
  get = getText >>= f
    where
      f x = case mkAmount . unpack $ x of
        Nothing -> fail $ "failed to load amount: " ++ unpack x
        Just a -> return a

-- | Ensures that incoming Amounts have only digits and (up to) one
-- decimal point.
mkAmount :: String -> Maybe Amount
mkAmount s =
  let isDigit c = c >= '0' && c <= '9'
      (_, rs) = span isDigit s
  in case rs of
      "" -> if not . null $ s
            then return . Amount . pack $ s
            else Nothing
      '.':rest -> if all isDigit rest
                  then return . Amount . pack $ s
                  else Nothing
      _ -> Nothing

translate
  :: IncDec
  -> Translator
  -> L.DrCr
translate Increase IncreaseIsDebit = L.Debit
translate Increase IncreaseIsCredit = L.Credit
translate Decrease IncreaseIsDebit = L.Credit
translate Decrease IncreaseIsCredit = L.Debit

type DbMap = M.Map UNumber Posting
type DbList = [(UNumber, Posting)]

data Posting = Posting
  { date :: Date
  , desc :: Desc
  , incDec :: IncDec
  , amount :: Amount
  , payee :: Payee
  , fitId :: FitId
  } deriving (Read, Show)


instance S.Serialize Posting where
  put x = S.put (date x)
          >> S.put (desc x)
          >> S.put (incDec x)
          >> S.put (amount x)
          >> S.put (payee x)
          >> S.put (fitId x)
  get = Posting
        <$> S.get
        <*> S.get
        <*> S.get
        <*> S.get
        <*> S.get
        <*> S.get

-- | Where is the database of postings?
newtype DbLocation = DbLocation { unDbLocation :: Text }
  deriving (Eq, Show)

instance L.HasText DbLocation where text = unDbLocation

-- | Text description of the financial institution account.
newtype FitAcctDesc = FitAcctDesc { unFitAcctDesc :: Text }
  deriving (Eq, Show)

instance L.HasText FitAcctDesc where text = unFitAcctDesc

-- | Text description of the parser itself.
newtype ParserDesc = ParserDesc { unParserDesc :: Text }
  deriving (Eq, Show)

instance L.HasText ParserDesc where text = unParserDesc

-- | A name used to refer to a batch of settings.
newtype FitAcctName = FitAcctName { unFitAcctName :: Text }
  deriving (Eq, Show)

instance L.HasText FitAcctName where text = unFitAcctName

-- | The Penny account holding postings for this financial
-- institution. For instance it might be @Assets:Checking@ if this is
-- your checking account, @Liabilities:Credit Card@, or whatever.
newtype PennyAcct = PennyAcct { unPennyAcct :: L.Account }
  deriving (Eq, Show)

instance L.HasTextList PennyAcct where
  textList = L.textList . unPennyAcct

-- | What the financial institution shows as an increase or decrease
-- has to be recorded as a debit or credit in the PennyAcct.
data Translator
  = IncreaseIsDebit
  -- ^ That is, when the financial institution shows a posting that
  -- increases your account balance, you record a debit. You will
  -- probably use this for deposit accounts, like checking and
  -- savings. These are asset accounts so if the balance goes up you
  -- record a debit in your ledger.

  | IncreaseIsCredit
  -- ^ That is, when the financial institution shows a posting that
  -- increases your account balance, you record a credit. You will
  -- probably use this for liabilities, such as credit cards and other
  -- loans.

  deriving (Eq, Show)

-- | The default account to place unclassified postings in. For
-- instance @Expenses:Unclassified@.
newtype DefaultAcct = DefaultAcct { unDefaultAcct :: L.Account }
  deriving (Eq, Show)

instance L.HasTextList DefaultAcct where
  textList = L.textList . unDefaultAcct

-- | The currency for all transactions, e.g. @$@.
newtype Currency = Currency { unCurrency :: L.Commodity }
  deriving (Eq, Show)

instance L.HasText Currency where text = L.text . unCurrency

-- | A batch of settings representing a single financial institution
-- account.
data FitAcct = FitAcct
  { fitAcctName :: FitAcctName
  , fitAcctDesc :: FitAcctDesc
  , dbLocation :: DbLocation
  , pennyAcct :: PennyAcct
  , defaultAcct :: DefaultAcct
  , currency :: Currency

  , qtySpec :: Su.S3 L.Radix L.PeriodGrp L.CommaGrp
  -- ^ How to turn Qty into QtyRep.

  , translator :: Translator

  , side :: L.Side
  -- ^ When creating new transactions, the commodity will be on this
  -- side

  , spaceBetween :: L.SpaceBetween
  -- ^ When creating new transactions, is there a space between the
  -- commodity and the quantity

  , parser :: ( ParserDesc
              , FitFileLocation -> IO (Either String [Posting]))
  -- ^ Parses a file of transactions from the financial
  -- institution. The function must open the file and parse it. This
  -- is in the IO monad not only because the function must open the
  -- file itself, but also so the function can perform arbitrary IO
  -- (run pdftotext, maybe?) If there is failure, the function can
  -- return an Exceptional String, which is the error
  -- message. Alternatively the function can raise an exception in the
  -- IO monad (currently Brenner makes no attempt to catch these) so
  -- if any of the IO functions throw you can simply not handle the
  -- exceptions.
  --
  -- The first element of the pair gives information about the parser.

  , toLincolnPayee :: Desc -> Payee -> L.Payee
  -- ^ Sometimes the financial institution provides Payee information,
  -- sometimes it does not. Sometimes the Desc might have additional
  -- information that you might want to remove. This function can be
  -- used to do that. The resulting Lincoln Payee is used for any
  -- transactions that are created by the merge command. The resulting
  -- payee is also used when comparing new financial institution
  -- postings to already existing ledger transactions in order to
  -- guess at which payee and accounts to create in the transactions
  -- created by the merge command.

  }

-- | Configuration for the Brenner program. You can optionally have
-- a default FitAcct, which is used if you do not specify any FitAcct on the
-- command line. You can also name any number of additional FitAccts. If
-- you do not specify a default FitAcct, you must specify a FitAcct on the
-- command line.

data Config = Config
  { defaultFitAcct :: Maybe FitAcct
  , moreFitAccts :: [FitAcct]
  }

newtype FitFileLocation = FitFileLocation { unFitFileLocation :: String }
  deriving (Show, Eq)

newtype AllowNew = AllowNew { unAllowNew :: Bool }
  deriving (Show, Eq)

-- | All parsers must be of this type.
type ParserFn
  = FitFileLocation
  -> IO (Either String [Posting])