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

{- | The fields a Simple-index read passes to its projection, and the fold that selects one release's
files. Selected reads retain pending fields only until the first filename excludes the release.
-}
module Ecluse.Core.Registry.PyPI.Streaming (
    PyPIRead (..),
    PyPIField (..),
    fileScalars,
    hashNames,
    SelectedFileEvent (..),
    SelectedFile (..),
    collectSelected,
    finishSelected,
) where

import Data.Aeson (Value (Object, String))
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Universe.Class qualified as Universe

import Ecluse.Core.Package (PackageName)
import Ecluse.Core.Package.Hash (HashAlg (SRI), renderHashAlg)
import Ecluse.Core.Registry.PyPI.Project (FileProject, fileVersionKey)

-- | Select all files or one canonical release while counting every input file.
data PyPIRead = FullRead | SelectedRead PackageName Text
    deriving stock (PyPIRead -> PyPIRead -> Bool
(PyPIRead -> PyPIRead -> Bool)
-> (PyPIRead -> PyPIRead -> Bool) -> Eq PyPIRead
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PyPIRead -> PyPIRead -> Bool
== :: PyPIRead -> PyPIRead -> Bool
$c/= :: PyPIRead -> PyPIRead -> Bool
/= :: PyPIRead -> PyPIRead -> Bool
Eq, Int -> PyPIRead -> ShowS
[PyPIRead] -> ShowS
PyPIRead -> String
(Int -> PyPIRead -> ShowS)
-> (PyPIRead -> String) -> ([PyPIRead] -> ShowS) -> Show PyPIRead
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PyPIRead -> ShowS
showsPrec :: Int -> PyPIRead -> ShowS
$cshow :: PyPIRead -> String
show :: PyPIRead -> String
$cshowList :: [PyPIRead] -> ShowS
showList :: [PyPIRead] -> ShowS
Show)

-- | File events carry original positions, including items that yield no retained payload.
data PyPIField
    = EnvelopeField Text Value
    | FilesShape Bool
    | FileField Int (Maybe Value)
    | VersionsShape Bool
    | InvalidVersionField Int Value
    | IgnoredField
    deriving stock (PyPIField -> PyPIField -> Bool
(PyPIField -> PyPIField -> Bool)
-> (PyPIField -> PyPIField -> Bool) -> Eq PyPIField
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PyPIField -> PyPIField -> Bool
== :: PyPIField -> PyPIField -> Bool
$c/= :: PyPIField -> PyPIField -> Bool
/= :: PyPIField -> PyPIField -> Bool
Eq, Int -> PyPIField -> ShowS
[PyPIField] -> ShowS
PyPIField -> String
(Int -> PyPIField -> ShowS)
-> (PyPIField -> String)
-> ([PyPIField] -> ShowS)
-> Show PyPIField
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PyPIField -> ShowS
showsPrec :: Int -> PyPIField -> ShowS
$cshow :: PyPIField -> String
show :: PyPIField -> String
$cshowList :: [PyPIField] -> ShowS
showList :: [PyPIField] -> ShowS
Show)

-- | The scalar members a file retains.
fileScalars :: [Text]
fileScalars :: [Text]
fileScalars = [Text
"filename", Text
"url", Text
"requires-python", Text
"size", Text
"upload-time", Text
"yanked", Text
"provenance"]

-- | The digest names a file's @hashes@ object can use. Any other name keeps its own key.
hashNames :: [Text]
hashNames :: [Text]
hashNames = [HashAlg -> Text
renderHashAlg HashAlg
alg | HashAlg
alg <- [HashAlg]
forall a. Universe a => [a]
Universe.universe, HashAlg
alg HashAlg -> HashAlg -> Bool
forall a. Eq a => a -> a -> Bool
/= HashAlg
SRI]

-- | One member event of a file object that a selected read inspects.
data SelectedFileEvent
    = FileScalar Key.Key Value
    | HashesStart
    | HashesEnd
    | HashField Key.Key Value
    | HashesValue Value

-- | A file under selection: rejected by its name, or a candidate with its fields so far.
data SelectedFile
    = RejectedFile
    | CandidateFile Bool [(Key.Key, Value)] (Maybe Value) Bool

-- | Fold one member event into a file under selection. The first of each field wins.
collectSelected :: FileProject -> Text -> SelectedFile -> SelectedFileEvent -> SelectedFile
collectSelected :: FileProject
-> Text -> SelectedFile -> SelectedFileEvent -> SelectedFile
collectSelected FileProject
_ Text
_ SelectedFile
RejectedFile SelectedFileEvent
_ = SelectedFile
RejectedFile
collectSelected FileProject
project Text
wanted current :: SelectedFile
current@(CandidateFile Bool
matched [(Key, Value)]
scalars Maybe Value
hashes Bool
active) SelectedFileEvent
event = case SelectedFileEvent
event of
    FileScalar Key
