module NStack.Registry.Types (module NStack.Registry.Types) where

import Data.Aeson (FromJSON(..), ToJSON(..), encode)
import Data.Aeson.Types (fieldLabelModifier, genericToJSON, genericParseJSON, defaultOptions, Value(..), Parser, GFromJSON, GToJSON, Zero )
import Data.Map (Map)
import qualified Data.HashMap.Strict as HM
import Data.Text (Text)
import qualified Data.Char as C
import qualified Data.Text as T
import GHC.Generics (Generic, Rep)

import Servant.API
import Servant.API.ContentTypes (eitherDecodeLenient)
import qualified Network.HTTP.Media as M


  TODO(jonboulle ): use  instead ?
-- TODO(jonboulle): explicit encode/decode rather than derive from generic?

jsonLower :: Value -> Value
jsonLower (Object o) = Object . HM.fromList . map lowerPair . HM.toList $ o
    where lowerPair (key, val) = (T.toLower key, val)
jsonLower x = x

fromDropPrefix :: (Generic a, GFromJSON Zero (Rep a)) => Int -> Value -> Parser a
fromDropPrefix x = genericParseJSON (defaultOptions { fieldLabelModifier = map C.toLower . drop x }) . jsonLower

toDropPrefix :: (Generic a, GToJSON Zero (Rep a)) => Int -> a -> Value
toDropPrefix x = genericToJSON defaultOptions { fieldLabelModifier = map C.toLower . drop x }

  |New Docker image manifest format ( schemaVersion = 2 )
-- application/vnd.docker.distribution.manifest.v2+json
-- -v2-2/#image-manifest-field-descriptions
data DockerImageManifestV2_2 = DockerImageManifestV2_2 {
  must be 2
    imMediaType :: Text, -- must be application/vnd.docker.distribution.manifest.v2+json
    imConfig :: Descriptor, -- references a DockerImageConfigV1
    imLayers :: [Descriptor]
} deriving (Generic, Show);

instance FromJSON DockerImageManifestV2_2 where
    parseJSON = fromDropPrefix 2

instance ToJSON DockerImageManifestV2_2 where
    toJSON = toDropPrefix 2

dockerImageManifestV2_2MediaType :: M.MediaType
dockerImageManifestV2_2MediaType = "application" M.// "vnd.docker.distribution.manifest.v2+json"

-- |Media type for use with Servant
-- -0.11/docs/Servant-API-ContentTypes.html
instance Accept DockerImageManifestV2_2 where
    contentType _ = dockerImageManifestV2_2MediaType

instance FromJSON a => MimeUnrender DockerImageManifestV2_2 a where
    mimeUnrender _ = eitherDecodeLenient

instance ToJSON a => MimeRender DockerImageManifestV2_2 a where
    mimeRender _ = encode

-- |Descriptor is a reference to another entity (like an image manifest or layer)
  No official  schema , just the OCI one
-- application/vnd.oci.descriptor.v1+json
  -spec/blob/master/descriptor.md
data Descriptor = Descriptor {
    dMediaType :: Text, -- REQUIRED
    dSize :: Integer, -- REQUIRED
    dDigest :: Text, -- REQUIRED
    dUrls :: Maybe [Text],  -- optional
    dPlatform :: Maybe Platform, -- optional, ONLY when mediaType == application/vnd.docker.distribution.manifest.v2+json
  optional , OCI extension
} deriving (Generic, Show);

instance FromJSON Descriptor where
    parseJSON = fromDropPrefix 1

instance ToJSON Descriptor where
    toJSON = toDropPrefix 1

data Platform = Platform {
    pArchitecture :: Text, -- REQUIRED
    pOs :: Text, -- REQUIRED
    pOs_version :: Maybe Text, -- optional
    pOs_features :: Maybe [Text], -- optional
    pVariant :: Maybe Text, -- optional
    fpFeatures :: Maybe [Text] -- optional
} deriving (Generic, Show);

