{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -O0 #-}

module Hercules.API
  ( api,
    servantApi,
    servantClientApi,
    swagger,
    useApi,
    enterApiE,
    API,
    ClientAuth,
    HerculesAPI (..),
    ClientAPI (..),
    HerculesServantAPI,
    AddAPIVersion,
    Id,
    Name,
    Result (..),

    -- * Reexports
    NoContent (..),

    -- * Utilities
    noContent,

    -- * Experimental
    openapi3,
  )
where

import Control.Lens
import Control.Monad
import Data.List qualified as L
import Data.OpenApi qualified as O3
import Data.Proxy (Proxy (..))
import Data.Swagger hiding (Header)
import Hercules.API.Accounts (AccountsAPI)
import Hercules.API.Agents (AgentsAPI)
import Hercules.API.Build as Client
  ( BuildAPI,
  )
import Hercules.API.ClientInfo (ClientInfoAPI)
import Hercules.API.Effects (EffectsAPI)
import Hercules.API.Forge (ForgeAPI)
import Hercules.API.GitLab (GitLabAPI)
import Hercules.API.Health (HealthAPI)
import Hercules.API.Organizations (OrganizationsAPI)
import Hercules.API.Orphans ()
import Hercules.API.Prelude
import Hercules.API.Projects (ProjectsAPI)
import Hercules.API.Repos (ReposAPI)
import Hercules.API.Result (Result (..))
import Hercules.API.Servant (useApi)
import Hercules.API.State (StateAPI)
import Servant.API
import Servant.Auth
import Servant.Auth.Swagger ()
import Servant.OpenApi qualified as SO3
import Servant.Swagger
import Servant.Swagger.UI.Core (SwaggerSchemaUI)

-- TODO remove health so we get clientapi
data HerculesAPI auth f = HerculesAPI
  { accounts :: f :- ToServantApi (AccountsAPI auth),
    clientInfo :: f :- ToServantApi (ClientInfoAPI auth),
    forges :: f :- ToServantApi (ForgeAPI auth),
    repos :: f :- ToServantApi (ReposAPI auth),
    projects :: f :- ToServantApi (ProjectsAPI auth),
    agents :: f :- ToServantApi (AgentsAPI auth),
    build :: f :- ToServantApi (Client.BuildAPI auth),
    effects :: f :- ToServantApi (EffectsAPI auth),
    health :: f :- ToServantApi (HealthAPI auth),
    organizations :: f :- ToServantApi (OrganizationsAPI auth),
    state :: f :- ToServantApi (StateAPI auth),
    gitlab :: f :- ToServantApi (GitLabAPI auth)
  }
  deriving (Generic)

data ClientAPI auth f = ClientAPI
  { clientAccounts :: f :- ToServantApi (AccountsAPI auth),
    clientClientInfo :: f :- ToServantApi (ClientInfoAPI auth),
    clientForges :: f :- ToServantApi (ForgeAPI auth),
    clientRepos :: f :- ToServantApi (ReposAPI auth),
    clientProjects :: f :- ToServantApi (ProjectsAPI auth),
    clientAgents :: f :- ToServantApi (AgentsAPI auth),
    clientBuild :: f :- ToServantApi (Client.BuildAPI auth),
    clientEffects :: f :- ToServantApi (EffectsAPI auth),
    clientOrganizations :: f :- ToServantApi (OrganizationsAPI auth),
    clientState :: f :- ToServantApi (StateAPI auth),
    clientGitLab :: f :- ToServantApi (GitLabAPI auth)
  }
  deriving (Generic)

type ClientAuth = Auth '[JWT, Cookie] ()

type HerculesServantAPI auth = AddAPIVersion (ToServantApi (HerculesAPI auth))

type ClientServantAPI auth = AddAPIVersion (ToServantApi (ClientAPI auth))

type AddAPIVersion api = "api" :> "v1" :> api

servantApi :: Proxy (HerculesServantAPI auth)
servantApi = Proxy

servantClientApi :: Proxy (ClientServantAPI auth)
servantClientApi = Proxy

type API auth =
  HerculesServantAPI auth
    :<|> "api"
      :> SwaggerSchemaUI "v1" "swagger.json"

api :: Proxy (API auth)
api = Proxy

swagger :: Swagger
swagger =
  toSwagger apiWithJWT
    & info
      . title
      .~ "Hercules CI API"
    & info
      . version
      .~ "v1"
    & info
      . description
      ?~ "You have reached the Hercules Continuous Integration Application Programming Interface. This user interface provides human friendly access to the various endpoints. To get a personal access token, use the `hci login` command and inspect `~/.config/hercules-ci/credentials.json`. Unquote the token and put it in a header as `Authorization: Bearer eyJ...`. To get started with Hercules CI, see hercules-ci.com. Happy building! —the Hercules team"
    & withTags clientProjects "project" "Project and job operations"
    & withTags clientBuild "build" "Build related operations"
    & withTags clientEffects "effect" "Effect related operations"
    & withTags clientState "state" "State files and locks, commonly used with effects and the hci command"
    & withTags clientForges "forge" "Forge operations"
    & withTags clientGitLab "gitlab" "GitLab-specific operations"
    & withTags clientRepos "repo" "Repository operations"
    & withTags clientAgents "agent" "Agent admin and monitoring operations"
    & withTags clientAccounts "account" "Account operations"
    & withTags clientOrganizations "organization" "Organizations and billing operations"
    & withTags clientClientInfo "client" "Ad hoc endpoints for the frontend and perhaps some client-side use cases"
  where
    withTags f tag desc = applyTagsFor (subOperations (clientApiProxy f) apiWithJWT) [tag & description ?~ desc]

apiWithJWT :: Proxy (ClientServantAPI (Auth '[JWT] ()))
apiWithJWT = servantClientApi @(Auth '[JWT] ())

-- | NOTE: this has not been tested yet.
openapi3 :: O3.OpenApi
openapi3 =
  SO3.toOpenApi apiWithJWT
    -- It's generating a lot of duplicate security requirements, so let's deduplicate them.
    & O3.security %~ (uniq . L.sortOn show)
  where
    -- Remove duplicates from a sorted list. (No custom prelude in this package for ordNub)
    uniq (a1 : a2 : as) = if a1 == a2 then uniq (a2 : as) else a1 : uniq (a2 : as)
    uniq as = as

clientApiProxy :: (ClientAPI (Auth '[JWT] ()) AsApi -> a) -> Proxy ("api" :> "v1" :> a)
clientApiProxy _ = Proxy

-- | 'Control.Monad.void' specialised to 'NoContent' to soothe the
-- compiler that rightfully warns about throwing away a do notation
-- result. By specialising, we make sure that we still get warnings
-- if the result type changes in the future. (We'll get an error)
noContent :: (Functor m) => m Servant.API.NoContent -> m ()
noContent = void
