#!/usr/bin/env runhaskell

{-# LANGUAGE OverloadedStrings #-}

-- | Calculates and displays an overview of my finances.
module Main where

import Data.Decimal (Decimal (..), DecimalRaw (..), divide, roundTo)
import Data.Either (fromRight)
import Data.Function ((&))
import qualified Data.List as List
import Data.Text (Text, pack)
import qualified Data.Text as T
import qualified Data.Text.IO as IO
import Data.Time.Calendar (Day, toGregorian)
import Data.Time.Clock (UTCTime (utctDay), getCurrentTime)
import Hledger

data Config = Config
  { age :: Decimal
  }

main = do
  j <- getJournal
  today <- getCurrentTime >>= return . utctDay
  let bal = getTotal j today $ defreportopts
  let balUSD = getTotal j today $ defreportopts {value_ = inUsdNow}
  sec "cash balances"
  row "simple" (prn $ bal "^as:me:cash:simple status:! status:*") Nothing
  row "wallet" (prn $ bal "^as:me:cash:wallet") Nothing
  row "  disc" (prn $ bal "^li:me:cred:discover status:*") Nothing
  row "  citi" (prn $ bal "^li:me:cred:citi status:*") Nothing
  row "   btc" (prn $ bal "^as cur:BTC") (Just $ prn $ balUSD "^as cur:BTC")

  sec "metrics"
  let netLiquid = bal "^as:me:cash ^li:me:cred cur:USD"
  let netWorth = balUSD "^as ^li"
  row "  in - ex" (prn $ bal "^in ^ex cur:USD") $ Just "keep this negative to make progress"
  row "cred load" (prn netLiquid) $ Just "net liquid: credit spending minus cash assets. keep it positive"
  row "net worth" (prn netWorth) Nothing
  row "    level" (pr $ level netWorth) Nothing

  sec "trivials"
  let trivialWorth = roundTo 2 $ trivial * netWorth
  let trivialLiquid = roundTo 2 $ trivial * netLiquid
  row "   net" (pr trivialWorth) Nothing
  row "liquid" (pr trivialLiquid) Nothing

  sec "fire"
  let (thisyear, _, _) = toGregorian today
  let cfg = Config {age = (fromInteger thisyear) - 1992}
  let n = whenFreedom j today
  let ageFree = roundTo 1 $ (n / 12) + (age cfg)
  row "savings rate" (pr $ savingsRate j today) Nothing
  row " target fund" (prn $ targetFund j today) Nothing
  row "   when free" ((pr n) <> " months") $ Just $ "I'll be " <> pr ageFree <> " years old"

  sec "runway"

--let (nut, cash, months) = runway j today
--row "   nut" (prn nut) Nothing
--row "  cash" (prn cash) Nothing
--row "months" (prn months) Nothing

sec :: String -> IO ()
sec label = putStrLn $ "\n" <> label <> ":"

pr :: Show s => s -> Text
pr = pack . show

row :: Text -> Text -> Maybe Text -> IO ()
row label value Nothing = IO.putStrLn $ gap <> label <> ":" <> gap <> value
row label value (Just nb) = IO.putStrLn $ gap <> label <> ":" <> gap <> value <> gap <> "\t(" <> nb <> ")"

gap :: Text
gap = "  "

-- | Pretty-print a number. From https://stackoverflow.com/a/61070523/1146898
prn :: Quantity -> Text
prn d = T.intercalate "." $ case T.splitOn "." $ T.pack $ show $ roundTo 2 d of
  x : xs -> (T.pack . reverse . go . reverse . T.unpack) x : xs
  xs -> xs
  where
    go (x : y : z : []) = x : y : z : []
    go (x : y : z : ['-']) = x : y : z : ['-']
    go (x : y : z : xs) = x : y : z : ',' : go xs
    go xs = xs

-- | There's levels to life, a proxy metric for what level you're at is your net
-- worth rounded down to the nearest power of 10.
level :: Decimal -> Integer
level = floor . logBase 10 . realToFrac

-- | A trivial decision is one that is between 0.01% of the total.
--
-- From <https://ofdollarsanddata.com/climbing-the-wealth-ladder/>
trivial :: Quantity
trivial = 0.0001

inUsdNow :: Maybe ValuationType
inUsdNow = Just $ AtNow $ Just "USD"

getTotal :: Journal -> Day -> ReportOpts -> String -> Quantity
getTotal j d opts q = sum $ map aquantity $ total
  where
    opts' = opts {today_ = Just d}
    (query, _) = parseQuery d $ pack q
    (_, (Mixed total)) = balanceReport opts' query j

getJournal :: IO Journal
getJournal = do
  jp <- defaultJournalPath
  let opts = definputopts {auto_ = True}
  ej <- readJournalFile opts jp
  return $ fromRight undefined ej

-- | These are the accounts that I consider a part of my savings and not my
-- cash-spending accounts.
savingsAccounts :: [String]
savingsAccounts =
  ["as:me:save", "as:me:vest"]

-- | Savings rate is a FIRE staple. Basically take your savings and divide it by
-- your income on a monthly basis.
--
-- I think this is wronge because I need to take the monthly ammounts, but this
-- gives total amounts
savingsRate :: Journal -> Day -> Quantity
savingsRate j d = roundTo 2 $ allSavings / allIncome
  where
    allSavings = getTotal j d (defreportopts {value_ = inUsdNow}) query
    query = List.intercalate " " $ savingsAccounts
    -- gotta flip the sign because income is negative
    allIncome = - getTotal j d (defreportopts {value_ = inUsdNow}) "^in"

-- | The target fund is simply 25x your annual expenditure.
--
-- This is going to be incomplete until I have a full year of
-- expenses.. currently, I just use my most recent quarter times 4 as a proxy
-- for the yearly expenses.
--
-- Assumptions: 4% withdrawal rate, 3-5% return on investments.
targetFund :: Journal -> Day -> Quantity
targetFund j d = 25 * yearlyExpenses
  where
    yearlyExpenses = sum $ map aquantity $ total
    (query, _) = parseQuery d $ pack "^ex"
    (_, (Mixed total)) = balanceReport opts query j
    opts =
      defreportopts
        { -- idk what the '2020 4' is for, but this actually results in the yearly
          -- report for some reason
          period_ = QuarterPeriod 2020 4,
          value_ = Just $ AtNow $ Just "USD",
          today_ = Just d
        }

-- | I have data going back to 2018.12. Use this for calculating averages per
-- month.
monthsSinceBeginning :: Day -> Quantity
monthsSinceBeginning d = fromInteger $ (year - 2019) * 12 + toInteger month + 1
  where
    (year, month, _) = toGregorian d

-- | How long until I can live off of my savings and investment returns?
--
-- Return integer is number of months until I'm free.
whenFreedom :: Journal -> Day -> Quantity
whenFreedom j d = roundTo 1 $ targetFund j d / monthlySavings
  where
    monthlySavings =
      savingsAccounts
        & map (getTotal j d (defreportopts {value_ = inUsdNow, period_ = MonthPeriod 2020 10}))
        & sum
        & \n -> (n / monthsSinceBeginning d)

-- | How many months I could sustain myself with my cash and savings, given my
-- current expenses.
-- runway :: Journal -> Day -> (Quantity, Quantity, Quantity)
-- runway j d = (nut, cash, cash / nut)
runway j d = total
  where
    nut = sum $ map aquantity total
    (_, (Mixed total)) =
      balanceReport
        (defreportopts {average_ = True, today_ = Just d, period_ = MonthPeriod 2020 10, interval_ = Months 6})
        (fst $ parseQuery d "^ex:me:need")
        j

    --        & \n -> (n / monthsSinceBeginning d)
    cash =
      getTotal
        j
        d
        (defreportopts {value_ = inUsdNow})
        "^as:me:save ^as:me:cash ^li:me:cred"

-- | Escape velocity:
--
-- In physics it is basically the movement of an object away from the earth,
-- calculated as:
--
-- v =  sqrt(2 * G * M / r)
--
-- where:
--
-- - G :: gravitational constant (inflation rate)
-- - M :: mass to be escaped from (liabilities)
-- - r :: distance from center of M (current net worth)
--
-- So, to translate that into my finances would be something like:
--
-- v = sqrt(2 * 3% * li / net_worth)
--
-- v = sqrt(2 * 3% * 20,000 / 100,000)
--
-- v = 0.1095
--
-- I don't know what this means... maybe my money must be growing at an 11% rate
-- in order to cover the debt? I need to think about this equation some more.
--
-- Basically, escape velocity will be when my assets are growing faster than my
-- debts. In order to know this, I need:
--
-- 1. the accrual of every liability
-- 2. the return on every investment i make
-- 3. accrual rate must be less than return rate
-- 4. my income must always be more than my expenses
--
-- Once I have all four conditions satisfied, then my finances will be in
-- correct order. The challenge then is to have a system that continually
-- satisfies the 4 conditions.
escapeVelocity :: Journal -> Quantity
escapeVelocity = undefined