instance FromJSON Platform where
    parseJSON = genericParseJSON opts . jsonLower where
        opts = defaultOptions {
            -- os.version and os.features
            fieldLabelModifier = map (C.toLower . (\c -> if c=='_' then '.' else c)) . drop 1}

instance ToJSON Platform where
    toJSON = genericToJSON opts where
        opts = defaultOptions {
            -- os.version and os.features
            fieldLabelModifier = map (C.toLower . (\c -> if c=='_' then '.' else c)) . drop 1}

-- |Container config JSON
-- application/vnd.docker.container.image.v1+json
--
-- In principle this is documented here:
-- #docker-image-specification-v100
  but this is * incomplete * ! ( e.g. -spec/pull/371 )
--
  Very similar ( but not identical ) to application / vnd.oci.image.config.v1+json
-- -spec/blob/97ae57f204b5956aa313d453ead9094ff9056b32/config.md#properties
--
data DockerImageConfigV1 = DockerImageConfigV1 {
    icArchitecture :: String, -- REQUIRED
  optional , RFC3339 § 5.6 - formatted
    icAuthor :: Maybe String, -- optional
    icOs :: String, -- REQUIRED
    icConfig :: Maybe DockerExecutionConfig, -- optional
    icRootfs :: RootFSEntry, -- REQUIRED
    icHistory :: Maybe [HistoryEntry] -- optional
} deriving (Generic, Show);
-- Real example of this (from docker.io/library/busybox):
-- {
--     "architecture": "amd64",
--     "config": {
           < see DockerExecutionConfig below >
--     },
--     "container": "f06386f489abf85537c32f826603214955d8731ccc5b45103d5548688abfa417",
--     "container_config": {
        " Hostname " : " dfb178f19369 " ,
        "  " : " " ,
--       "User": "",
--       "AttachStdin": false,
--       "AttachStdout": false,
--       "AttachStderr": false,
--       "Tty": false,
        " OpenStdin " : false ,
--       "StdinOnce": false,
--       "Env": [
--         "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
--       ],
--       "Cmd": [
--         "/bin/sh",
--         "-c",
--         "#(nop) ",
          " CMD [  " ] "
--       ],
--       "ArgsEscaped": true,
--       "Image": "sha256:0348edacacf159ab6f19138182733dea8a418e06f526a2233d317c1f94549b4a",
--       "Volumes": null,
        " WorkingDir " : " " ,
        " Entrypoint " : null ,
        "  " : null ,
--       "Labels": {}
--     },
      " created " : " 2017 - 08 - 23T22:31:12.496245278Z " ,
--     "docker_version": "17.03.2-ce",
--     "history": [
--       < see HistoryEntry below >
--     ],
--     "os": "linux",
      "  " : < see RootFSEntry below >
--   }

-- |OCI Image Configuration
  application / vnd.oci.image.config.v1+json

instance FromJSON DockerImageConfigV1 where
    parseJSON = fromDropPrefix 2

instance ToJSON DockerImageConfigV1 where
    toJSON = toDropPrefix 2

instance Accept DockerImageConfigV1 where
    contentType _ =
        "application" M.// "vnd.docker.container.image.v1+json"

instance FromJSON a => MimeUnrender DockerImageConfigV1 a where
    mimeUnrender _ = eitherDecodeLenient

data RootFSEntry = RootFSEntry {
    rfType :: String, -- REQUIRED
    rfDiff_IDs :: [String] -- REQUIRED
} deriving (Generic, Show);
-- example:
--  {
--      "type": "layers",
--      "diff_ids": [
--          "sha256:6a749002dd6a65988a6696ca4d0c4cbe87145df74e3bf6feae4025ab28f420f2"
--      ]
--  }

instance FromJSON RootFSEntry where
    parseJSON = fromDropPrefix 2

instance ToJSON RootFSEntry where
    toJSON = toDropPrefix 2

