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

{- | The vocabulary of the ecosystem adapter registry: the capability record an
ecosystem registers ('RegistryAdapter') and its four cohesive slices.

A 'RegistryAdapter' captures what an ecosystem __is__ -- a static fact of the
build, independent of anything an operator configures: how its request paths classify
and respond (the serve surface), how its metadata is read and
assembled, how its artifact requests are formed, and how a publish reaches a
registry. Which ecosystems are __active__ is configuration's fact, not this
record's: nothing here holds a URL, a credential, a limit, or a policy. Those
arrive as arguments when the composition root projects a consuming pipeline's
dependency record ('Ecluse.Core.Server.Context.PackumentDeps',
'Ecluse.Core.Server.Context.PublishDeps', the worker runtime's fetch wiring) from
an adapter's fields. The pipelines keep their own records and never read this one,
so an adapter is resolved at boot and never rides the hot path.

The record vocabulary lives apart from the registration
("Ecluse.Core.Registry.Adapter") so an ecosystem's adapter module can type its
record without importing the registry, which must import every adapter -- the
cycle-breaking @.Types@ extraction STYLE.md sanctions.
-}
module Ecluse.Core.Registry.Adapter.Types (
    -- * The capability record
    RegistryAdapter (..),

    -- * The serve surface
    AdapterServe (..),

    -- * Metadata
    AdapterMetadata (..),

    -- * Artifact requests
    AdapterArtifact (..),

    -- * Publish
    AdapterPublish (..),
) where

import Network.HTTP.Client (Manager, Request)

import Ecluse.Core.Credential (Secret)
import Ecluse.Core.Ecosystem (Ecosystem)
import Ecluse.Core.Package (InvalidEntry, PackageName)
import Ecluse.Core.Package.Merge (MergePlan, SourceId)
import Ecluse.Core.Registry (
    PublishRelayFault,
    PublishRelayResponse,
    UrlFormationError,
 )
import Ecluse.Core.Registry.CachedDocument (CachedDoc)
import Ecluse.Core.Registry.Metadata (MetadataClient, MetadataError)
import Ecluse.Core.Registry.Publish (PublishCodec)
import Ecluse.Core.Security (Limits)
import Ecluse.Core.Server.Context (MountRouter)
import Ecluse.Core.Server.Metadata (ManifestCaching)
import Ecluse.Core.Server.RouteSpec (RouteSpec)
import Ecluse.Core.Telemetry.Metrics qualified as Metric
import Ecluse.Core.Telemetry.Record (MetricsPort)
import Ecluse.Core.Telemetry.Span (TracingPort)

{- | One ecosystem's complete capability record: the interfaces every consuming
pipeline's wiring is projected from. Assembled once per ecosystem (npm's is
'Ecluse.Core.Registry.Npm.Adapter.npmAdapter') and resolved through
'Ecluse.Core.Registry.Adapter.adapterFor'.
-}
data RegistryAdapter = RegistryAdapter
    { RegistryAdapter -> Ecosystem
adapterEcosystem :: Ecosystem
    {- ^ The ecosystem this record serves. The registry's key must agree with it
    (pinned by the adapter spec), so a record can never be registered under a
    foreign ecosystem unnoticed.
    -}
    , RegistryAdapter -> AdapterServe
adapterServe :: AdapterServe
    -- ^ The web-facing serve surface: the route grammar and response contracts.
    , RegistryAdapter -> AdapterMetadata
adapterMetadata :: AdapterMetadata
    -- ^ The metadata capability: the read-handle constructor and the packument assembly.
    , RegistryAdapter -> AdapterArtifact
adapterArtifact :: AdapterArtifact
    -- ^ The artifact request formation, by filename and by authoritative URL.
    , RegistryAdapter -> AdapterPublish
adapterPublish :: AdapterPublish
    {- ^ The publish capability: the first-party relay, the name canonicaliser, the
    declared-name extractor the anti-shadowing guard reads a publish body through, and
    the mirror write's protocol codec.
    -}
    }

