content: {-|
  
  The @accounts@ command lists account names:
  
  - in flat mode (default), it lists the full names of accounts posted to by matched postings,
    clipped to the specified depth, possibly with leading components dropped.
  
  - in tree mode, it shows the indented short names of accounts posted to by matched postings,
    and their parents, to the specified depth.
  
  -}
  
  {-# LANGUAGE MultiWayIf #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  {-# LANGUAGE TemplateHaskell #-}
  
  module Hledger.Cli.Commands.Accounts (
    accountsmode
   ,accounts
  ) where
  
  import Control.Monad (forM_)
  import Data.List
  import Data.Maybe (fromMaybe)
  import Data.Text qualified as T
  import Data.Text.IO qualified as T
  import Safe (headDef)
  import System.Console.CmdArgs.Explicit as C
  
  import Hledger
  import Hledger.Cli.CliOptions
  
  
  -- | Command line options for this command.
  accountsmode = hledgerCommandMode
    $(embedFileRelative "Hledger/Cli/Commands/Accounts.txt")
    (
    [flagNone ["used","u"]     (setboolopt "used")       "list accounts used"
    ,flagNone ["declared","d"] (setboolopt "declared")   "list accounts declared"
    ,flagNone ["undeclared"]   (setboolopt "undeclared") "list accounts used but not declared"
    ,flagNone ["unused"]       (setboolopt "unused")     "list accounts declared but not used"
    ,flagNone ["find"]         (setboolopt "find")       "list the first account matched by the first argument (a case-insensitive infix regexp)"
  
    ,flagNone ["directives"]   (setboolopt "directives") "show as account directives, for use in journals"
    ,flagNone ["positions"]    (setboolopt "positions")  "also show where accounts were declared"
    ,flagNone ["types"]        (setboolopt "types")      "also show account types when known"
    ]
    ++ flattreeflags False ++
    [flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "flat mode: omit N leading account name parts"]
    )
    cligeneralflagsgroups1
    hiddenflags
    ([], Just $ argsFlag "[QUERY..]")
  
  -- | The accounts command.
  accounts :: CliOpts -> Journal -> IO ()
  accounts opts@CliOpts{rawopts_=rawopts, reportspec_=ReportSpec{_rsQuery=query,_rsReportOpts=ropts}} j = do
  
    -- 1. identify the accounts we'll show
    let tree     = tree_ ropts
        directives = boolopt "directives" rawopts
        positions = boolopt "positions" rawopts
        types = boolopt "types" rawopts
        -- Modified queries. These may not work with boolean queries (#2371).
        -- a depth limit will clip and exclude account names later, but we don't want to exclude accounts at this stage
        nodepthq = dbg4 "nodepthq" $ filterQuery (not . queryIsDepth) query
        -- just the acct: part of the query will be reapplied later, after clipping
        acctq = dbg4 "acctq" $ filterQuery queryIsAcct query
        dep = dbg4 "depth" $ queryDepth $ filterQuery queryIsDepth query
        -- when finding accounts used by postings, we remove tags that were declared on the posting,
        -- so that a tag: query will match account tags and not posting tags.
        matchedused = dbg5 "matchedused" $ nub $ map paccount $ journalPostings $
          filterJournalPostings nodepthq $ journalPostingsKeepAccountTagsOnly j
        matcheddeclared = dbg5 "matcheddeclared" $
          nub $
          filter (matchesAccountExtra (journalAccountType j) (journalInheritedAccountTags j) nodepthq) $
          map fst $ jdeclaredaccounts j
        matchedundeclared = dbg5 "matchedundeclared" $ nub $ matchedused \\ matcheddeclared
        matchedunused = dbg5 "matchedunused" $ nub $ matcheddeclared \\ matchedused
        -- keep synced with aregister
        matchedacct = dbg5 "matchedacct" $
          fromMaybe (error' $ show apat ++ " did not match any account.")   -- PARTIAL:
              . firstMatch $ journalAccountNamesDeclaredOrImplied j
          where
            firstMatch = case toRegexCI $ T.pack apat of
                Right re -> find (regexMatchText re)
                Left  _  -> const Nothing
            apat = headDef
              (error' "With --find, please provide an account name or\naccount pattern (case-insensitive, infix, regexp) as first command argument.")
              $ listofstringopt "args" rawopts
        matchedall = matcheddeclared ++ matchedused
        accts = dbg5 "accts to show" $
          case (usedOrDeclaredFromOpts opts, boolopt "find" rawopts) of
            (Nothing,         False) -> matchedall
            (Nothing,         True)  -> [matchedacct]
            (Just Used,       False) -> matchedused
            (Just Declared,   False) -> matcheddeclared
            (Just Undeclared, False) -> matchedundeclared
            (Just Unused,     False) -> matchedunused
            _ -> error' "please pick at most one of --used, --declared, --undeclared, --unused, --find"
  
    -- 2. sort them by declaration order (then undeclared accounts alphabetically)
    -- within each group of siblings
        sortedaccts = sortAccountNamesByDeclaration j tree accts
  
    -- 3. if there's a depth limit, depth-clip and remove any no longer useful items
        clippedaccts =
          dbg4 "clippedaccts" $
          filter (matchesAccount acctq) $  -- clipping can leave accounts that no longer match the query, remove such
          nub $                            -- clipping can leave duplicates (adjacent, hopefully)
          filter (not . T.null) $          -- depth:0 can leave nulls
          map (clipAccountName dep) $      -- clip at depth if specified
          sortedaccts
  
    -- 4. print what remains as a list or tree, maybe applying --drop in the former case.
    -- Add various bits of info if enabled.
    let
      showKeyword = if directives then "account " else ""
      -- some contortions here to show types nicely aligned
      showName a = case accountlistmode_ ropts of
        ALTree -> indent <> accountLeafName droppedName
        ALFlat -> droppedName
        where
          indent      = T.replicate (2 * (max 0 (accountNameLevel a - drop_ ropts) - 1)) " "
          droppedName = accountNameDrop (drop_ ropts) a
      showType a =
        case (types, journalAccountType j a) of
          (True, Just t) -> pad a <> "    ; type: " <> T.pack (show t)
          _ -> ""
      showAcctDeclOrder a
        | positions =
          (if types then "," else pad a <> "    ;") <>
          case lookup a $ jdeclaredaccounts j of
            Just adi ->
              " declared at " <> (T.pack $ sourcePosPretty $ adisourcepos adi) <>  -- TODO: hide the column number
              ", overall declaration order " <> (T.pack $ show $ adideclarationorder adi)
            Nothing -> " undeclared"
        | otherwise = ""
      pad a = T.replicate (maxwidth - T.length (showName a)) " "
      maxwidth = maximum $ map (T.length . showName) clippedaccts
  
    forM_ clippedaccts $ \a -> T.putStrLn $ showKeyword <> showName a <> showType a <> showAcctDeclOrder a
  