| 

 Options common to most hledger reports . 



Options common to most hledger reports.

-}

{-# LANGUAGE FlexibleContexts      #-}
# LANGUAGE FlexibleInstances      #
{-# LANGUAGE LambdaCase            #-}
# LANGUAGE MultiParamTypeClasses #
# LANGUAGE OverloadedStrings      #
{-# LANGUAGE RankNTypes            #-}
# LANGUAGE RecordWildCards        #
{-# LANGUAGE TemplateHaskell       #-}
# LANGUAGE TypeFamilies           #
# LANGUAGE TypeOperators #

module Hledger.Reports.ReportOptions (
  ReportOpts(..),
  HasReportOptsNoUpdate(..),
  HasReportOpts(..),
  ReportSpec(..),
  HasReportSpec(..),
  overEither,
  setEither,
  BalanceCalculation(..),
  BalanceAccumulation(..),
  AccountListMode(..),
  ValuationType(..),
  Layout(..),
  defreportopts,
  rawOptsToReportOpts,
  defreportspec,
  setDefaultConversionOp,
  reportOptsToSpec,
  updateReportSpec,
  updateReportSpecWith,
  rawOptsToReportSpec,
  balanceAccumulationOverride,
  flat_,
  tree_,
  reportOptsToggleStatus,
  simplifyStatuses,
  whichDate,
  journalValueAndFilterPostings,
  journalValueAndFilterPostingsWith,
  journalApplyValuationFromOpts,
  journalApplyValuationFromOptsWith,
  mixedAmountApplyValuationAfterSumFromOptsWith,
  valuationAfterSum,
  intervalFromRawOpts,
  queryFromFlags,
  transactionDateFn,
  postingDateFn,
  reportSpan,
  reportSpanBothDates,
  reportStartDate,
  reportEndDate,
  reportPeriodStart,
  reportPeriodOrJournalStart,
  reportPeriodLastDay,
  reportPeriodOrJournalLastDay,
  reportPeriodName
)
where

import Control.Applicative (Const(..), (<|>), liftA2)
import Control.Monad ((<=<), guard, join)
import Data.Char (toLower)
import Data.Either (fromRight)
import Data.Either.Extra (eitherToMaybe)
import Data.Functor.Identity (Identity(..))
import Data.List.Extra (find, isPrefixOf, nubSort)
import Data.Maybe (fromMaybe, isJust, isNothing)
import qualified Data.Text as T
import Data.Time.Calendar (Day, addDays)
import Data.Default (Default(..))
import Safe (headMay, lastDef, lastMay, maximumMay, readMay)

import Text.Megaparsec.Custom

import Hledger.Data
import Hledger.Query
import Hledger.Utils


-- | What to calculate for each cell in a balance report.
  " Balance report types - > Calculation type " in the hledger manual .
data BalanceCalculation =
    CalcChange      -- ^ Sum of posting amounts in the period.
  | CalcBudget      -- ^ Sum of posting amounts and the goal for the period.
  | CalcValueChange -- ^ Change from previous period's historical end value to this period's historical end value.
  | CalcGain        -- ^ Change from previous period's gain, i.e. valuation minus cost basis.
  deriving (Eq, Show)

instance Default BalanceCalculation where def = CalcChange

-- | How to accumulate calculated values across periods (columns) in a balance report.
  " Balance report types - > Accumulation type " in the hledger manual .
data BalanceAccumulation =
    PerPeriod   -- ^ No accumulation. Eg, shows the change of balance in each period.
  ^ Accumulate changes across periods , starting from zero at report start .
  | Historical  -- ^ Accumulate changes across periods, including any from before report start.
                --   Eg, shows the historical end balance of each period.
  deriving (Eq,Show)

instance Default BalanceAccumulation where def = PerPeriod

-- | Should accounts be displayed: in the command's default style, hierarchically, or as a flat list ?
data AccountListMode = ALFlat | ALTree deriving (Eq, Show)

instance Default AccountListMode where def = ALFlat

data Layout = LayoutWide (Maybe Int)
            | LayoutTall
            | LayoutBare
            | LayoutTidy
  deriving (Eq, Show)

-- | Standard options for customising report filtering and output.
  Most of these correspond to standard hledger command - line options
-- or query arguments, but not all. Some are used only by certain
-- commands, as noted below.
data ReportOpts = ReportOpts {
     -- for most reports:
     period_           :: Period
    ,interval_         :: Interval
  ^ Zero , one , or two statuses to be matched
    ,conversionop_     :: Maybe ConversionOp  -- ^ Which operation should we apply to conversion transactions?
    ,value_            :: Maybe ValuationType  -- ^ What value should amounts be converted to ?
    ,infer_prices_     :: Bool      -- ^ Infer market prices from transactions ?
    ,depth_            :: Maybe Int
    ,date2_            :: Bool
    ,empty_            :: Bool
    ,no_elide_         :: Bool
    ,real_             :: Bool
    ,format_           :: StringFormat
    ,pretty_           :: Bool
    ,querystring_      :: [T.Text]
    --
    ,average_          :: Bool
    -- for posting reports (register)
    ,related_          :: Bool
    -- for account transactions reports (aregister)
    ,txn_dates_        :: Bool
  for balance reports ( bal , bs , cf , is )
    ,balancecalc_      :: BalanceCalculation  -- ^ What to calculate in balance report cells
    ,balanceaccum_     :: BalanceAccumulation -- ^ How to accumulate balance report values over time
    ,budgetpat_        :: Maybe T.Text  -- ^ A case-insensitive description substring
                                        --   to select periodic transactions for budget reports.
    ( Not a regexp , nor a full hledger query , for now . )
    ,accountlistmode_  :: AccountListMode
    ,drop_             :: Int
    ,declared_         :: Bool  -- ^ Include accounts declared but not yet posted to ?
    ,row_total_        :: Bool
    ,no_total_         :: Bool
    ,show_costs_       :: Bool  -- ^ Show costs for reports which normally don't show them ?
    ,sort_amount_      :: Bool
    ,percent_          :: Bool
    ,invert_           :: Bool  -- ^ Flip all amount signs in reports ?
    ,normalbalance_    :: Maybe NormalSign
      -- ^ This can be set when running balance reports on a set of accounts
      --   with the same normal balance type (eg all assets, or all incomes).
      -- - It helps --sort-amount know how to sort negative numbers
      --   (eg in the income section of an income statement)
      -- - It helps compound balance report commands (is, bs etc.) do
      --   sign normalisation, converting normally negative subreports to
      --   normally positive for a more conventional display.
    ,color_            :: Bool
      -- ^ Whether to use ANSI color codes in text output.
    Influenced by the --color / colour flag ( cf CliOptions ) ,
      --   whether stdout is an interactive terminal, and the value of
    TERM and existence of  environment variables .
    ,transpose_        :: Bool
    ,layout_           :: Layout
 } deriving (Show)

instance Default ReportOpts where def = defreportopts

defreportopts :: ReportOpts
defreportopts = ReportOpts
    { period_           = PeriodAll
    , interval_         = NoInterval
    , statuses_         = []
    , conversionop_     = Nothing
    , value_            = Nothing
    , infer_prices_     = False
    , depth_            = Nothing
    , date2_            = False
    , empty_            = False
    , no_elide_         = False
    , real_             = False
    , format_           = def
    , pretty_           = False
    , querystring_      = []
    , average_          = False
    , related_          = False
    , txn_dates_        = False
    , balancecalc_      = def
    , balanceaccum_     = def
    , budgetpat_        = Nothing
    , accountlistmode_  = ALFlat
    , drop_             = 0
    , declared_         = False
    , row_total_        = False
    , no_total_         = False
    , show_costs_       = False
    , sort_amount_      = False
    , percent_          = False
    , invert_           = False
    , normalbalance_    = Nothing
    , color_            = False
    , transpose_        = False
    , layout_           = LayoutWide Nothing
    }

  | Generate a ReportOpts from raw command - line input , given a day .
-- This will fail with a usage error if it is passed
  - an invalid --format argument ,
-- - an invalid --value argument,
-- - if --valuechange is called with a valuation type other than -V/--value=end.
-- - an invalid --pretty argument,
rawOptsToReportOpts :: Day -> RawOpts -> ReportOpts
rawOptsToReportOpts d rawopts =

    let formatstring = T.pack <$> maybestringopt "format" rawopts
        querystring  = map T.pack $ listofstringopt "args" rawopts  -- doesn't handle an arg like "" right
        pretty = fromMaybe False $ alwaysneveropt "pretty" rawopts

        format = case parseStringFormat <$> formatstring of
            Nothing         -> defaultBalanceLineFormat
            Just (Right x)  -> x
            Just (Left err) -> usageError $ "could not parse format option: " ++ err

    in defreportopts
          {period_           = periodFromRawOpts d rawopts
          ,interval_         = intervalFromRawOpts rawopts
          ,statuses_         = statusesFromRawOpts rawopts
          ,conversionop_     = conversionOpFromRawOpts rawopts
          ,value_            = valuationTypeFromRawOpts rawopts
          ,infer_prices_     = boolopt "infer-market-prices" rawopts
          ,depth_            = maybeposintopt "depth" rawopts
          ,date2_            = boolopt "date2" rawopts
          ,empty_            = boolopt "empty" rawopts
          ,no_elide_         = boolopt "no-elide" rawopts
          ,real_             = boolopt "real" rawopts
          ,format_           = format
          ,querystring_      = querystring
          ,average_          = boolopt "average" rawopts
          ,related_          = boolopt "related" rawopts
          ,txn_dates_        = boolopt "txn-dates" rawopts
          ,balancecalc_      = balancecalcopt rawopts
          ,balanceaccum_     = balanceaccumopt rawopts
          ,budgetpat_        = maybebudgetpatternopt rawopts
          ,accountlistmode_  = accountlistmodeopt rawopts
          ,drop_             = posintopt "drop" rawopts
          ,declared_         = boolopt "declared" rawopts
          ,row_total_        = boolopt "row-total" rawopts
          ,no_total_         = boolopt "no-total" rawopts
          ,show_costs_       = boolopt "show-costs" rawopts
          ,sort_amount_      = boolopt "sort-amount" rawopts
          ,percent_          = boolopt "percent" rawopts
          ,invert_           = boolopt "invert" rawopts
          ,pretty_           = pretty
          ,color_            = useColorOnStdout -- a lower-level helper
          ,transpose_        = boolopt "transpose" rawopts
          ,layout_           = layoutopt rawopts
          }

-- | A fully-determined set of report parameters 
-- (report options with all partial values made total, eg the begin and end
-- dates are known, avoiding date/regex errors; plus the reporting date),
-- and the query successfully calculated from them.
--
  If you change the report options or date in one of these , you should
-- use `reportOptsToSpec` to regenerate the whole thing, avoiding inconsistency.
--
data ReportSpec = ReportSpec
  ^ The underlying ReportOpts used to generate this ReportSpec
  ^ The Day this ReportSpec is generated for
  ^ The generated Query for the given day
  ^ A list of QueryOpts for the given day
  } deriving (Show)

instance Default ReportSpec where def = defreportspec

defreportspec :: ReportSpec
defreportspec = ReportSpec
    { _rsReportOpts = def
    , _rsDay        = nulldate
    , _rsQuery      = Any
    , _rsQueryOpts  = []
    }

  | Set the default ConversionOp .
setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec
setDefaultConversionOp defop rspec@ReportSpec{_rsReportOpts=ropts} =
    rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just defop}}

accountlistmodeopt :: RawOpts -> AccountListMode
accountlistmodeopt =
  fromMaybe ALFlat . choiceopt parse where
    parse = \case
      "tree" -> Just ALTree
      "flat" -> Just ALFlat
      _      -> Nothing

-- Get the argument of the --budget option if any, or the empty string.
maybebudgetpatternopt :: RawOpts -> Maybe T.Text
maybebudgetpatternopt = fmap T.pack . maybestringopt "budget"

balancecalcopt :: RawOpts -> BalanceCalculation
balancecalcopt =
  fromMaybe CalcChange . choiceopt parse where
    parse = \case
      "sum"         -> Just CalcChange
      "valuechange" -> Just CalcValueChange
      "gain"        -> Just CalcGain
      "budget"      -> Just CalcBudget
      _             -> Nothing

balanceaccumopt :: RawOpts -> BalanceAccumulation
balanceaccumopt = fromMaybe PerPeriod . balanceAccumulationOverride

alwaysneveropt :: String -> RawOpts -> Maybe Bool
alwaysneveropt opt rawopts = case maybestringopt opt rawopts of
    Just "always" -> Just True
    Just "yes"    -> Just True
    Just "y"      -> Just True
    Just "never"  -> Just False
    Just "no"     -> Just False
    Just "n"      -> Just False
    Just _        -> usageError "--pretty's argument should be \"yes\" or \"no\" (or y, n, always, never)"
    _             -> Nothing

balanceAccumulationOverride :: RawOpts -> Maybe BalanceAccumulation
balanceAccumulationOverride rawopts = choiceopt parse rawopts <|> reportbal
  where
    parse = \case
      "historical" -> Just Historical
      "cumulative" -> Just Cumulative
      "change"     -> Just PerPeriod
      _            -> Nothing
    reportbal = case balancecalcopt rawopts of
      CalcValueChange -> Just PerPeriod
      _               -> Nothing

layoutopt :: RawOpts -> Layout
layoutopt rawopts = fromMaybe (LayoutWide Nothing) $ layout <|> column
  where
    layout = parse <$> maybestringopt "layout" rawopts
    column = LayoutBare <$ guard (boolopt "commodity-column" rawopts)

    parse opt = maybe err snd $ guard (not $ null s) *> find (isPrefixOf s . fst) checkNames
      where
        checkNames = [ ("wide", LayoutWide w)
                     , ("tall", LayoutTall)
                     , ("bare", LayoutBare)
                     , ("tidy", LayoutTidy)
                     ]
        -- For `--layout=elided,n`, elide to the given width
        (s,n) = break (==',') $ map toLower opt
        w = case drop 1 n of
              "" -> Nothing
              c | Just w' <- readMay c -> Just w'
              _ -> usageError "width in --layout=wide,WIDTH must be an integer"

        err = usageError "--layout's argument should be \"wide[,WIDTH]\", \"tall\", \"bare\", or \"tidy\""

-- Get the period specified by any -b/--begin, -e/--end and/or -p/--period
-- options appearing in the command line.
-- Its bounds are the rightmost begin date specified by a -b or -p, and
  the rightmost end date specified by a -e or -p . Cf # 1011 .
  Today 's date is provided to help interpret any relative dates .
periodFromRawOpts :: Day -> RawOpts -> Period
periodFromRawOpts d rawopts =
  case (mlastb, mlaste) of
    (Nothing, Nothing) -> PeriodAll
    (Just b, Nothing)  -> PeriodFrom b
    (Nothing, Just e)  -> PeriodTo e
    (Just b, Just e)   -> simplifyPeriod $ PeriodBetween b e
  where
    mlastb = case beginDatesFromRawOpts d rawopts of
                   [] -> Nothing
                   bs -> Just $ fromEFDay $ last bs
    mlaste = case endDatesFromRawOpts d rawopts of
                   [] -> Nothing
                   es -> Just $ fromEFDay $ last es

-- Get all begin dates specified by -b/--begin or -p/--period options, in order,
-- using the given date to interpret relative date expressions.
beginDatesFromRawOpts :: Day -> RawOpts -> [EFDay]
beginDatesFromRawOpts d = collectopts (begindatefromrawopt d)
  where
    begindatefromrawopt d' (n,v)
      | n == "begin" =
          either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
          fixSmartDateStrEither' d' (T.pack v)
      | n == "period" =
        case
          either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
          parsePeriodExpr d' (stripquotes $ T.pack v)
        of
          (_, DateSpan (Just b) _) -> Just b
          _                        -> Nothing
      | otherwise = Nothing

-- Get all end dates specified by -e/--end or -p/--period options, in order,
-- using the given date to interpret relative date expressions.
endDatesFromRawOpts :: Day -> RawOpts -> [EFDay]
endDatesFromRawOpts d = collectopts (enddatefromrawopt d)
  where
    enddatefromrawopt d' (n,v)
      | n == "end" =
          either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
          fixSmartDateStrEither' d' (T.pack v)
      | n == "period" =
        case
          either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
          parsePeriodExpr d' (stripquotes $ T.pack v)
        of
          (_, DateSpan _ (Just e)) -> Just e
          _                        -> Nothing
      | otherwise = Nothing

-- | Get the report interval, if any, specified by the last of -p/--period,
-- -D/--daily, -W/--weekly, -M/--monthly etc. options.
-- An interval from --period counts only if it is explicitly defined.
intervalFromRawOpts :: RawOpts -> Interval
intervalFromRawOpts = lastDef NoInterval . collectopts intervalfromrawopt
  where
    intervalfromrawopt (n,v)
      | n == "period" =
          either
            (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e)
            extractIntervalOrNothing $
            parsePeriodExpr
              (error' "intervalFromRawOpts: did not expect to need today's date here")  -- PARTIAL: should not happen; we are just getting the interval, which does not use the reference date
              (stripquotes $ T.pack v)
      | n == "daily"     = Just $ Days 1
      | n == "weekly"    = Just $ Weeks 1
      | n == "monthly"   = Just $ Months 1
      | n == "quarterly" = Just $ Quarters 1
      | n == "yearly"    = Just $ Years 1
      | otherwise = Nothing

-- | Extract the interval from the parsed -p/--period expression.
-- Return Nothing if an interval is not explicitly defined.
extractIntervalOrNothing :: (Interval, DateSpan) -> Maybe Interval
extractIntervalOrNothing (NoInterval, _) = Nothing
extractIntervalOrNothing (interval, _) = Just interval

-- | Get any statuses to be matched, as specified by -U/--unmarked,
-- -P/--pending, -C/--cleared flags. -UPC is equivalent to no flags,
  so this returns a list of 0 - 2 unique statuses .
statusesFromRawOpts :: RawOpts -> [Status]
statusesFromRawOpts = simplifyStatuses . collectopts statusfromrawopt
  where
    statusfromrawopt (n,_)
      | n == "unmarked"  = Just Unmarked
      | n == "pending"   = Just Pending
      | n == "cleared"   = Just Cleared
      | otherwise        = Nothing

-- | Reduce a list of statuses to just one of each status,
-- and if all statuses are present return the empty list.
simplifyStatuses l
  | length l' >= numstatuses = []
  | otherwise                = l'
  where
    l' = nubSort l
    numstatuses = length [minBound .. maxBound :: Status]

  | Add / remove this status from the status list . Used by    .
reportOptsToggleStatus s ropts@ReportOpts{statuses_=ss}
  | s `elem` ss = ropts{statuses_=filter (/= s) ss}
  | otherwise   = ropts{statuses_=simplifyStatuses (s:ss)}

-- | Parse the type of valuation to be performed, if any, specified by -V,
  -X/--exchange , or --value flags . If there 's more than one valuation type ,
-- the rightmost flag wins. This will fail with a usage error if an invalid
-- argument is passed to --value, or if --valuechange is called with a
-- valuation type other than -V/--value=end.
valuationTypeFromRawOpts :: RawOpts -> Maybe ValuationType
valuationTypeFromRawOpts rawopts = case (balancecalcopt rawopts, directval) of
    (CalcValueChange, Nothing       ) -> Just $ AtEnd Nothing  -- If no valuation requested for valuechange, use AtEnd
    (CalcValueChange, Just (AtEnd _)) -> directval             -- If AtEnd valuation requested, use it
    (CalcValueChange, _             ) -> usageError "--valuechange only produces sensible results with --value=end"
    (CalcGain,        Nothing       ) -> Just $ AtEnd Nothing  -- If no valuation requested for gain, use AtEnd
    (_,               _             ) -> directval             -- Otherwise, use requested valuation
  where
    directval = lastMay $ collectopts valuationfromrawopt rawopts
    valuationfromrawopt (n,v)  -- option name, value
      | n == "V"     = Just $ AtEnd Nothing
      | n == "X"     = Just $ AtEnd (Just $ T.pack v)
      | n == "value" = valueopt v
      | otherwise    = Nothing
    valueopt v
      | t `elem` ["cost","c"]  = AtEnd . Just <$> mc  -- keep supporting --value=cost,COMM for now
      | t `elem` ["then" ,"t"] = Just $ AtThen mc
      | t `elem` ["end" ,"e"]  = Just $ AtEnd  mc
      | t `elem` ["now" ,"n"]  = Just $ AtNow  mc
      | otherwise = case parsedateM t of
            Just d  -> Just $ AtDate d mc
            Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: then|end|now|t|e|n|YYYY-MM-DD"
      where
        -- parse --value's value: TYPE[,COMM]
        (t,c') = break (==',') v
        mc     = case drop 1 c' of
                   "" -> Nothing
                   c  -> Just $ T.pack c

-- | Parse the type of costing to be performed, if any, specified by -B/--cost
  or --value flags . If there 's more than one costing type , the rightmost flag
-- wins. This will fail with a usage error if an invalid argument is passed to
-- --cost or if a costing type is requested with --gain.
conversionOpFromRawOpts :: RawOpts -> Maybe ConversionOp
conversionOpFromRawOpts rawopts
    | isJust costFlag && balancecalcopt rawopts == CalcGain = usageError "--gain cannot be combined with --cost"
    | otherwise = costFlag
  where
    costFlag = lastMay $ collectopts conversionopfromrawopt rawopts
    conversionopfromrawopt (n,v)  -- option name, value
      | n == "B"                                    = Just ToCost
      | n == "value", takeWhile (/=',') v `elem` ["cost", "c"] = Just ToCost  -- keep supporting --value=cost for now
      | otherwise                                   = Nothing

-- | Select the Transaction date accessor based on --date2.
transactionDateFn :: ReportOpts -> (Transaction -> Day)
transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate

-- | Select the Posting date accessor based on --date2.
postingDateFn :: ReportOpts -> (Posting -> Day)
postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate

-- | Report which date we will report on based on --date2.
whichDate :: ReportOpts -> WhichDate
whichDate ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate

-- | Legacy-compatible convenience aliases for accountlistmode_.
tree_ :: ReportOpts -> Bool
tree_ ReportOpts{accountlistmode_ = ALTree} = True
tree_ ReportOpts{accountlistmode_ = ALFlat} = False

flat_ :: ReportOpts -> Bool
flat_ = not . tree_

  depthFromOpts : : ReportOpts - > Int
  depthFromOpts opts = min ( fromMaybe 99999 $ depth _ opts ) ( queryDepth $ queryFromOpts nulldate opts )

  | Convert a ' Journal ' 's amounts to cost and/or to value ( see
  ' journalApplyValuationFromOpts ' ) , and filter by the ' ReportSpec ' ' Query ' .
--
  We make sure to first filter by amt : and cur : terms , then value the
  ' Journal ' , then filter by the remaining terms .
journalValueAndFilterPostings :: ReportSpec -> Journal -> Journal
journalValueAndFilterPostings rspec j = journalValueAndFilterPostingsWith rspec j priceoracle
  where priceoracle = journalPriceOracle (infer_prices_ $ _rsReportOpts rspec) j

  | Like ' journalValueAndFilterPostings ' , but takes a ' PriceOracle ' as an argument .
journalValueAndFilterPostingsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
journalValueAndFilterPostingsWith rspec@ReportSpec{_rsQuery=q, _rsReportOpts=ropts} j =
    -- Filter by the remainder of the query
      filterJournal reportq
    -- Apply valuation and costing
    . journalApplyValuationFromOptsWith rspec
    -- Filter by amount and currency, so it matches pre-valuation/costing
      (if queryIsNull amtsymq then j else filterJournalAmounts amtsymq j)
  where
    -- with -r, replace each posting with its sibling postings
    filterJournal = if related_ ropts then filterJournalRelatedPostings else filterJournalPostings
    amtsymq = dbg3 "amtsymq" $ filterQuery queryIsAmtOrSym q
    reportq = dbg3 "reportq" $ filterQuery (not . queryIsAmtOrSym) q
    queryIsAmtOrSym = liftA2 (||) queryIsAmt queryIsSym

-- | Convert this journal's postings' amounts to cost and/or to value, if specified
-- by options (-B/--cost/-V/-X/--value etc.). Strip prices if not needed. This
-- should be the main stop for performing costing and valuation. The exception is
-- whenever you need to perform valuation _after_ summing up amounts, as in a
-- historical balance report with --value=end. valuationAfterSum will check for this
-- condition.
journalApplyValuationFromOpts :: ReportSpec -> Journal -> Journal
journalApplyValuationFromOpts rspec j =
    journalApplyValuationFromOptsWith rspec j priceoracle
  where priceoracle = journalPriceOracle (infer_prices_ $ _rsReportOpts rspec) j

  | Like journalApplyValuationFromOpts , but takes PriceOracle as an argument .
journalApplyValuationFromOptsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle =
    case balancecalc_ ropts of
      CalcGain -> journalMapPostings (\p -> postingTransformAmount (gain p) p) j
      _        -> journalMapPostings (\p -> postingTransformAmount (valuation p) p) $ costing j
  where
    valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)
    gain      p = maybe id (mixedAmountApplyGain      priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)
    costing     = journalToCost (fromMaybe NoConversionOp $ conversionop_ ropts)

    -- Find the end of the period containing this posting
    postingperiodend  = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts)
    mPeriodEnd = case interval_ ropts of
        NoInterval -> const . spanEnd . fst $ reportSpan j rspec
        _          -> spanEnd <=< latestSpanContaining (historical : spans)
    historical = DateSpan Nothing $ (fmap Exact . spanStart) =<< headMay spans
    spans = snd $ reportSpanBothDates j rspec
    styles = journalCommodityStyles j
    err = error "journalApplyValuationFromOpts: expected all spans to have an end date"

