-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

-- | Bounded inventory accumulation counts source entries before unusable releases and duplicates drop.
module Ecluse.Core.Registry.VersionList (
    VersionListItem (..),
    VersionListState,
    emptyVersionList,
    collectVersionList,
    finishVersionList,
) where

import Data.Map.Strict qualified as Map

import Ecluse.Core.Registry (ParseError (ParseError))
import Ecluse.Core.Security (LimitError, Limits, checkVersionCountOf)
import Ecluse.Core.Version (Version, renderVersion)

-- | Container markers distinguish an empty inventory from a document of the wrong shape.
data VersionListItem = VersionListObject | VersionListContainer | VersionListInvalidContainer | VersionListEntry (Maybe Version)

-- | Count every observed entry, retaining only usable identifiers in source-key order.
data VersionListState = VersionListState
    { VersionListState -> Bool
inventoryObjectSeen :: Bool
    , VersionListState -> Bool
inventoryContainerSeen :: Bool
    , VersionListState -> Bool
inventoryAcceptEntries :: Bool
    , VersionListState -> Bool
inventoryInvalidContainer :: Bool
    , VersionListState -> Int
inventoryCount :: Int
    , VersionListState -> Map Text Version
inventoryVersions :: Map Text Version
    }

-- | Start without assuming that the response contains an inventory object.
emptyVersionList :: VersionListState
emptyVersionList :: VersionListState
emptyVersionList = Bool
-> Bool
-> Bool
-> Bool
-> Int
-> Map Text Version
-> VersionListState
VersionListState Bool
False Bool
False Bool
False Bool
False Int
0 Map Text Version
forall a. Monoid a => a
mempty

-- | Apply the ceiling before inserting a usable identifier or discarding an unusable entry.
collectVersionList :: Limits -> VersionListState -> VersionListItem -> Either LimitError VersionListState
collectVersionList :: Limits
-> VersionListState
-> VersionListItem
-> Either LimitError VersionListState
collectVersionList Limits
limits VersionListState
inventory = \case
    VersionListItem
VersionListObject -> VersionListState -> Either LimitError VersionListState
forall a b. b -> Either a b
Right VersionListState
inventory{inventoryObjectSeen = True}
    VersionListItem
VersionListContainer ->
        VersionListState -> Either LimitError VersionListState
forall a b. b -> Either a b
Right
            VersionListState
inventory
                { inventoryContainerSeen = True
                , inventoryAcceptEntries = not (inventoryContainerSeen inventory)
                }
    VersionListItem
VersionListInvalidContainer ->
        VersionListState -> Either LimitError VersionListState
forall a b. b -> Either a b
Right
            VersionListState
inventory
                { inventoryContainerSeen = True
                , inventoryAcceptEntries = False
                , inventoryInvalidContainer = inventoryInvalidContainer inventory || not (inventoryContainerSeen inventory)
                }
    VersionListEntry Maybe Version
_ | Bool -> Bool
not (VersionListState -> Bool
inventoryAcceptEntries VersionListState
inventory) -> VersionListState -> Either LimitError VersionListState
forall a b. b -> Either a b
Right VersionListState
inventory
    VersionListEntry Maybe Version
candidate -> do
        let count :: Int
count = VersionListState -> Int
inventoryCount VersionListState
inventory Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
        Limits -> Int -> Either LimitError ()
checkVersionCountOf Limits
limits Int
count
        VersionListState -> Either LimitError VersionListState
forall a. a -> Either LimitError a
forall (f :: * -> *) a. Applicative f => a -> f a
pure
            VersionListState
inventory
                { inventoryCount = count
                , inventoryVersions = maybe (inventoryVersions inventory) (\Version
version -> Text -> Version -> Map Text Version -> Map Text Version
forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert (Version -> Text
renderVersion Version
version) Version
version (VersionListState -> Map Text Version
inventoryVersions VersionListState
inventory)) candidate
                }

-- | Refuse invalid response and inventory containers instead of certifying an empty store.
finishVersionList :: VersionListState -> Either ParseError [Version]
finishVersionList :: VersionListState -> Either ParseError [Version]
finishVersionList VersionListState
inventory
    | Bool -> Bool
not (VersionListState -> Bool
inventoryObjectSeen VersionListState
inventory) = ParseError -> Either ParseError [Version]
forall a b. a -> Either a b
Left (Text -> ParseError
ParseError Text
"the version list is not a JSON object")
    | VersionListState -> Bool
inventoryInvalidContainer VersionListState
inventory = ParseError -> Either ParseError [Version]
forall a b. a -> Either a b
Left (Text -> ParseError
ParseError Text
"the versions field is not an object or null")
    | Bool
otherwise = [Version] -> Either ParseError [Version]
forall a b. b -> Either a b
Right (Map Text Version -> [Version]
forall k a. Map k a -> [a]
Map.elems (VersionListState -> Map Text Version
inventoryVersions VersionListState
inventory))