data HistoryEntry = HistoryEntry {
    -- All fields optional
    hAuthor :: Maybe String,
    hCreated :: Maybe String,
    hCreated_By :: Maybe String,
    hEmpty_Layer :: Maybe Bool,
    hComment :: Maybe String
} deriving (Generic, Show);
-- example:
--  {
       " created " : " 2017 - 08 - 23T22:31:12.496245278Z " ,
       "  " : " /bin / sh -c # ( nop )   CMD [  " ] " ,
--      "empty_layer": true
--  }

instance FromJSON HistoryEntry where
    parseJSON = fromDropPrefix 1

instance ToJSON HistoryEntry where
    toJSON = toDropPrefix 1

-- |Execution parameters and configuration
-- As above with the enclosing DockerImageConfig, this is not well documented.
  In practise the best reference we have is probably the  code :
-- #L30
-- but this is a superset of what's used in practise.
--
  Similar implementation of the related OCI type :
-- -spec/blob/97ae57f204b5956aa313d453ead9094ff9056b32/specs-go/v1/config.go#L23
--
data DockerExecutionConfig = DockerExecutionConfig {
    -- All fields optional
    user :: Maybe String,
    exposedPorts :: Maybe (Map String Value),
    env :: Maybe [String],
    entrypoint :: Maybe [String],
    cmd :: Maybe [String],
    volumes :: Maybe (Map String String),
    workingDir :: Maybe String,
    labels :: Maybe (Map String String),
    stopSignal :: Maybe String
} deriving (Generic, Show);
-- Real example of this (from docker.io/library/busybox):
-- {
        " Hostname " : " dfb178f19369 " ,
        "  " : " " ,
--       "User": "",
--       "AttachStdin": false,
--       "AttachStdout": false,
--       "AttachStderr": false,
--       "Tty": false,
        " OpenStdin " : false ,
--       "StdinOnce": false,
--       "Env": [
--         "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
--       ],
--       "Cmd": [
--         "sh"
--       ],
--       "ArgsEscaped": true,
--       "Image": "sha256:0348edacacf159ab6f19138182733dea8a418e06f526a2233d317c1f94549b4a",
--       "Volumes": null,
        " WorkingDir " : " " ,
        " Entrypoint " : null ,
        "  " : null ,
--       "Labels": {}
-- }

instance ToJSON DockerExecutionConfig

instance FromJSON DockerExecutionConfig

  |Manifest list , aka " fat manifest " , pointing to one or more manifests
-- application/vnd.docker.distribution.manifest.list.v2+json
  -v2-2/#manifest-list
--
-- TODO(jonboulle): implement.
-- While these are likely rare, per the documentation we should be able to handle
-- them being returned whenever we request an image manifest.
--
-- data DockerManifestListV2 = DockerManifestListV2 {
--    schemaVersion :: Int,
--    mediaType :: Text,
--    manifests :: [Descriptor]
--}

-- |Marker file for an OCI Image Layout
-- application/vnd.oci.layout.header.v1+json: OCI Layout
-- -spec/blob/master/image-layout.md#oci-layout-file
--
-- This is a file that must exist at the path `oci-layout` within an OCI Image Layout
-- It may include additional fields
--
data OCIImageLayoutHeader = OCIImageLayoutHeader {
    imageLayoutVersion :: String -- REQUIRED
} deriving (Generic, Show);

instance ToJSON OCIImageLayoutHeader

instance FromJSON OCIImageLayoutHeader

-- | Image Index
-- Entrypoint for references and descriptors within an OCI Image Layout
-- application/vnd.oci.image.index.v1+json: Image Index
--
-- This must exist in the file `index.json` in the root of an OCI Image Layout
--
data OCIImageIndex = OCIImageIndex {
-- TODO(jonboulle): finish me
}

-- | OCI Image Manifest
-- application/vnd.oci.image.manifest.v1+json: Image manifest