-- | Select the Account valuation functions required for performing valuation after summing
  amounts . Used in MultiBalanceReport to value historical and similar reports .
mixedAmountApplyValuationAfterSumFromOptsWith :: ReportOpts -> Journal -> PriceOracle
                                              -> (DateSpan -> MixedAmount -> MixedAmount)
mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle =
    case valuationAfterSum ropts of
        Just mc -> case balancecalc_ ropts of
            CalcGain -> gain mc
            _        -> \spn -> valuation mc spn . costing
        Nothing      -> const id
  where
    valuation mc spn = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)
    gain mc spn = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)
    costing = case fromMaybe NoConversionOp $ conversionop_ ropts of
        NoConversionOp -> id
        ToCost         -> styleMixedAmount styles . mixedAmountCost
    styles = journalCommodityStyles j
    err = error "mixedAmountApplyValuationAfterSumFromOptsWith: expected all spans to have an end date"

  | If the ReportOpts specify that we are performing valuation after summing amounts ,
-- return Just of the commodity symbol we're converting to, Just Nothing for the default,
-- and otherwise return Nothing.
-- Used for example with historical reports with --value=end.
valuationAfterSum :: ReportOpts -> Maybe (Maybe CommoditySymbol)
valuationAfterSum ropts = case value_ ropts of
    Just (AtEnd mc) | valueAfterSum -> Just mc
    _                               -> Nothing
  where valueAfterSum = balancecalc_  ropts == CalcValueChange
                     || balancecalc_  ropts == CalcGain
                     || balanceaccum_ ropts /= PerPeriod