{- | The ecosystem's web-facing serve surface: the one slice of the record that is
about HTTP shape rather than registry protocol, typed against the agnostic action
and response vocabulary ("Ecluse.Core.Server.Context", "Ecluse.Core.Server.Response")
because it is web-facing by definition (the registry-to-server import direction is
deliberate here). Serve surfaces are where ecosystems diverge most, so this slice
stands alone rather than sharing shape with the protocol slices.

Both routing fields are __derived by the adapter from one declarative route table__
(npm's is "Ecluse.Core.Registry.Npm.Route"), so the surface the server routes and the
surface the manifest documents are two interpretations of a single declaration and
cannot drift apart.
-}
data AdapterServe = AdapterServe
    { AdapterServe -> MountRouter
serveRouter :: MountRouter
    {- ^ The ecosystem's __whole routing decision__: which of its paths a
    mount-relative request names, and what serving that amounts to (an
    'Ecluse.Core.Server.Context.RouteAction'). The authoritative router the server
    dispatches through. An unrecognised path yields the deny-by-default @404@.
    -}
    , AdapterServe -> NonEmpty RouteSpec
serveRoutes :: NonEmpty RouteSpec
    {- ^ The same route table as data: the declarative 'RouteSpec' projection of the
    patterns 'serveRouter' routes on, one per served route. The capability manifest
    ("Ecluse.Manifest") renders this rather than re-describing the path grammar, so the
    documented surface cannot drift from what is routed.
    -}
    }

{- | The ecosystem's metadata capability: how a package's metadata is read from an
origin and how a served document is assembled. The fields have exactly the shapes
the consuming dependency records carry
('Ecluse.Core.Server.Context.pdNewMetadataClient' and
'Ecluse.Core.Server.Context.pdAssemble'), so the composition root projects them
unchanged and registering an adapter cannot reshape a pipeline.
-}
data AdapterMetadata = AdapterMetadata
    { AdapterMetadata
-> TracingPort
-> MetricsPort
-> Upstream
-> ManifestCaching
-> (PackageName -> MetadataError -> IO ())
-> (PackageName -> [InvalidEntry] -> IO ())
-> (PackageName -> IO ())
-> Limits
-> Manager
-> Text
-> Maybe Secret
-> MetadataClient
metadataNewClient ::
        TracingPort ->
        MetricsPort ->
        Metric.Upstream ->
        ManifestCaching ->
        (PackageName -> MetadataError -> IO ()) ->
        (PackageName -> [InvalidEntry] -> IO ()) ->
        (PackageName -> IO ()) ->
        Limits ->
        Manager ->
        Text ->
        Maybe Secret ->
        MetadataClient
    {- ^ Build a per-request metadata client for one origin, given the per-fetch
    runtime parameters; the adapter closes over the ecosystem's raw fetch
    primitives.
    -}
    , AdapterMetadata
-> Text
-> Map SourceId CachedDoc
-> MergePlan
-> Maybe CachedDoc
-> CachedDoc
metadataAssemble :: Text -> Map SourceId CachedDoc -> MergePlan -> Maybe CachedDoc -> CachedDoc
    {- ^ Assemble the served document ('CachedDoc') from a merge plan and the raw source
    documents, rewriting each surviving version's artifact URL under the given mount
    base. The adapter projects each source and the precedence-winning base document
    ('Nothing' when there is none) into its own representation, merges, and injects the
    result back, so the neutral pipeline threads the documents without reading them.
    -}
    , AdapterMetadata -> CachedDoc -> LByteString
metadataSerialise :: CachedDoc -> LByteString
    {- ^ Encode an assembled served document ('CachedDoc') to its wire bytes. The
    adapter projects the document to its own representation and serialises it, so the
    neutral serve tail turns a served document into bytes without knowing its shape.
    -}
    }

{- | The ecosystem's artifact request formation: the two ways an artifact is
addressed, by conventional filename under a registry base and by its authoritative
upstream URL. The fields have exactly the shapes the consuming dependency records
carry ('Ecluse.Core.Server.Context.pdBuildArtifactRequestByFile' and
'Ecluse.Core.Server.Context.pdBuildArtifactRequestByUrl').
-}
data AdapterArtifact = AdapterArtifact
    { AdapterArtifact
-> Limits
-> Manager
-> Text
-> Maybe Secret
-> PackageName
-> Text
-> Either UrlFormationError Request
artifactByFile :: Limits -> Manager -> Text -> Maybe Secret -> PackageName -> Text -> Either UrlFormationError Request
    {- ^ Build an artifact request by conventional filename path under a base URL:
    how a trusted origin is addressed.
    -}
    , AdapterArtifact
-> Limits
-> Manager
-> Text
-> Maybe Secret
-> Text
-> Either UrlFormationError Request
artifactByUrl :: Limits -> Manager -> Text -> Maybe Secret -> Text -> Either UrlFormationError Request
    {- ^ Build an artifact request at its authoritative upstream URL: how a
    location the upstream chose is honoured. The URL is complete on its own, so an
    implementation must form the request from it alone: a caller may pass an empty
    base URL (there is no base to resolve against) and an anonymous credential
    ('Nothing'), as the mirror worker's fetch does.
    -}
    , AdapterArtifact -> [Text]
artifactHosts :: [Text]
    {- ^ The ecosystem's canonical artifact hosts, as URLs whose authorities feed
    the tarball-host gate: hosts the public registry serves artifact bytes from
    __by design__ (PyPI's @https://files.pythonhosted.org@), so the secure-default
    same-host policy admits them without the operator naming hostnames. Empty for
    an ecosystem (npm) whose artifacts ride the registry host.
    -}
    }

{- | The ecosystem's publish capability: relaying a client's own publish document,
canonicalising a raw package name, extracting the names a publish body declares, and
the mirror-write protocol codec. The relay, canonicaliser, and declared-name extractor
have exactly the shapes the consuming dependency record carries
('Ecluse.Core.Server.Context.pubRelayPublish',
'Ecluse.Core.Server.Context.pubCanonicaliseName', and
'Ecluse.Core.Server.Context.pubDeclaredNames'); the codec is the protocol half
of the mirror write, which the composition root marries to the shared publish
transport per mounted ecosystem ('Ecluse.Core.Registry.Publish.newMirrorPublish').
-}
data AdapterPublish = AdapterPublish
    { AdapterPublish
-> Limits
-> Manager
-> Text
-> Maybe Secret
-> PackageName
-> ByteString
-> IO (Either PublishRelayFault PublishRelayResponse)
publishRelay :: Limits -> Manager -> Text -> Maybe Secret -> PackageName -> ByteString -> IO (Either PublishRelayFault PublishRelayResponse)
    -- ^ Relay a client's publish document to the publication target, returning its response.
    , AdapterPublish -> Text -> Maybe PackageName
publishCanonicaliseName :: Text -> Maybe PackageName
    -- ^ Canonicalise a raw package-name string, or 'Nothing' when it cannot be parsed.
    , AdapterPublish -> LByteString -> [Text]
publishDeclaredNames :: LByteString -> [Text]
    {- ^ Extract every package name a publish body declares as its own identity, read
    from the ecosystem's own publish-document schema. The neutral publish pipeline's
    anti-shadowing body-name guard injects this and refuses any declared name that
    disagrees with the URL-path name, so npm's document shape stays adapter-side rather
    than coupling the pipeline. A body that declares no readable name yields @[]@.
    -}
    , AdapterPublish -> PublishCodec
publishCodec :: PublishCodec
    {- ^ The mirror write's protocol codec: publish document assembly and request
    formation, the probe's request and version-list projection, and the status
    semantics -- protocol only. The manager, credential mint, and fault
    classification are the shared transport's, supplied at the marriage.
    -}
    }