-- Should be unnecessary to implement:
--
-- application/vnd.docker.image.rootfs.diff.tar.gzip: "Layer", as a gzipped tar
-- application/vnd.docker.image.rootfs.foreign.diff.tar.gzip: "Layer", as a gzipped tar that should never be pushed
-- application/vnd.docker.distribution.manifest.v1+json: schema1 (old manifest format)
-- application/vnd.docker.plugin.v1+json: Plugin config JSON
--
-- application/vnd.oci.image.layer.v1.tar+gzip: identical to application/vnd.docker.image.rootfs.diff.tar.gzip
-- application/vnd.oci.image.layer.v1.tar: uncompressed variant
-- application/vnd.oci.image.layer.nondistributable.v1.tar+gzip: layer that should not be uploaded
-- application/vnd.oci.image.layer.nondistributable.v1.tar: uncompressed variant

 TODO(jonboulle): explicit encode/decode rather than derive from generic?
 application/vnd.docker.distribution.manifest.v2+json
 -v2-2/#image-manifest-field-descriptions
 must be application/vnd.docker.distribution.manifest.v2+json
 references a DockerImageConfigV1
 |Media type for use with Servant
 -0.11/docs/Servant-API-ContentTypes.html
 |Descriptor is a reference to another entity (like an image manifest or layer)
 application/vnd.oci.descriptor.v1+json
 REQUIRED
 REQUIRED
 REQUIRED
 optional
 optional, ONLY when mediaType == application/vnd.docker.distribution.manifest.v2+json
 REQUIRED
 REQUIRED
 optional
 optional
 optional
 optional
 os.version and os.features
 os.version and os.features
 |Container config JSON
 application/vnd.docker.container.image.v1+json

 In principle this is documented here:
 #docker-image-specification-v100

 -spec/blob/97ae57f204b5956aa313d453ead9094ff9056b32/config.md#properties

 REQUIRED
 optional
 REQUIRED
 optional
 REQUIRED
 optional
 Real example of this (from docker.io/library/busybox):
 {
     "architecture": "amd64",
     "config": {
     },
     "container": "f06386f489abf85537c32f826603214955d8731ccc5b45103d5548688abfa417",
     "container_config": {
       "User": "",
       "AttachStdin": false,
       "AttachStdout": false,
       "AttachStderr": false,
       "Tty": false,
       "StdinOnce": false,
       "Env": [
         "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
       ],
       "Cmd": [
         "/bin/sh",
         "-c",
         "#(nop) ",
       ],
       "ArgsEscaped": true,
       "Image": "sha256:0348edacacf159ab6f19138182733dea8a418e06f526a2233d317c1f94549b4a",
       "Volumes": null,
       "Labels": {}
     },
     "docker_version": "17.03.2-ce",
     "history": [
       < see HistoryEntry below >
     ],
     "os": "linux",
   }
 |OCI Image Configuration
 REQUIRED
 REQUIRED
 example:
  {
      "type": "layers",
      "diff_ids": [
          "sha256:6a749002dd6a65988a6696ca4d0c4cbe87145df74e3bf6feae4025ab28f420f2"
      ]
  }
 All fields optional
 example:
  {
      "empty_layer": true
  }
 |Execution parameters and configuration
 As above with the enclosing DockerImageConfig, this is not well documented.
 #L30
 but this is a superset of what's used in practise.

 -spec/blob/97ae57f204b5956aa313d453ead9094ff9056b32/specs-go/v1/config.go#L23

 All fields optional
 Real example of this (from docker.io/library/busybox):
 {
       "User": "",
       "AttachStdin": false,
       "AttachStdout": false,
       "AttachStderr": false,
       "Tty": false,
       "StdinOnce": false,
       "Env": [
         "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
       ],
       "Cmd": [
         "sh"
       ],
       "ArgsEscaped": true,
       "Image": "sha256:0348edacacf159ab6f19138182733dea8a418e06f526a2233d317c1f94549b4a",
       "Volumes": null,
       "Labels": {}
 }
 application/vnd.docker.distribution.manifest.list.v2+json

 TODO(jonboulle): implement.
 While these are likely rare, per the documentation we should be able to handle
 them being returned whenever we request an image manifest.

 data DockerManifestListV2 = DockerManifestListV2 {
    schemaVersion :: Int,
    mediaType :: Text,
    manifests :: [Descriptor]
}
 |Marker file for an OCI Image Layout
 application/vnd.oci.layout.header.v1+json: OCI Layout
 -spec/blob/master/image-layout.md#oci-layout-file

 This is a file that must exist at the path `oci-layout` within an OCI Image Layout
 It may include additional fields

 REQUIRED
 | Image Index
 Entrypoint for references and descriptors within an OCI Image Layout
 application/vnd.oci.image.index.v1+json: Image Index

 This must exist in the file `index.json` in the root of an OCI Image Layout

 TODO(jonboulle): finish me
 | OCI Image Manifest
 application/vnd.oci.image.manifest.v1+json: Image manifest
 Should be unnecessary to implement:

 application/vnd.docker.image.rootfs.diff.tar.gzip: "Layer", as a gzipped tar
 application/vnd.docker.image.rootfs.foreign.diff.tar.gzip: "Layer", as a gzipped tar that should never be pushed
 application/vnd.docker.distribution.manifest.v1+json: schema1 (old manifest format)
 application/vnd.docker.plugin.v1+json: Plugin config JSON

 application/vnd.oci.image.layer.v1.tar+gzip: identical to application/vnd.docker.image.rootfs.diff.tar.gzip
 application/vnd.oci.image.layer.v1.tar: uncompressed variant
 application/vnd.oci.image.layer.nondistributable.v1.tar+gzip: layer that should not be uploaded
 application/vnd.oci.image.layer.nondistributable.v1.tar: uncompressed variant