-- | Convert report options to a query, ignoring any non-flag command line arguments.
queryFromFlags :: ReportOpts -> Query
queryFromFlags ReportOpts{..} = simplifyQuery $ And flagsq
  where
    flagsq = consIf   Real  real_
           . consJust Depth depth_
           $   [ (if date2_ then Date2 else Date) $ periodAsDateSpan period_
               , Or $ map StatusQ statuses_
               ]
    consIf f b = if b then (f True:) else id
    consJust f = maybe id ((:) . f)

-- Report dates.

-- | The effective report span is the start and end dates specified by
-- options or queries, or otherwise the earliest and latest transaction or
-- posting dates in the journal. If no dates are specified by options/queries
-- and the journal is empty, returns the null date span.
-- Also return the intervals if they are requested.
reportSpan :: Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpan = reportSpanHelper False

  | Like reportSpan , but uses both primary and secondary dates when calculating
-- the span.
reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpanBothDates = reportSpanHelper True

  | A helper for reportSpan , which takes a  indicating whether to use both
-- primary and secondary dates.
reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} =
    (reportspan, intervalspans)
  where
    -- The date span specified by -b/-e/-p options and query args if any.
    requestedspan  = dbg3 "requestedspan" $ if bothdates then queryDateSpan' query else queryDateSpan (date2_ ropts) query
    -- If we are requesting period-end valuation, the journal date span should
    -- include price directives after the last transaction
    journalspan = dbg3 "journalspan" $ if bothdates then journalDateSpanBothDates j else journalDateSpan (date2_ ropts) j
    pricespan = dbg3 "pricespan" . DateSpan Nothing $ case value_ ropts of
        Just (AtEnd _) -> fmap (Exact . addDays 1) . maximumMay . map pddate $ jpricedirectives j
        _              -> Nothing
    -- If the requested span is open-ended, close it using the journal's start and end dates.
    -- This can still be the null (open) span if the journal is empty.
    requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (journalspan `spanUnion` pricespan)
    -- The list of interval spans enclosing the requested span.
    -- This list can be empty if the journal was empty,
  or if    has added its special date:-tomorrow to the query
    -- and all txns are in the future.
    intervalspans  = dbg3 "intervalspans" $ splitSpan adjust (interval_ ropts) requestedspan'
      where
        -- When calculating report periods, we will adjust the start date back to the nearest interval boundary
        -- unless a start date was specified explicitly.
        adjust = isNothing $ spanStart requestedspan
    -- The requested span enlarged to enclose a whole number of intervals.
    -- This can be the null span if there were no intervals.
    reportspan = dbg3 "reportspan" $ DateSpan (fmap Exact . spanStart =<< headMay intervalspans)
                                              (fmap Exact . spanEnd =<< lastMay intervalspans)

