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

{- | PyPI's own request facts, composed through the agnostic mechanics in
"Ecluse.Core.Registry.Request".

Two protocol details are load-bearing. The index path carries its trailing slash, and its
project segment the PEP 503 canonical name, because PyPI redirects both and no data-plane
request follows a redirect. The index read asks for @gzip@ while an artifact request advertises
no encoding at all, so the bytes a client verifies against the served @sha256@ are the bytes
that arrived.
-}
module Ecluse.Core.Registry.PyPI.Request (
    -- * The ecosystem's artifact hosts
    pypiArtifactHosts,

    -- * Request building
    simpleIndexRequest,
    artifactRequestByFile,
    artifactRequestByUrl,

    -- * URL building
    simpleIndexUrl,
    artifactFileUrl,
    artifactPath,
) where

import Network.HTTP.Client (Request (decompress, requestHeaders))
import Network.HTTP.Types.Header (hAccept, hAcceptEncoding)

import Ecluse.Core.Credential (ClientCredential)
import Ecluse.Core.Package (PackageName)
import Ecluse.Core.Registry (UrlFormationError)
import Ecluse.Core.Registry.PyPI.Credential (pypiCredential)
import Ecluse.Core.Registry.PyPI.Project (canonicalName)
import Ecluse.Core.Registry.PyPI.Wire (simpleIndexMediaType)
import Ecluse.Core.Registry.Request (attachCredential, joinPath, parseRequestEither)
import Ecluse.Core.Registry.Request qualified as Request
import Ecluse.Core.Server.Path (encodeComponent)

{- | PyPI's canonical artifact hosts, declared to the artifact-host gate so the same-host default
admits the files host without an operator naming it.
-}
pypiArtifactHosts :: [Text]
pypiArtifactHosts :: [Text]
pypiArtifactHosts = [Text
"https://files.pythonhosted.org"]

{- | Build the Simple-index @GET@ for a project. It fails only when the URL cannot be formed,
which here means an empty base URL.
-}
simpleIndexRequest ::
    Text ->
    Maybe ClientCredential ->
    PackageName ->
    Either UrlFormationError Request
simpleIndexRequest :: Text
-> Maybe ClientCredential
-> PackageName
-> Either UrlFormationError Request
simpleIndexRequest Text
baseUrl Maybe ClientCredential
credential PackageName
name = do
    url <- Text -> PackageName -> Either UrlFormationError Text
simpleIndexUrl Text
baseUrl PackageName
name
    base <- parseRequestEither url
    pure
        . attachCredential pypiCredential credential
        $ base
            { requestHeaders =
                (hAccept, simpleIndexMediaType)
                    : (hAcceptEncoding, "gzip")
                    : requestHeaders base
            }

{- | Build the artifact @GET@ at @{baseUrl}\/simple\/{canonical-name}\/{filename}@, the spelling
this mount serves and a private index addresses its own files under.
-}
artifactRequestByFile ::
    Text ->
    Maybe ClientCredential ->
    PackageName ->
    Text ->
    Either UrlFormationError Request
artifactRequestByFile :: Text
-> Maybe ClientCredential
-> PackageName
-> Text
-> Either UrlFormationError Request
artifactRequestByFile Text
baseUrl Maybe ClientCredential
credential PackageName
name Text
filename = do
    url <- Text -> PackageName -> Text -> Either UrlFormationError Text
artifactFileUrl Text
baseUrl PackageName
name Text
filename
    base <- parseRequestEither url
    pure
        . attachCredential pypiCredential credential
        $ base{decompress = const False}

{- | Build PyPI's artifact @GET@ for the absolute @url@ the projection preserved from the index's
own @files[].url@, so it names no base URL of its own.
-}
artifactRequestByUrl ::
    Maybe ClientCredential ->
    Text ->
    Either UrlFormationError Request
artifactRequestByUrl :: Maybe ClientCredential -> Text -> Either UrlFormationError Request
artifactRequestByUrl = CredentialMapping
-> Maybe ClientCredential
-> Text
-> Either UrlFormationError Request
Request.artifactRequestByUrl CredentialMapping
pypiCredential

-- | The Simple-index URL @{baseUrl}\/simple\/{canonical-name}\/@.
simpleIndexUrl :: Text -> PackageName -> Either UrlFormationError Text
simpleIndexUrl :: Text -> PackageName -> Either UrlFormationError Text
simpleIndexUrl Text
baseUrl PackageName
name = Text -> Text -> Either UrlFormationError Text
joinPath Text
baseUrl (Text -> Text
projectPath (PackageName -> Text
canonicalName PackageName
name) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"/")

-- | The artifact URL @{baseUrl}\/simple\/{canonical-name}\/{encoded-filename}@.
artifactFileUrl :: Text -> PackageName -> Text -> Either UrlFormationError Text
artifactFileUrl :: Text -> PackageName -> Text -> Either UrlFormationError Text
artifactFileUrl Text
baseUrl PackageName
name Text
filename = Text -> Text -> Either UrlFormationError Text
joinPath Text
baseUrl (Text -> Text -> Text
artifactPath (PackageName -> Text
canonicalName PackageName
name) Text
filename)

{- | @simple\/{canonical-project}\/{file}@, relative to an index root. Each component is
percent-encoded, so a decoded escape cannot reach upstream raw.
-}
artifactPath :: Text -> Text -> Text
artifactPath :: Text -> Text -> Text
artifactPath Text
project Text
filename = Text -> Text
projectPath Text
project Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"/" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text
encodeComponent Text
filename

-- The project's index path, @simple\/{canonical-name}@.
projectPath :: Text -> Text
projectPath :: Text -> Text
projectPath Text
project = Text
"simple/" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text
encodeComponent Text
project