module NStack.Registry.Types (module NStack.Registry.Types) where

import Data.Aeson (FromJSON(..), ToJSON(..), encode)
import Data.Aeson.Types (fieldLabelModifier, genericToJSON, genericParseJSON, defaultOptions, Value(..), Parser, GFromJSON, GToJSON, Zero )
import Data.Map (Map)
import qualified Data.HashMap.Strict as HM
import Data.Text (Text)
import qualified Data.Char as C
import qualified Data.Text as T
import GHC.Generics (Generic, Rep)

import Servant.API
import Servant.API.ContentTypes (eitherDecodeLenient)
import qualified Network.HTTP.Media as M


  TODO(jonboulle ): use  instead ?

jsonLower :: Value -> Value
jsonLower (Object o) = Object . HM.fromList . map lowerPair . HM.toList $ o
    where lowerPair (key, val) = (T.toLower key, val)
jsonLower x = x

fromDropPrefix :: (Generic a, GFromJSON Zero (Rep a)) => Int -> Value -> Parser a
fromDropPrefix x = genericParseJSON (defaultOptions { fieldLabelModifier = map C.toLower . drop x }) . jsonLower

toDropPrefix :: (Generic a, GToJSON Zero (Rep a)) => Int -> a -> Value
toDropPrefix x = genericToJSON defaultOptions { fieldLabelModifier = map C.toLower . drop x }

  |New Docker image manifest format ( schemaVersion = 2 )
data DockerImageManifestV2_2 = DockerImageManifestV2_2 {
  must be 2
    imLayers :: [Descriptor]
} deriving (Generic, Show);

instance FromJSON DockerImageManifestV2_2 where
    parseJSON = fromDropPrefix 2

instance ToJSON DockerImageManifestV2_2 where
    toJSON = toDropPrefix 2

dockerImageManifestV2_2MediaType :: M.MediaType
dockerImageManifestV2_2MediaType = "application" M.// "vnd.docker.distribution.manifest.v2+json"

instance Accept DockerImageManifestV2_2 where
    contentType _ = dockerImageManifestV2_2MediaType

instance FromJSON a => MimeUnrender DockerImageManifestV2_2 a where
    mimeUnrender _ = eitherDecodeLenient

instance ToJSON a => MimeRender DockerImageManifestV2_2 a where
    mimeRender _ = encode

  No official  schema , just the OCI one
  -spec/blob/master/descriptor.md