reportStartDate :: Journal -> ReportSpec -> Maybe Day
reportStartDate j = spanStart . fst . reportSpan j

reportEndDate :: Journal -> ReportSpec -> Maybe Day
reportEndDate j = spanEnd . fst . reportSpan j

-- Some pure alternatives to the above. XXX review/clean up

-- Get the report's start date.
-- If no report period is specified, will be Nothing.
reportPeriodStart :: ReportSpec -> Maybe Day
reportPeriodStart = queryStartDate False . _rsQuery

-- Get the report's start date, or if no report period is specified,
-- the journal's start date (the earliest posting date). If there's no
-- report period and nothing in the journal, will be Nothing.
reportPeriodOrJournalStart :: ReportSpec -> Journal -> Maybe Day
reportPeriodOrJournalStart rspec j =
  reportPeriodStart rspec <|> journalStartDate False j

  Get the last day of the overall report period .
  This the inclusive end date ( one day before the
-- more commonly used, exclusive, report end date).
-- If no report period is specified, will be Nothing.
reportPeriodLastDay :: ReportSpec -> Maybe Day
reportPeriodLastDay = fmap (addDays (-1)) . queryEndDate False . _rsQuery

  Get the last day of the overall report period , or if no report
  period is specified , the last day of the journal ( ie the latest
-- posting date). If we're doing period-end valuation, include price
-- directive dates. If there's no report period and nothing in the
-- journal, will be Nothing.
reportPeriodOrJournalLastDay :: ReportSpec -> Journal -> Maybe Day
reportPeriodOrJournalLastDay rspec j = reportPeriodLastDay rspec <|> journalOrPriceEnd
  where
    journalOrPriceEnd = case value_ $ _rsReportOpts rspec of
        Just (AtEnd _) -> max (journalLastDay False j) lastPriceDirective
        _              -> journalLastDay False j
    lastPriceDirective = fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j

-- | Make a name for the given period in a multiperiod report, given
-- the type of balance being reported and the full set of report
-- periods. This will be used as a column heading (or row heading, in
-- a register summary report). We try to pick a useful name as follows:
--
-- - ending-balance reports: the period's end date
--
  - balance change reports where the periods are months and all in the same year :
--   the short month name in the current locale
--
  - all other balance change reports : a description of the datespan ,
--   abbreviated to compact form if possible (see showDateSpan).
reportPeriodName :: BalanceAccumulation -> [DateSpan] -> DateSpan -> T.Text
reportPeriodName balanceaccumulation spans =
  case balanceaccumulation of
    PerPeriod -> if multiyear then showDateSpan else showDateSpanMonthAbbrev
      where
        multiyear = (>1) $ length $ nubSort $ map spanStartYear spans
    _ -> maybe "" (showDate . prevday) . spanEnd

-- lenses

-- Reportable functors are so that we can create special lenses which can fail
-- and report on their failure.
class Functor f => Reportable f e where
    report :: a -> f (Either e a) -> f a

instance Reportable (Const r) e where
    report _ (Const x) = Const x

instance Reportable Identity e where
    report a (Identity i) = Identity $ fromRight a i

instance Reportable Maybe e where
    report _ = (eitherToMaybe =<<)

instance (e ~ a) => Reportable (Either a) e where
    report _ = join

-- | Apply a function over a lens, but report on failure.
overEither :: ((a -> Either e b) -> s -> Either e t) -> (a -> b) -> s -> Either e t
overEither l f = l (pure . f)

-- | Set a field using a lens, but report on failure.
setEither :: ((a -> Either e b) -> s -> Either e t) -> b -> s -> Either e t
setEither l = overEither l . const

type ReportableLens' s a = forall f. Reportable f String => (a -> f a) -> s -> f s

  | Lenses for ReportOpts .

  Implement HasReportOptsNoUpdate , the basic lenses for ReportOpts .
makeHledgerClassyLenses ''ReportOpts
makeHledgerClassyLenses ''ReportSpec

  | Special lenses for ReportOpts which also update the Query and QueryOpts in ReportSpec .
-- Note that these are not true lenses, as they have a further restriction on
-- the functor. This will work as a normal lens for all common uses, but since they
-- don't obey the lens laws for some fancy cases, they may fail in some exotic circumstances.
--
  Note that setEither / overEither should only be necessary with
-- querystring and reportOpts: the other lenses should never fail.
--
-- === Examples:
  > > > import Lens . Micro ( set )
  > > > _ rsQuery < $ > setEither querystring [ " assets " ] 
-- Right (Acct (RegexpCI "assets"))
  > > > _ rsQuery < $ > setEither querystring [ " ( assets " ] 
-- Left "This regular expression is malformed...
  > > > _ rsQuery $ set querystring [ " assets " ] 
-- Acct (RegexpCI "assets")
  > > > _ rsQuery $ set querystring [ " ( assets " ] 
  * * * Exception : Error : Updating ReportSpec failed : try using overEither instead of over or setEither instead of set
  > > > _ rsQuery $ set period ( MonthPeriod 2021 08 ) defreportspec
  Date DateSpan 2021 - 08
class HasReportOptsNoUpdate a => HasReportOpts a where
    reportOpts :: ReportableLens' a ReportOpts
    reportOpts = reportOptsNoUpdate
