{-# LANGUAGE BlockArguments #-}

module Hercules.CLI.Nix where

import Control.Concurrent.Async (mapConcurrently)
import Control.Monad.IO.Unlift (unliftIO)
import Data.Has (Has)
import qualified Data.Map as M
import qualified Data.Text as T
import Hercules.API.Agent.Evaluate.EvaluateEvent.InputDeclaration (InputDeclaration (SiblingInput))
import qualified Hercules.API.Agent.Evaluate.EvaluateEvent.InputDeclaration as InputDeclaration
import Hercules.API.Attribute (attributePathFromString, attributePathToString)
import qualified Hercules.API.Inputs.ImmutableGitInput as API.ImmutableGitInput
import Hercules.API.Projects (getJobSource)
import Hercules.Agent.NixFile (getVirtualValueByPath)
import qualified Hercules.Agent.NixFile.GitSource as GitSource
import Hercules.Agent.NixFile.HerculesCIArgs (CISystems (CISystems), HerculesCIArgs)
import qualified Hercules.Agent.NixFile.HerculesCIArgs as HerculesCIArgs
import Hercules.CLI.Client (HerculesClientEnv, HerculesClientToken, determineDefaultApiBaseUrl, retryOnFail)
import Hercules.CLI.Common (runAuthenticated)
import Hercules.CLI.Git (getGitRoot, getRef, getRev, getUpstreamURL, guessForgeTypeFromURL)
import Hercules.CLI.Options (scanOption)
import Hercules.CLI.Project (ProjectPath (projectPathProject), getProjectPath, projectPathReadM, projectResourceClientByPath)
import Hercules.CNix (Store)
import Hercules.CNix.Expr as Expr (EvalState, Match (IsAttrs), NixAttrs, RawValue, Value, getAttr, getAttrs, getFlakeFromGit, init, isDerivation, match', toValue, withEvalState, withStore)
import qualified Hercules.CNix.Util as CNix.Util
import qualified Hercules.CNix.Verbosity as CNix.Verbosity
import Options.Applicative as Optparse
import Options.Applicative.Types (unReadM)
import Protolude hiding (evalState)
import RIO (RIO)
import UnliftIO (MonadUnliftIO, UnliftIO (UnliftIO), askUnliftIO)

createHerculesCIArgs :: Maybe Text -> IO HerculesCIArgs
createHerculesCIArgs passedRef = do
  gitRoot <- getGitRoot
  gitRev <- getRev
  ref <- computeRef passedRef
  upstreamURL <- getUpstreamURL
  let remoteHttpUrl = upstreamURL <$ guard ("http" `T.isPrefixOf` upstreamURL)
      remoteSshUrl = upstreamURL <$ guard (not ("http" `T.isPrefixOf` upstreamURL))
      guessWebUrlFromHttpUrl url = T.stripSuffix ".git" url & fromMaybe url
  let gitSource =
        GitSource.GitSource
          { outPath = toS gitRoot,
            ref = ref,
            rev = gitRev,
            shortRev = GitSource.shortRevFromRev gitRev,
            branch = GitSource.branchFromRef ref,
            tag = GitSource.tagFromRef ref,
            remoteHttpUrl = remoteHttpUrl,
            remoteSshUrl = remoteSshUrl,
            webUrl = guessWebUrlFromHttpUrl <$> remoteHttpUrl,
            forgeType = guessForgeTypeFromURL upstreamURL,
            owner = Nothing {- TODO; agent only for now -},
            name = Nothing {- TODO; agent only for now -}
          }
  url <- determineDefaultApiBaseUrl
  pure $ HerculesCIArgs.fromGitSource gitSource HerculesCIArgs.HerculesCIMeta {apiBaseUrl = url, ciSystems = CISystems Nothing}

computeRef :: Maybe Text -> IO Text
computeRef Nothing = getRef
computeRef (Just passedRef) = pure passedRef

resolveInputs ::
  (Has HerculesClientToken r, Has HerculesClientEnv r) =>
  UnliftIO (RIO r) ->
  Ptr EvalState ->
  Maybe ProjectPath ->
  Map ByteString InputDeclaration ->
  IO (Value NixAttrs)
resolveInputs uio evalState projectMaybe inputs = do
  projectPath <- unliftIO uio $ getProjectPath projectMaybe
  let resolveInput :: ByteString -> InputDeclaration -> IO RawValue
      resolveInput _name (SiblingInput input) = unliftIO uio do
        let resourceClient = projectResourceClientByPath (projectPath {projectPathProject = InputDeclaration.project input})
            jobNames = []
        immutableGitInput <- retryOnFail "get job source" (getJobSource resourceClient (InputDeclaration.ref input) jobNames)
        liftIO $ mkImmutableGitInputFlakeThunk evalState immutableGitInput
      resolveInput _name InputDeclaration.BogusInput {} = panic "resolveInput: not implemented yet"
  inputs
    & M.mapWithKey (,)
    & mapConcurrently (uncurry resolveInput)
    & (>>= toValue evalState)

refBranchToRef :: Maybe Text -> Maybe Text -> Maybe Text
refBranchToRef ref branch = ref <|> (("refs/heads/" <>) <$> branch)

withNix :: (MonadUnliftIO m) => (Store -> Ptr EvalState -> m b) -> m b
withNix f = do
  liftIO do
    Expr.init
    CNix.Util.installDefaultSigINTHandler
  UnliftIO uio <- askUnliftIO
  liftIO $ withStore \store -> withEvalState store (uio . f store)

mkSemanticTextCompleter :: (Text -> IO [(CompletionItemOptions, Text)]) -> Completer
mkSemanticTextCompleter f =
  mkTextCompleter
    ( \input -> do
        let startsEscape = input & T.reverse & T.takeWhile (== '\\') & T.length & odd
            innerCompleter = isoCompleter decodeBash encodeBash f
        if startsEscape
          then do
            r <- innerCompleter (T.dropEnd 1 input)
            -- Requiring input to be a prefix of the suggestions prevents corrections,
            -- so we only filter when necessary.
            pure
              [ item
                | item@(_, suggestionText) <- r,
                  input `T.isPrefixOf` suggestionText
              ]
          else innerCompleter input
    )

mkAttributePathCompleter :: (([Text], Text) -> IO [(CompletionItemOptions, ([Text], Bool))]) -> Completer
mkAttributePathCompleter f =
  mkSemanticTextCompleter
    ( \input -> do
        let startsEscape =
              (input & T.reverse & T.takeWhile (== '\\') & T.length & odd)
                || (".\"" `T.isSuffixOf` input)
            decode s
              | ".\"\"" `T.isSuffixOf` s =
                  (attributePathFromString s, "")
            decode s =
              let path = attributePathFromString s
               in (initSafe path, lastMay path & fromMaybe "")
            encode (path, dot) = attributePathToString path <> if dot then "." else ""
            innerCompleter = nestedCompleter decode encode f
        if startsEscape
          then do
            r <- innerCompleter (T.dropEnd 1 input)
            -- Requiring input to be a prefix of the suggestions prevents corrections,
            -- so we only filter when necessary.
            pure
              [ item
                | item@(_, suggestionText) <- r,
                  input `T.isPrefixOf` suggestionText
              ]
          else innerCompleter input
    )

isoCompleter :: (b -> a) -> (a -> b) -> (a -> IO [(CompletionItemOptions, a)]) -> (b -> IO [(CompletionItemOptions, b)])
isoCompleter = nestedCompleter

nestedCompleter :: (a -> b) -> (c -> d) -> (b -> IO [(CompletionItemOptions, c)]) -> (a -> IO [(CompletionItemOptions, d)])
nestedCompleter parse unparse f = fmap (fmap (fmap unparse)) . f . parse

encodeBash :: Text -> Text
encodeBash = toS . f . toS
  where
    f ('"' : s) = '\\' : '"' : f s
    f ('\'' : s) = '\\' : '\'' : f s
    f ('\\' : s) = '\\' : '\\' : f s
    f (' ' : s) = '\\' : ' ' : f s
    f (c : s) = c : f s
    f "" = ""

decodeBash :: Text -> Text
decodeBash = toS . g . toS
  where
    g ('\\' : c : s) = c : g s
    g (c : s) = c : g s
    g "" = ""

ciNixAttributeCompleter :: Optparse.Completer
ciNixAttributeCompleter = mkAttributePathCompleter \(partialPath, partialComponent) -> do
  withNix \_store evalState -> do
    CNix.Verbosity.setVerbosity CNix.Verbosity.Error
    ref <- do
      ref <- liftA2 (<|>) (scanOption "--as-ref") (scanOption "--pretend-ref")
      branch <- liftA2 (<|>) (scanOption "--as-branch") (scanOption "--pretend-branch")
      pure $ refBranchToRef ref branch
    projectMaybe <-
      scanOption "--project" <&> \maybeStr -> do
        s <- maybeStr
        rightToMaybe (runExcept (runReaderT (unReadM projectPathReadM) (toS s)))
    args <- createHerculesCIArgs ref
    runAuthenticated do
      uio <- askUnliftIO
      liftIO $
        getVirtualValueByPath evalState (toS $ GitSource.outPath $ HerculesCIArgs.primaryRepo args) args (resolveInputs uio evalState projectMaybe) (encodeUtf8 <$> partialPath) >>= \case
          Nothing -> pure []
          Just focusValue -> do
            match' evalState focusValue >>= \case
              IsAttrs attrset -> do
                attrs <- getAttrs evalState attrset
                isDeriv <- isDerivation evalState focusValue
                if isDeriv
                  then pure [(mempty {Optparse.cioFiles = False}, (partialPath, False))]
                  else
                    let matches =
                          attrs
                            & M.keys
                            & map decodeUtf8
                            & filter (/= "recurseForDerivations")
                            & filter (T.isPrefixOf partialComponent)
                     in case matches of
                          [singleMatch] -> do
                            ma <- getAttr evalState attrset (encodeUtf8 singleMatch)
                            matchIsDeriv <-
                              ma
                                & traverse (isDerivation evalState)
                                <&> fromMaybe False
                            pure $
                              matches
                                & map (\match -> (mempty {Optparse.cioAddSpace = matchIsDeriv, Optparse.cioFiles = False}, (partialPath ++ [match], not matchIsDeriv)))
                          _ ->
                            pure $
                              matches
                                & map (\match -> (mempty {Optparse.cioAddSpace = False, Optparse.cioFiles = False}, (partialPath ++ [match], False)))
              _ -> pure []

attrByPath :: Ptr EvalState -> RawValue -> [ByteString] -> IO (Maybe RawValue)
attrByPath _ v [] = pure (Just v)
attrByPath evalState v (a : as) = do
  match' evalState v >>= \case
    IsAttrs attrs ->
      getAttr evalState attrs a
        >>= traverse (\attrValue -> attrByPath evalState attrValue as)
        & fmap join
    _ -> pure Nothing

mkTextCompleter :: (Text -> IO [(Optparse.CompletionItemOptions, Text)]) -> Completer
mkTextCompleter f = Optparse.mkCompleterWithOptions (fmap (fmap (uncurry CompletionItem . fmap toS)) . f . toS)

mkImmutableGitInputFlakeThunk :: Ptr EvalState -> API.ImmutableGitInput.ImmutableGitInput -> IO RawValue
mkImmutableGitInputFlakeThunk evalState git = do
  -- TODO: allow picking ssh/http url
  getFlakeFromGit
    evalState
    (API.ImmutableGitInput.httpURL git)
    (API.ImmutableGitInput.ref git)
    (API.ImmutableGitInput.rev git)
