module Prompt (transactionsToLedger) where

import App (App, Env (..), PromptResult (..), printText)
import Config.AppConfig (AppConfig (..))
import Config.Files (getTemplateFile)
import Config.YamlConfig (Fill, Filling (..), LedgerConfig (..))
import Control.Arrow ((>>>))
import Control.Monad (forM_, when)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Trans.Reader (asks)
import Data.Either (partitionEithers)
import Data.Function ((&))
import Data.Map (Map, fromList, insert, toList, (!?))
import Data.Map qualified as Map
import Data.Maybe (fromJust)
import Data.Set (Set, notMember)
import Data.Set qualified as Set
import Data.String (IsString (fromString))
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Format.Heavy (format)
import Data.Text.Lazy qualified as TL
import GHC.Arr (Array, elems)
import Matching.Matching (findMatch)
import Text.Regex.TDFA (AllTextMatches (getAllTextMatches), (=~))
import Transactions (Amount (..), Transaction (..))
import Utils (calculateMd5Value, formatDouble, toLazyTemplateMap)
import Prelude hiding (appendFile, putStrLn, readFile)

{- | A Map of key/value pairs that will be used to fill the template file
We fill these values step by step by:
  * using default values from the config
  * prompting the user for it
  * using automatic filling rules from the config
-}
type TemplateMap = Map Text Text

transactionsToLedger :: [Transaction] -> App ()
transactionsToLedger transactions = do
  config <- asks (.config)
  readFile <- asks (.readFile)
  existingMd5Sums <- getExistingMd5Sums <$> liftIO (readFile config.journalFile)
  template <- liftIO $ getTemplateFile config.configDirectory readFile

  printText "        Controls:"
  printText "            - Ctrl + D or enter 's' to skip an entry"
  printText "            - Ctrl + C to abort"

  forM_ transactions do
    transactionToLedger existingMd5Sums template

transactionToLedger :: Set Text -> Text -> Transaction -> App ()
transactionToLedger existingMd5Sums template transaction = do
  config <- asks (.config)
  appendFile <- asks (.appendFile)
  let templateMapToShow = insertTransaction transaction config.ledgerConfig.defaults
      md5Sum = getMd5 config.ledgerConfig templateMapToShow
  when (md5Sum `notMember` existingMd5Sums) do
    let prompter = case findMatch templateMapToShow config.ledgerConfig.fills of
          Just filling -> getPromptResultForMatchingEntry filling.fill
          Nothing -> getPromptResultForManualEntry

    maybeResult <- prompter templateMapToShow
    case maybeResult of
      Skip -> return ()
      Result templateMapWithUserInputs -> do
        let templateMapFinal =
              templateMapWithUserInputs
                & insert "md5sum" md5Sum
                & insertCreditDebit transaction
                & insertAccountsWithUnderscore
        let renderedTemplate = renderTemplate templateMapFinal template
        liftIO $ appendFile config.journalFile $ "\n\n" <> renderedTemplate

insertTransaction :: Transaction -> TemplateMap -> TemplateMap
insertTransaction transaction =
  insert "date" transaction.date
    >>> insert "amount" (formatDouble transaction.amount.amount)
    >>> insert "currency" transaction.currency
    >>> insert "payee" transaction.payee
    >>> insert "posting" transaction.posting
    >>> insert "purpose" transaction.purpose

insertCreditDebit :: Transaction -> TemplateMap -> TemplateMap
insertCreditDebit transaction =
  insert "debit" (formatDouble transaction.amount.amount)
    >>> insert "credit" (formatDouble $ -transaction.amount.amount)

getMd5 :: LedgerConfig -> TemplateMap -> Text
getMd5 ledgerConfig templateMap = calculateMd5Value md5Values
 where
  md5Keys = map T.pack ledgerConfig.md5
  -- This is validated, so fromJust should not error. TODO: find a way to not use fromJust
  md5Values = map (fromJust . flip Map.lookup templateMap) md5Keys

renderTemplate :: TemplateMap -> Text -> Text
renderTemplate templateMap template =
  TL.toStrict $
    format
      (fromString $ T.unpack template)
      (toLazyTemplateMap templateMap) -- `format` only accepts Maps with lazy text in it

-- This is needed because the library for the template rendering doesn't accept underscores
insertAccountsWithUnderscore :: TemplateMap -> TemplateMap
insertAccountsWithUnderscore templateMap =
  templateMap
    & maybeInsert "creditAccount" creditAccount
    & maybeInsert "debitAccount" debitAccount
 where
  creditAccount = templateMap !? "credit_account"
  debitAccount = templateMap !? "debit_account"
  maybeInsert key maybeValue theMap = maybe theMap (\value -> insert key value theMap) maybeValue

getPromptResultForMatchingEntry :: Fill -> TemplateMap -> App (PromptResult TemplateMap)
getPromptResultForMatchingEntry fill templateMap = do
  let fillAsList = toList fill
      (prompts, fills) =
        fillAsList
          & map toEither
          & partitionEithers
      templateMapWithFills = fromList fills <> templateMap

  printTemplateMap templateMapWithFills

  sleep <- asks (.sleep)
  printText ""
  forM_ fills \(key, value) ->
    printText $ "Set '" <> key <> "' to '" <> value <> "'"
  liftIO sleep

  updateTemplateMapFromPrompts prompts templateMapWithFills
 where
  toEither (key, Just value) = Right (key, value)
  toEither (key, Nothing) = Left key

getPromptResultForManualEntry :: TemplateMap -> App (PromptResult TemplateMap)
getPromptResultForManualEntry templateMap = do
  prompts <- asks (.config.ledgerConfig.prompts)
  printTemplateMap templateMap
  updateTemplateMapFromPrompts prompts templateMap

updateTemplateMapFromPrompts :: [Text] -> TemplateMap -> App (PromptResult TemplateMap)
updateTemplateMapFromPrompts [] templateMap = return $ Result templateMap
updateTemplateMapFromPrompts (prompt : restPrompts) templateMap = do
  promptForEntry <- asks (.promptForEntry)
  maybeResult <- promptForEntry templateMap prompt
  case maybeResult of
    Result result -> updateTemplateMapFromPrompts restPrompts $ insert prompt result templateMap
    Skip -> return Skip

md5Regex :: Text
md5Regex = "; md5sum: ([A-Za-z0-9]{32})"

getExistingMd5Sums :: Text -> Set Text
getExistingMd5Sums textToSearchIn =
  textToSearchIn
    =~ md5Regex
    & getAllTextMatches @(Array Int)
    & elems
    & map (!! 1)
    & Set.fromList

printTemplateMap :: TemplateMap -> App ()
printTemplateMap templateMap = do
  printText "\n"
  -- TODO make these string type safe
  let keysInOrder = ["date", "amount", "currency", "payee", "posting", "purpose", "credit_account", "debit_account"]
  forM_ keysInOrder printIfPresent
 where
  printIfPresent key = case templateMap !? key of
    Just s -> printText $ "    " <> key <> ": " <> s
    Nothing -> return ()