# INLINE reportOpts #

  XXX these names are a bit clashy

    period :: ReportableLens' a Period
    period = reportOpts.periodNoUpdate
    {-# INLINE period #-}

    statuses :: ReportableLens' a [Status]
    statuses = reportOpts.statusesNoUpdate
    {-# INLINE statuses #-}

    depth :: ReportableLens' a (Maybe Int)
    depth = reportOpts.depthNoUpdate
# INLINE depth #

    date2 :: ReportableLens' a Bool
    date2 = reportOpts.date2NoUpdate
# INLINE date2 #

    real :: ReportableLens' a Bool
    real = reportOpts.realNoUpdate
# INLINE real #

    querystring :: ReportableLens' a [T.Text]
    querystring = reportOpts.querystringNoUpdate
# INLINE querystring #

instance HasReportOpts ReportOpts

instance HasReportOptsNoUpdate ReportSpec where
    reportOptsNoUpdate = rsReportOpts

instance HasReportOpts ReportSpec where
    reportOpts f rspec = report (error' "Updating ReportSpec failed: try using overEither instead of over or setEither instead of set") $  -- PARTIAL:
      reportOptsToSpec (_rsDay rspec) <$> f (_rsReportOpts rspec)
# INLINE reportOpts #

  | Generate a ReportSpec from a set of ReportOpts on a given day .
reportOptsToSpec :: Day -> ReportOpts -> Either String ReportSpec
reportOptsToSpec day ropts = do
    (argsquery, queryopts) <- parseQueryList day $ querystring_ ropts
    return ReportSpec
      { _rsReportOpts = ropts
      , _rsDay        = day
      , _rsQuery      = simplifyQuery $ And [queryFromFlags ropts, argsquery]
      , _rsQueryOpts  = queryopts
      }

  | Update the ReportOpts and the fields derived from it in a ReportSpec ,
-- or return an error message if there is a problem such as missing or
  unparseable options data . This is the safe way to change a ReportSpec ,
-- ensuring that all fields (_rsQuery, _rsReportOpts, querystring_, etc.) are in sync.
updateReportSpec :: ReportOpts -> ReportSpec -> Either String ReportSpec
updateReportSpec = setEither reportOpts

  | Like updateReportSpec , but takes a ReportOpts - modifying function .
updateReportSpecWith :: (ReportOpts -> ReportOpts) -> ReportSpec -> Either String ReportSpec
updateReportSpecWith = overEither reportOpts

  | Generate a ReportSpec from RawOpts and a provided day , or return an error
-- string if there are regular expression errors.
rawOptsToReportSpec :: Day -> RawOpts -> Either String ReportSpec
rawOptsToReportSpec day = reportOptsToSpec day . rawOptsToReportOpts day

# LANGUAGE FlexibleContexts      #
# LANGUAGE LambdaCase            #
# LANGUAGE RankNTypes            #
# LANGUAGE TemplateHaskell       #
 | What to calculate for each cell in a balance report.
 ^ Sum of posting amounts in the period.
 ^ Sum of posting amounts and the goal for the period.
 ^ Change from previous period's historical end value to this period's historical end value.
 ^ Change from previous period's gain, i.e. valuation minus cost basis.
 | How to accumulate calculated values across periods (columns) in a balance report.
 ^ No accumulation. Eg, shows the change of balance in each period.
 ^ Accumulate changes across periods, including any from before report start.
   Eg, shows the historical end balance of each period.
 | Should accounts be displayed: in the command's default style, hierarchically, or as a flat list ?
 | Standard options for customising report filtering and output.
 or query arguments, but not all. Some are used only by certain
 commands, as noted below.
 for most reports:
 ^ Which operation should we apply to conversion transactions?
 ^ What value should amounts be converted to ?
 ^ Infer market prices from transactions ?

 for posting reports (register)
 for account transactions reports (aregister)
 ^ What to calculate in balance report cells
 ^ How to accumulate balance report values over time
 ^ A case-insensitive description substring
   to select periodic transactions for budget reports.
 ^ Include accounts declared but not yet posted to ?
 ^ Show costs for reports which normally don't show them ?
 ^ Flip all amount signs in reports ?
 ^ This can be set when running balance reports on a set of accounts
   with the same normal balance type (eg all assets, or all incomes).
 - It helps --sort-amount know how to sort negative numbers
   (eg in the income section of an income statement)
 - It helps compound balance report commands (is, bs etc.) do
   sign normalisation, converting normally negative subreports to
   normally positive for a more conventional display.
 ^ Whether to use ANSI color codes in text output.
color / colour flag ( cf CliOptions ) ,
   whether stdout is an interactive terminal, and the value of
 This will fail with a usage error if it is passed
format argument ,
 - an invalid --value argument,
 - if --valuechange is called with a valuation type other than -V/--value=end.
 - an invalid --pretty argument,
 doesn't handle an arg like "" right
 a lower-level helper
 | A fully-determined set of report parameters 
 (report options with all partial values made total, eg the begin and end
 dates are known, avoiding date/regex errors; plus the reporting date),
 and the query successfully calculated from them.

 use `reportOptsToSpec` to regenerate the whole thing, avoiding inconsistency.

 Get the argument of the --budget option if any, or the empty string.
 For `--layout=elided,n`, elide to the given width
 Get the period specified by any -b/--begin, -e/--end and/or -p/--period
 options appearing in the command line.
 Its bounds are the rightmost begin date specified by a -b or -p, and
 Get all begin dates specified by -b/--begin or -p/--period options, in order,
 using the given date to interpret relative date expressions.
 Get all end dates specified by -e/--end or -p/--period options, in order,
 using the given date to interpret relative date expressions.
 | Get the report interval, if any, specified by the last of -p/--period,
 -D/--daily, -W/--weekly, -M/--monthly etc. options.
 An interval from --period counts only if it is explicitly defined.
 PARTIAL: should not happen; we are just getting the interval, which does not use the reference date
 | Extract the interval from the parsed -p/--period expression.
 Return Nothing if an interval is not explicitly defined.
 | Get any statuses to be matched, as specified by -U/--unmarked,
 -P/--pending, -C/--cleared flags. -UPC is equivalent to no flags,
 | Reduce a list of statuses to just one of each status,
 and if all statuses are present return the empty list.
 | Parse the type of valuation to be performed, if any, specified by -V,
exchange , or --value flags . If there 's more than one valuation type ,
 the rightmost flag wins. This will fail with a usage error if an invalid
 argument is passed to --value, or if --valuechange is called with a
 valuation type other than -V/--value=end.
 If no valuation requested for valuechange, use AtEnd
 If AtEnd valuation requested, use it
 If no valuation requested for gain, use AtEnd
 Otherwise, use requested valuation
 option name, value
 keep supporting --value=cost,COMM for now
 parse --value's value: TYPE[,COMM]
 | Parse the type of costing to be performed, if any, specified by -B/--cost
value flags . If there 's more than one costing type , the rightmost flag
 wins. This will fail with a usage error if an invalid argument is passed to
 --cost or if a costing type is requested with --gain.
 option name, value
 keep supporting --value=cost for now
 | Select the Transaction date accessor based on --date2.
 | Select the Posting date accessor based on --date2.
 | Report which date we will report on based on --date2.
 | Legacy-compatible convenience aliases for accountlistmode_.

 Filter by the remainder of the query
 Apply valuation and costing
 Filter by amount and currency, so it matches pre-valuation/costing
 with -r, replace each posting with its sibling postings
 | Convert this journal's postings' amounts to cost and/or to value, if specified
 by options (-B/--cost/-V/-X/--value etc.). Strip prices if not needed. This
 should be the main stop for performing costing and valuation. The exception is
 whenever you need to perform valuation _after_ summing up amounts, as in a
 historical balance report with --value=end. valuationAfterSum will check for this
 condition.
 Find the end of the period containing this posting
 | Select the Account valuation functions required for performing valuation after summing
 return Just of the commodity symbol we're converting to, Just Nothing for the default,
 and otherwise return Nothing.
 Used for example with historical reports with --value=end.
 | Convert report options to a query, ignoring any non-flag command line arguments.
 Report dates.
 | The effective report span is the start and end dates specified by
 options or queries, or otherwise the earliest and latest transaction or
 posting dates in the journal. If no dates are specified by options/queries
 and the journal is empty, returns the null date span.
 Also return the intervals if they are requested.
 the span.
 primary and secondary dates.
 The date span specified by -b/-e/-p options and query args if any.
 If we are requesting period-end valuation, the journal date span should
 include price directives after the last transaction
 If the requested span is open-ended, close it using the journal's start and end dates.
 This can still be the null (open) span if the journal is empty.
 The list of interval spans enclosing the requested span.
 This list can be empty if the journal was empty,
 and all txns are in the future.
 When calculating report periods, we will adjust the start date back to the nearest interval boundary
 unless a start date was specified explicitly.
 The requested span enlarged to enclose a whole number of intervals.
 This can be the null span if there were no intervals.
 Some pure alternatives to the above. XXX review/clean up
 Get the report's start date.
 If no report period is specified, will be Nothing.
 Get the report's start date, or if no report period is specified,
 the journal's start date (the earliest posting date). If there's no
 report period and nothing in the journal, will be Nothing.
 more commonly used, exclusive, report end date).
 If no report period is specified, will be Nothing.
 posting date). If we're doing period-end valuation, include price
 directive dates. If there's no report period and nothing in the
 journal, will be Nothing.
 | Make a name for the given period in a multiperiod report, given
 the type of balance being reported and the full set of report
 periods. This will be used as a column heading (or row heading, in
 a register summary report). We try to pick a useful name as follows:

 - ending-balance reports: the period's end date

   the short month name in the current locale

   abbreviated to compact form if possible (see showDateSpan).
 lenses
 Reportable functors are so that we can create special lenses which can fail
 and report on their failure.
 | Apply a function over a lens, but report on failure.
 | Set a field using a lens, but report on failure.
 Note that these are not true lenses, as they have a further restriction on
 the functor. This will work as a normal lens for all common uses, but since they
 don't obey the lens laws for some fancy cases, they may fail in some exotic circumstances.

 querystring and reportOpts: the other lenses should never fail.

 === Examples:
 Right (Acct (RegexpCI "assets"))
 Left "This regular expression is malformed...
 Acct (RegexpCI "assets")
# INLINE period #
# INLINE statuses #
 PARTIAL:
 or return an error message if there is a problem such as missing or
 ensuring that all fields (_rsQuery, _rsReportOpts, querystring_, etc.) are in sync.
 string if there are regular expression errors.
| 

 Options common to most hledger reports . 



Options common to most hledger reports.

-}

# LANGUAGE FlexibleInstances      #
# LANGUAGE MultiParamTypeClasses #
# LANGUAGE OverloadedStrings      #
# LANGUAGE RecordWildCards        #
# LANGUAGE TypeFamilies           #
# LANGUAGE TypeOperators #