key Value
value
        | ((Key, Value) -> Bool) -> [(Key, Value)] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ((Key -> Key -> Bool
forall a. Eq a => a -> a -> Bool
== Key
key) (Key -> Bool) -> ((Key, Value) -> Key) -> (Key, Value) -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Key, Value) -> Key
forall a b. (a, b) -> a
fst) [(Key, Value)]
scalars -> SelectedFile
current
        | Key
key Key -> Key -> Bool
forall a. Eq a => a -> a -> Bool
== Key
"filename" -> case Value
value of
            String Text
filename
                | FileProject -> Text -> Maybe Text
fileVersionKey FileProject
project Text
filename Maybe Text -> Maybe Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text -> Maybe Text
forall a. a -> Maybe a
Just Text
wanted ->
                    Bool -> [(Key, Value)] -> Maybe Value -> Bool -> SelectedFile
CandidateFile Bool
True ((Key
key, Value
value) (Key, Value) -> [(Key, Value)] -> [(Key, Value)]
forall a. a -> [a] -> [a]
: [(Key, Value)]
scalars) Maybe Value
hashes Bool
active
            Value
_ -> SelectedFile
RejectedFile
        | Bool
otherwise -> Bool -> [(Key, Value)] -> Maybe Value -> Bool -> SelectedFile
CandidateFile Bool
matched ((Key
key, Value
value) (Key, Value) -> [(Key, Value)] -> [(Key, Value)]
forall a. a -> [a] -> [a]
: [(Key, Value)]
scalars) Maybe Value
hashes Bool
active
    SelectedFileEvent
HashesStart ->
        Bool -> [(Key, Value)] -> Maybe Value -> Bool -> SelectedFile
CandidateFile Bool
matched [(Key, Value)]
scalars (Maybe Value
hashes Maybe Value -> Maybe Value -> Maybe Value
forall a. Maybe a -> Maybe a -> Maybe a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> Value -> Maybe Value
forall a. a -> Maybe a
Just (Object -> Value
Object Object
forall a. Monoid a => a
mempty)) (Maybe Value -> Bool
forall a. Maybe a -> Bool
isNothing Maybe Value
hashes)
    SelectedFileEvent
HashesEnd -> Bool -> [(Key, Value)] -> Maybe Value -> Bool -> SelectedFile
CandidateFile Bool
matched [(Key, Value)]
scalars Maybe Value
hashes Bool
False
    HashesValue Value
value -> Bool -> [(Key, Value)] -> Maybe Value -> Bool -> SelectedFile
CandidateFile Bool
matched [(Key, Value)]
scalars (Maybe Value
hashes Maybe Value -> Maybe Value -> Maybe Value
forall a. Maybe a -> Maybe a -> Maybe a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> Value -> Maybe Value
forall a. a -> Maybe a
Just Value
value) Bool
active
    HashField Key
key Value
value
        | Bool
active
        , Just (Object Object
fields) <- Maybe Value
hashes
        , Bool -> Bool
not (Key -> Object -> Bool
forall a. Key -> KeyMap a -> Bool
KeyMap.member Key
key Object
fields) ->
            Bool -> [(Key, Value)] -> Maybe Value -> Bool -> SelectedFile
CandidateFile Bool
matched [(Key, Value)]
scalars (Value -> Maybe Value
forall a. a -> Maybe a
Just (Object -> Value
Object (Key -> Value -> Object -> Object
forall v. Key -> v -> KeyMap v -> KeyMap v
KeyMap.insert Key
key Value
value Object
fields))) Bool
active
        | Bool
otherwise -> SelectedFile
current

-- | The retained object of a file the requested release names.
finishSelected :: SelectedFile -> Maybe Value
finishSelected :: SelectedFile -> Maybe Value
finishSelected (CandidateFile Bool
True [(Key, Value)]
fields Maybe Value
hashes Bool
_) =
    Value -> Maybe Value
forall a. a -> Maybe a
Just (Object -> Value
Object ([(Key, Value)] -> Object
forall v. [(Key, v)] -> KeyMap v
KeyMap.fromList (Maybe (Key, Value) -> [(Key, Value)]
forall a. Maybe a -> [a]
maybeToList ((,) Key
"hashes" (Value -> (Key, Value)) -> Maybe Value -> Maybe (Key, Value)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Value
hashes) [(Key, Value)] -> [(Key, Value)] -> [(Key, Value)]
forall a. Semigroup a => a -> a -> a
<> [(Key, Value)]
fields)))
finishSelected SelectedFile
_ = Maybe Value
forall a. Maybe a
Nothing