data Descriptor = Descriptor {
  optional , OCI extension
} deriving (Generic, Show);

instance FromJSON Descriptor where
    parseJSON = fromDropPrefix 1

instance ToJSON Descriptor where
    toJSON = toDropPrefix 1

data Platform = Platform {
} deriving (Generic, Show);

instance FromJSON Platform where
    parseJSON = genericParseJSON opts . jsonLower where
        opts = defaultOptions {
            fieldLabelModifier = map (C.toLower . (\c -> if c=='_' then '.' else c)) . drop 1}

instance ToJSON Platform where
    toJSON = genericToJSON opts where
        opts = defaultOptions {
            fieldLabelModifier = map (C.toLower . (\c -> if c=='_' then '.' else c)) . drop 1}

  but this is * incomplete * ! ( e.g. -spec/pull/371 )
  Very similar ( but not identical ) to application / vnd.oci.image.config.v1+json
data DockerImageConfigV1 = DockerImageConfigV1 {
  optional , RFC3339 § 5.6 - formatted
} deriving (Generic, Show);
           < see DockerExecutionConfig below >
        " Hostname " : " dfb178f19369 " ,
        "  " : " " ,
        " OpenStdin " : false ,
          " CMD [  " ] "
        " WorkingDir " : " " ,
        " Entrypoint " : null ,
        "  " : null ,
      " created " : " 2017 - 08 - 23T22:31:12.496245278Z " ,
      "  " : < see RootFSEntry below >

  application / vnd.oci.image.config.v1+json

instance FromJSON DockerImageConfigV1 where
    parseJSON = fromDropPrefix 2

instance ToJSON DockerImageConfigV1 where
    toJSON = toDropPrefix 2

instance Accept DockerImageConfigV1 where
    contentType _ =
        "application" M.// "vnd.docker.container.image.v1+json"

instance FromJSON a => MimeUnrender DockerImageConfigV1 a where
    mimeUnrender _ = eitherDecodeLenient

data RootFSEntry = RootFSEntry {
} deriving (Generic, Show);

instance FromJSON RootFSEntry where
    parseJSON = fromDropPrefix 2

instance ToJSON RootFSEntry where
    toJSON = toDropPrefix 2

data HistoryEntry = HistoryEntry {
    hAuthor :: Maybe String,
    hCreated :: Maybe String,
    hCreated_By :: Maybe String,
    hEmpty_Layer :: Maybe Bool,
    hComment :: Maybe String
} deriving (Generic, Show);
       " created " : " 2017 - 08 - 23T22:31:12.496245278Z " ,
       "  " : " /bin / sh -c # ( nop )   CMD [  " ] " ,

instance FromJSON HistoryEntry where
    parseJSON = fromDropPrefix 1

instance ToJSON HistoryEntry where
    toJSON = toDropPrefix 1

  In practise the best reference we have is probably the  code :
  Similar implementation of the related OCI type :
data DockerExecutionConfig = DockerExecutionConfig {
    user :: Maybe String,
    exposedPorts :: Maybe (Map String Value),
    env :: Maybe [String],
    entrypoint :: Maybe [String],
    cmd :: Maybe [String],
    volumes :: Maybe (Map String String),
    workingDir :: Maybe String,
    labels :: Maybe (Map String String),
    stopSignal :: Maybe String
} deriving (Generic, Show);
        " Hostname " : " dfb178f19369 " ,
        "  " : " " ,
        " OpenStdin " : false ,
        " WorkingDir " : " " ,
        " Entrypoint " : null ,
        "  " : null ,

instance ToJSON DockerExecutionConfig

instance FromJSON DockerExecutionConfig

  |Manifest list , aka " fat manifest " , pointing to one or more manifests
  -v2-2/#manifest-list

data OCIImageLayoutHeader = OCIImageLayoutHeader {
} deriving (Generic, Show);

instance ToJSON OCIImageLayoutHeader

instance FromJSON OCIImageLayoutHeader

data OCIImageIndex = OCIImageIndex {
}