module Hledger.Reports.ReportOptions (
  ReportOpts(..),
  HasReportOptsNoUpdate(..),
  HasReportOpts(..),
  ReportSpec(..),
  HasReportSpec(..),
  overEither,
  setEither,
  BalanceCalculation(..),
  BalanceAccumulation(..),
  AccountListMode(..),
  ValuationType(..),
  Layout(..),
  defreportopts,
  rawOptsToReportOpts,
  defreportspec,
  setDefaultConversionOp,
  reportOptsToSpec,
  updateReportSpec,
  updateReportSpecWith,
  rawOptsToReportSpec,
  balanceAccumulationOverride,
  flat_,
  tree_,
  reportOptsToggleStatus,
  simplifyStatuses,
  whichDate,
  journalValueAndFilterPostings,
  journalValueAndFilterPostingsWith,
  journalApplyValuationFromOpts,
  journalApplyValuationFromOptsWith,
  mixedAmountApplyValuationAfterSumFromOptsWith,
  valuationAfterSum,
  intervalFromRawOpts,
  queryFromFlags,
  transactionDateFn,
  postingDateFn,
  reportSpan,
  reportSpanBothDates,
  reportStartDate,
  reportEndDate,
  reportPeriodStart,
  reportPeriodOrJournalStart,
  reportPeriodLastDay,
  reportPeriodOrJournalLastDay,
  reportPeriodName
)
where

import Control.Applicative (Const(..), (<|>), liftA2)
import Control.Monad ((<=<), guard, join)
import Data.Char (toLower)
import Data.Either (fromRight)
import Data.Either.Extra (eitherToMaybe)
import Data.Functor.Identity (Identity(..))
import Data.List.Extra (find, isPrefixOf, nubSort)
import Data.Maybe (fromMaybe, isJust, isNothing)
import qualified Data.Text as T
import Data.Time.Calendar (Day, addDays)
import Data.Default (Default(..))
import Safe (headMay, lastDef, lastMay, maximumMay, readMay)

import Text.Megaparsec.Custom

import Hledger.Data
import Hledger.Query
import Hledger.Utils


  " Balance report types - > Calculation type " in the hledger manual .
data BalanceCalculation =
  deriving (Eq, Show)

instance Default BalanceCalculation where def = CalcChange

  " Balance report types - > Accumulation type " in the hledger manual .
data BalanceAccumulation =
  ^ Accumulate changes across periods , starting from zero at report start .
  deriving (Eq,Show)

instance Default BalanceAccumulation where def = PerPeriod

data AccountListMode = ALFlat | ALTree deriving (Eq, Show)

instance Default AccountListMode where def = ALFlat

data Layout = LayoutWide (Maybe Int)
            | LayoutTall
            | LayoutBare
            | LayoutTidy
  deriving (Eq, Show)

  Most of these correspond to standard hledger command - line options
data ReportOpts = ReportOpts {
     period_           :: Period
    ,interval_         :: Interval
  ^ Zero , one , or two statuses to be matched
    ,depth_            :: Maybe Int
    ,date2_            :: Bool
    ,empty_            :: Bool
    ,no_elide_         :: Bool
    ,real_             :: Bool
    ,format_           :: StringFormat
    ,pretty_           :: Bool
    ,querystring_      :: [T.Text]
    ,average_          :: Bool
    ,related_          :: Bool
    ,txn_dates_        :: Bool
  for balance reports ( bal , bs , cf , is )
    ( Not a regexp , nor a full hledger query , for now . )
    ,accountlistmode_  :: AccountListMode
    ,drop_             :: Int
    ,row_total_        :: Bool
    ,no_total_         :: Bool
    ,sort_amount_      :: Bool
    ,percent_          :: Bool
    ,normalbalance_    :: Maybe NormalSign
    ,color_            :: Bool
    TERM and existence of  environment variables .
    ,transpose_        :: Bool
    ,layout_           :: Layout
 } deriving (Show)

instance Default ReportOpts where def = defreportopts

defreportopts :: ReportOpts
defreportopts = ReportOpts
    { period_           = PeriodAll
    , interval_         = NoInterval
    , statuses_         = []
    , conversionop_     = Nothing
    , value_            = Nothing
    , infer_prices_     = False
    , depth_            = Nothing
    , date2_            = False
    , empty_            = False
    , no_elide_         = False
    , real_             = False
    , format_           = def
    , pretty_           = False
    , querystring_      = []
    , average_          = False
    , related_          = False
    , txn_dates_        = False
    , balancecalc_      = def
    , balanceaccum_     = def
    , budgetpat_        = Nothing
    , accountlistmode_  = ALFlat
    , drop_             = 0
    , declared_         = False
    , row_total_        = False
    , no_total_         = False
    , show_costs_       = False
    , sort_amount_      = False
    , percent_          = False
    , invert_           = False
    , normalbalance_    = Nothing
    , color_            = False
    , transpose_        = False
    , layout_           = LayoutWide Nothing
    }

  | Generate a ReportOpts from raw command - line input , given a day .
rawOptsToReportOpts :: Day -> RawOpts -> ReportOpts
rawOptsToReportOpts d rawopts =

    let formatstring = T.pack <$> maybestringopt "format" rawopts
        pretty = fromMaybe False $ alwaysneveropt "pretty" rawopts

        format = case parseStringFormat <$> formatstring of
            Nothing         -> defaultBalanceLineFormat
            Just (Right x)  -> x
            Just (Left err) -> usageError $ "could not parse format option: " ++ err

    in defreportopts
          {period_           = periodFromRawOpts d rawopts
          ,interval_         = intervalFromRawOpts rawopts
          ,statuses_         = statusesFromRawOpts rawopts
          ,conversionop_     = conversionOpFromRawOpts rawopts
          ,value_            = valuationTypeFromRawOpts rawopts
          ,infer_prices_     = boolopt "infer-market-prices" rawopts
          ,depth_            = maybeposintopt "depth" rawopts
          ,date2_            = boolopt "date2" rawopts
          ,empty_            = boolopt "empty" rawopts
          ,no_elide_         = boolopt "no-elide" rawopts
          ,real_             = boolopt "real" rawopts
          ,format_           = format
          ,querystring_      = querystring
          ,average_          = boolopt "average" rawopts
          ,related_          = boolopt "related" rawopts
          ,txn_dates_        = boolopt "txn-dates" rawopts
          ,balancecalc_      = balancecalcopt rawopts
          ,balanceaccum_     = balanceaccumopt rawopts
          ,budgetpat_        = maybebudgetpatternopt rawopts
          ,accountlistmode_  = accountlistmodeopt rawopts
          ,drop_             = posintopt "drop" rawopts
          ,declared_         = boolopt "declared" rawopts
          ,row_total_        = boolopt "row-total" rawopts
          ,no_total_         = boolopt "no-total" rawopts
          ,show_costs_       = boolopt "show-costs" rawopts
          ,sort_amount_      = boolopt "sort-amount" rawopts
          ,percent_          = boolopt "percent" rawopts
          ,invert_           = boolopt "invert" rawopts
          ,pretty_           = pretty
          ,transpose_        = boolopt "transpose" rawopts
          ,layout_           = layoutopt rawopts
          }

  If you change the report options or date in one of these , you should
data ReportSpec = ReportSpec
  ^ The underlying ReportOpts used to generate this ReportSpec
  ^ The Day this ReportSpec is generated for
  ^ The generated Query for the given day
  ^ A list of QueryOpts for the given day
  } deriving (Show)

instance Default ReportSpec where def = defreportspec

defreportspec :: ReportSpec
defreportspec = ReportSpec
    { _rsReportOpts = def
    , _rsDay        = nulldate
    , _rsQuery      = Any
    , _rsQueryOpts  = []
    }

  | Set the default ConversionOp .
setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec
setDefaultConversionOp defop rspec@ReportSpec{_rsReportOpts=ropts} =
    rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just defop}}

accountlistmodeopt :: RawOpts -> AccountListMode
accountlistmodeopt =
  fromMaybe ALFlat . choiceopt parse where
    parse = \case
      "tree" -> Just ALTree
      "flat" -> Just ALFlat
      _      -> Nothing

maybebudgetpatternopt :: RawOpts -> Maybe T.Text
maybebudgetpatternopt = fmap T.pack . maybestringopt "budget"

balancecalcopt :: RawOpts -> BalanceCalculation
balancecalcopt =
  fromMaybe CalcChange . choiceopt parse where
    parse = \case
      "sum"         -> Just CalcChange
      "valuechange" -> Just CalcValueChange
      "gain"        -> Just CalcGain
      "budget"      -> Just CalcBudget
      _             -> Nothing

balanceaccumopt :: RawOpts -> BalanceAccumulation
balanceaccumopt = fromMaybe PerPeriod . balanceAccumulationOverride

alwaysneveropt :: String -> RawOpts -> Maybe Bool
alwaysneveropt opt rawopts = case maybestringopt opt rawopts of
    Just "always" -> Just True
    Just "yes"    -> Just True
    Just "y"      -> Just True
    Just "never"  -> Just False
    Just "no"     -> Just False
    Just "n"      -> Just False
    Just _        -> usageError "--pretty's argument should be \"yes\" or \"no\" (or y, n, always, never)"
    _             -> Nothing

balanceAccumulationOverride :: RawOpts -> Maybe BalanceAccumulation
balanceAccumulationOverride rawopts = choiceopt parse rawopts <|> reportbal
  where
    parse = \case
      "historical" -> Just Historical
      "cumulative" -> Just Cumulative
      "change"     -> Just PerPeriod
      _            -> Nothing
    reportbal = case balancecalcopt rawopts of
      CalcValueChange -> Just PerPeriod
      _               -> Nothing

layoutopt :: RawOpts -> Layout
layoutopt rawopts = fromMaybe (LayoutWide Nothing) $ layout <|> column
  where
    layout = parse <$> maybestringopt "layout" rawopts
    column = LayoutBare <$ guard (boolopt "commodity-column" rawopts)

    parse opt = maybe err snd $ guard (not $ null s) *> find (isPrefixOf s . fst) checkNames
      where
        checkNames = [ ("wide", LayoutWide w)
                     , ("tall", LayoutTall)
                     , ("bare", LayoutBare)
                     , ("tidy", LayoutTidy)
                     ]
        (s,n) = break (==',') $ map toLower opt
        w = case drop 1 n of
              "" -> Nothing
              c | Just w' <- readMay c -> Just w'
              _ -> usageError "width in --layout=wide,WIDTH must be an integer"

        err = usageError "--layout's argument should be \"wide[,WIDTH]\", \"tall\", \"bare\", or \"tidy\""

  the rightmost end date specified by a -e or -p . Cf # 1011 .
  Today 's date is provided to help interpret any relative dates .
periodFromRawOpts :: Day -> RawOpts -> Period
periodFromRawOpts d rawopts =
  case (mlastb, mlaste) of
    (Nothing, Nothing) -> PeriodAll
    (Just b, Nothing)  -> PeriodFrom b
    (Nothing, Just e)  -> PeriodTo e
    (Just b, Just e)   -> simplifyPeriod $ PeriodBetween b e
  where
    mlastb = case beginDatesFromRawOpts d rawopts of
                   [] -> Nothing
                   bs -> Just $ fromEFDay $ last bs
    mlaste = case endDatesFromRawOpts d rawopts of
                   [] -> Nothing
                   es -> Just $ fromEFDay $ last es

beginDatesFromRawOpts :: Day -> RawOpts -> [EFDay]
beginDatesFromRawOpts d = collectopts (begindatefromrawopt d)
  where
    begindatefromrawopt d' (n,v)
      | n == "begin" =
          either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
          fixSmartDateStrEither' d' (T.pack v)
      | n == "period" =
        case
          either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
          parsePeriodExpr d' (stripquotes $ T.pack v)
        of
          (_, DateSpan (Just b) _) -> Just b
          _                        -> Nothing
      | otherwise = Nothing

endDatesFromRawOpts :: Day -> RawOpts -> [EFDay]
endDatesFromRawOpts d = collectopts (enddatefromrawopt d)
  where
    enddatefromrawopt d' (n,v)
      | n == "end" =
          either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
          fixSmartDateStrEither' d' (T.pack v)
      | n == "period" =
        case
          either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
          parsePeriodExpr d' (stripquotes $ T.pack v)
        of
          (_, DateSpan _ (Just e)) -> Just e
          _                        -> Nothing
      | otherwise = Nothing

intervalFromRawOpts :: RawOpts -> Interval
intervalFromRawOpts = lastDef NoInterval . collectopts intervalfromrawopt
  where
    intervalfromrawopt (n,v)
      | n == "period" =
          either
            (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e)
            extractIntervalOrNothing $
            parsePeriodExpr
              (stripquotes $ T.pack v)
      | n == "daily"     = Just $ Days 1
      | n == "weekly"    = Just $ Weeks 1
      | n == "monthly"   = Just $ Months 1
      | n == "quarterly" = Just $ Quarters 1
      | n == "yearly"    = Just $ Years 1
      | otherwise = Nothing

extractIntervalOrNothing :: (Interval, DateSpan) -> Maybe Interval
extractIntervalOrNothing (NoInterval, _) = Nothing
extractIntervalOrNothing (interval, _) = Just interval

  so this returns a list of 0 - 2 unique statuses .
statusesFromRawOpts :: RawOpts -> [Status]
statusesFromRawOpts = simplifyStatuses . collectopts statusfromrawopt
  where
    statusfromrawopt (n,_)
      | n == "unmarked"  = Just Unmarked
      | n == "pending"   = Just Pending
      | n == "cleared"   = Just Cleared
      | otherwise        = Nothing

simplifyStatuses l
  | length l' >= numstatuses = []
  | otherwise                = l'
  where
    l' = nubSort l
    numstatuses = length [minBound .. maxBound :: Status]

  | Add / remove this status from the status list . Used by    .
reportOptsToggleStatus s ropts@ReportOpts{statuses_=ss}
  | s `elem` ss = ropts{statuses_=filter (/= s) ss}
  | otherwise   = ropts{statuses_=simplifyStatuses (s:ss)}

valuationTypeFromRawOpts :: RawOpts -> Maybe ValuationType
valuationTypeFromRawOpts rawopts = case (balancecalcopt rawopts, directval) of
    (CalcValueChange, _             ) -> usageError "--valuechange only produces sensible results with --value=end"
  where
    directval = lastMay $ collectopts valuationfromrawopt rawopts
      | n == "V"     = Just $ AtEnd Nothing
      | n == "X"     = Just $ AtEnd (Just $ T.pack v)
      | n == "value" = valueopt v
      | otherwise    = Nothing
    valueopt v
      | t `elem` ["then" ,"t"] = Just $ AtThen mc
      | t `elem` ["end" ,"e"]  = Just $ AtEnd  mc
      | t `elem` ["now" ,"n"]  = Just $ AtNow  mc
      | otherwise = case parsedateM t of
            Just d  -> Just $ AtDate d mc
            Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: then|end|now|t|e|n|YYYY-MM-DD"
      where
        (t,c') = break (==',') v
        mc     = case drop 1 c' of
                   "" -> Nothing
                   c  -> Just $ T.pack c

conversionOpFromRawOpts :: RawOpts -> Maybe ConversionOp
conversionOpFromRawOpts rawopts
    | isJust costFlag && balancecalcopt rawopts == CalcGain = usageError "--gain cannot be combined with --cost"
    | otherwise = costFlag
  where
    costFlag = lastMay $ collectopts conversionopfromrawopt rawopts
      | n == "B"                                    = Just ToCost
      | otherwise                                   = Nothing

transactionDateFn :: ReportOpts -> (Transaction -> Day)
transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate

postingDateFn :: ReportOpts -> (Posting -> Day)
postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate

whichDate :: ReportOpts -> WhichDate
whichDate ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate

tree_ :: ReportOpts -> Bool
tree_ ReportOpts{accountlistmode_ = ALTree} = True
tree_ ReportOpts{accountlistmode_ = ALFlat} = False

flat_ :: ReportOpts -> Bool
flat_ = not . tree_

  depthFromOpts : : ReportOpts - > Int
  depthFromOpts opts = min ( fromMaybe 99999 $ depth _ opts ) ( queryDepth $ queryFromOpts nulldate opts )

  | Convert a ' Journal ' 's amounts to cost and/or to value ( see
  ' journalApplyValuationFromOpts ' ) , and filter by the ' ReportSpec ' ' Query ' .
  We make sure to first filter by amt : and cur : terms , then value the
  ' Journal ' , then filter by the remaining terms .
journalValueAndFilterPostings :: ReportSpec -> Journal -> Journal
journalValueAndFilterPostings rspec j = journalValueAndFilterPostingsWith rspec j priceoracle
  where priceoracle = journalPriceOracle (infer_prices_ $ _rsReportOpts rspec) j

  | Like ' journalValueAndFilterPostings ' , but takes a ' PriceOracle ' as an argument .
journalValueAndFilterPostingsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
journalValueAndFilterPostingsWith rspec@ReportSpec{_rsQuery=q, _rsReportOpts=ropts} j =
      filterJournal reportq
    . journalApplyValuationFromOptsWith rspec
      (if queryIsNull amtsymq then j else filterJournalAmounts amtsymq j)
  where
    filterJournal = if related_ ropts then filterJournalRelatedPostings else filterJournalPostings
    amtsymq = dbg3 "amtsymq" $ filterQuery queryIsAmtOrSym q
    reportq = dbg3 "reportq" $ filterQuery (not . queryIsAmtOrSym) q
    queryIsAmtOrSym = liftA2 (||) queryIsAmt queryIsSym

journalApplyValuationFromOpts :: ReportSpec -> Journal -> Journal
journalApplyValuationFromOpts rspec j =
    journalApplyValuationFromOptsWith rspec j priceoracle
  where priceoracle = journalPriceOracle (infer_prices_ $ _rsReportOpts rspec) j

  | Like journalApplyValuationFromOpts , but takes PriceOracle as an argument .
journalApplyValuationFromOptsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle =
    case balancecalc_ ropts of
      CalcGain -> journalMapPostings (\p -> postingTransformAmount (gain p) p) j
      _        -> journalMapPostings (\p -> postingTransformAmount (valuation p) p) $ costing j
  where
    valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)
    gain      p = maybe id (mixedAmountApplyGain      priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)
    costing     = journalToCost (fromMaybe NoConversionOp $ conversionop_ ropts)

    postingperiodend  = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts)
    mPeriodEnd = case interval_ ropts of
        NoInterval -> const . spanEnd . fst $ reportSpan j rspec
        _          -> spanEnd <=< latestSpanContaining (historical : spans)
    historical = DateSpan Nothing $ (fmap Exact . spanStart) =<< headMay spans
    spans = snd $ reportSpanBothDates j rspec
    styles = journalCommodityStyles j
    err = error "journalApplyValuationFromOpts: expected all spans to have an end date"

  amounts . Used in MultiBalanceReport to value historical and similar reports .
mixedAmountApplyValuationAfterSumFromOptsWith :: ReportOpts -> Journal -> PriceOracle
                                              -> (DateSpan -> MixedAmount -> MixedAmount)
mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle =
    case valuationAfterSum ropts of
        Just mc -> case balancecalc_ ropts of
            CalcGain -> gain mc
            _        -> \spn -> valuation mc spn . costing
        Nothing      -> const id
  where
    valuation mc spn = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)
    gain mc spn = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)
    costing = case fromMaybe NoConversionOp $ conversionop_ ropts of
        NoConversionOp -> id
        ToCost         -> styleMixedAmount styles . mixedAmountCost
    styles = journalCommodityStyles j
    err = error "mixedAmountApplyValuationAfterSumFromOptsWith: expected all spans to have an end date"

  | If the ReportOpts specify that we are performing valuation after summing amounts ,
valuationAfterSum :: ReportOpts -> Maybe (Maybe CommoditySymbol)
valuationAfterSum ropts = case value_ ropts of
    Just (AtEnd mc) | valueAfterSum -> Just mc
    _                               -> Nothing
  where valueAfterSum = balancecalc_  ropts == CalcValueChange
                     || balancecalc_  ropts == CalcGain
                     || balanceaccum_ ropts /= PerPeriod


queryFromFlags :: ReportOpts -> Query
queryFromFlags ReportOpts{..} = simplifyQuery $ And flagsq
  where
    flagsq = consIf   Real  real_
           . consJust Depth depth_
           $   [ (if date2_ then Date2 else Date) $ periodAsDateSpan period_
               , Or $ map StatusQ statuses_
               ]
    consIf f b = if b then (f True:) else id
    consJust f = maybe id ((:) . f)


reportSpan :: Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpan = reportSpanHelper False

  | Like reportSpan , but uses both primary and secondary dates when calculating
reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpanBothDates = reportSpanHelper True

  | A helper for reportSpan , which takes a  indicating whether to use both
reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} =
    (reportspan, intervalspans)
  where
    requestedspan  = dbg3 "requestedspan" $ if bothdates then queryDateSpan' query else queryDateSpan (date2_ ropts) query
    journalspan = dbg3 "journalspan" $ if bothdates then journalDateSpanBothDates j else journalDateSpan (date2_ ropts) j
    pricespan = dbg3 "pricespan" . DateSpan Nothing $ case value_ ropts of
        Just (AtEnd _) -> fmap (Exact . addDays 1) . maximumMay . map pddate $ jpricedirectives j
        _              -> Nothing
    requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (journalspan `spanUnion` pricespan)
  or if    has added its special date:-tomorrow to the query
    intervalspans  = dbg3 "intervalspans" $ splitSpan adjust (interval_ ropts) requestedspan'
      where
        adjust = isNothing $ spanStart requestedspan
    reportspan = dbg3 "reportspan" $ DateSpan (fmap Exact . spanStart =<< headMay intervalspans)
                                              (fmap Exact . spanEnd =<< lastMay intervalspans)

reportStartDate :: Journal -> ReportSpec -> Maybe Day
reportStartDate j = spanStart . fst . reportSpan j

reportEndDate :: Journal -> ReportSpec -> Maybe Day
reportEndDate j = spanEnd . fst . reportSpan j


reportPeriodStart :: ReportSpec -> Maybe Day
reportPeriodStart = queryStartDate False . _rsQuery

reportPeriodOrJournalStart :: ReportSpec -> Journal -> Maybe Day
reportPeriodOrJournalStart rspec j =
  reportPeriodStart rspec <|> journalStartDate False j

  Get the last day of the overall report period .
  This the inclusive end date ( one day before the
reportPeriodLastDay :: ReportSpec -> Maybe Day
reportPeriodLastDay = fmap (addDays (-1)) . queryEndDate False . _rsQuery

  Get the last day of the overall report period , or if no report
  period is specified , the last day of the journal ( ie the latest
reportPeriodOrJournalLastDay :: ReportSpec -> Journal -> Maybe Day
reportPeriodOrJournalLastDay rspec j = reportPeriodLastDay rspec <|> journalOrPriceEnd
  where
    journalOrPriceEnd = case value_ $ _rsReportOpts rspec of
        Just (AtEnd _) -> max (journalLastDay False j) lastPriceDirective
        _              -> journalLastDay False j
    lastPriceDirective = fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j

  - balance change reports where the periods are months and all in the same year :
  - all other balance change reports : a description of the datespan ,
reportPeriodName :: BalanceAccumulation -> [DateSpan] -> DateSpan -> T.Text
reportPeriodName balanceaccumulation spans =
  case balanceaccumulation of
    PerPeriod -> if multiyear then showDateSpan else showDateSpanMonthAbbrev
      where
        multiyear = (>1) $ length $ nubSort $ map spanStartYear spans
    _ -> maybe "" (showDate . prevday) . spanEnd


class Functor f => Reportable f e where
    report :: a -> f (Either e a) -> f a

instance Reportable (Const r) e where
    report _ (Const x) = Const x

instance Reportable Identity e where
    report a (Identity i) = Identity $ fromRight a i

instance Reportable Maybe e where
    report _ = (eitherToMaybe =<<)

instance (e ~ a) => Reportable (Either a) e where
    report _ = join

overEither :: ((a -> Either e b) -> s -> Either e t) -> (a -> b) -> s -> Either e t
overEither l f = l (pure . f)

setEither :: ((a -> Either e b) -> s -> Either e t) -> b -> s -> Either e t
setEither l = overEither l . const

type ReportableLens' s a = forall f. Reportable f String => (a -> f a) -> s -> f s

  | Lenses for ReportOpts .

  Implement HasReportOptsNoUpdate , the basic lenses for ReportOpts .
makeHledgerClassyLenses ''ReportOpts
makeHledgerClassyLenses ''ReportSpec

  | Special lenses for ReportOpts which also update the Query and QueryOpts in ReportSpec .
  Note that setEither / overEither should only be necessary with
  > > > import Lens . Micro ( set )
  > > > _ rsQuery < $ > setEither querystring [ " assets " ] 
  > > > _ rsQuery < $ > setEither querystring [ " ( assets " ] 
  > > > _ rsQuery $ set querystring [ " assets " ] 
  > > > _ rsQuery $ set querystring [ " ( assets " ] 
  * * * Exception : Error : Updating ReportSpec failed : try using overEither instead of over or setEither instead of set
  > > > _ rsQuery $ set period ( MonthPeriod 2021 08 ) defreportspec
  Date DateSpan 2021 - 08
class HasReportOptsNoUpdate a => HasReportOpts a where
    reportOpts :: ReportableLens' a ReportOpts
    reportOpts = reportOptsNoUpdate
# INLINE reportOpts #

  XXX these names are a bit clashy

    period :: ReportableLens' a Period
    period = reportOpts.periodNoUpdate

    statuses :: ReportableLens' a [Status]
    statuses = reportOpts.statusesNoUpdate

    depth :: ReportableLens' a (Maybe Int)
    depth = reportOpts.depthNoUpdate
# INLINE depth #

    date2 :: ReportableLens' a Bool
    date2 = reportOpts.date2NoUpdate
# INLINE date2 #

    real :: ReportableLens' a Bool
    real = reportOpts.realNoUpdate
# INLINE real #

    querystring :: ReportableLens' a [T.Text]
    querystring = reportOpts.querystringNoUpdate
# INLINE querystring #

instance HasReportOpts ReportOpts

instance HasReportOptsNoUpdate ReportSpec where
    reportOptsNoUpdate = rsReportOpts

instance HasReportOpts ReportSpec where
      reportOptsToSpec (_rsDay rspec) <$> f (_rsReportOpts rspec)
# INLINE reportOpts #

  | Generate a ReportSpec from a set of ReportOpts on a given day .
reportOptsToSpec :: Day -> ReportOpts -> Either String ReportSpec
reportOptsToSpec day ropts = do
    (argsquery, queryopts) <- parseQueryList day $ querystring_ ropts
    return ReportSpec
      { _rsReportOpts = ropts
      , _rsDay        = day
      , _rsQuery      = simplifyQuery $ And [queryFromFlags ropts, argsquery]
      , _rsQueryOpts  = queryopts
      }

  | Update the ReportOpts and the fields derived from it in a ReportSpec ,
  unparseable options data . This is the safe way to change a ReportSpec ,
updateReportSpec :: ReportOpts -> ReportSpec -> Either String ReportSpec
updateReportSpec = setEither reportOpts

  | Like updateReportSpec , but takes a ReportOpts - modifying function .
updateReportSpecWith :: (ReportOpts -> ReportOpts) -> ReportSpec -> Either String ReportSpec
updateReportSpecWith = overEither reportOpts

  | Generate a ReportSpec from RawOpts and a provided day , or return an error
rawOptsToReportSpec :: Day -> RawOpts -> Either String ReportSpec
rawOptsToReportSpec day = reportOptsToSpec day . rawOptsToReportOpts day
