{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where

import Control.Applicative
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Trans.Maybe
import Control.Monad.Writer hiding ((<>))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Char
import Data.Digest.Pure.SHA
import Data.Foldable
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import Data.Semigroup
import qualified Data.Set as Set
import Data.Traversable
import Network.HTTP.Conduit hiding (path)
import Network.HTTP.Types.Status
import System.Directory
import System.Environment
import System.Exit
import System.FilePath
import System.IO
import System.IO.Temp
import System.Process
import Text.XML.Light

data HavenEnv = HavenEnv
  { _havenEnv_manager :: Manager
  , _havenEnv_repos :: Map String String
  , _havenEnv_m2Local :: FilePath
  }

-- | Takes multiple maven package descriptions as command line arguments
-- and finds the dependencies of those maven packages.
-- Package descriptions should be of the form @groupid:artifactid:version@
main :: IO ()
main = do
  mgr <- newManager tlsManagerSettings
  [pomXml] <- getArgs
  Just repos <- fmap parseRepos . parseXMLDoc <$> BS.readFile pomXml

  withSystemTempFile "out.txt" $ \tmpFile hTmpFile -> withSystemTempDirectory "m2" $ \m2Repo -> do
    let havenEnv = HavenEnv mgr repos m2Repo
    hProc <- runProcess
      "mvn"
      ["-f", pomXml, "dependency:tree", "-Dverbose", "-DoutputFile=" <> tmpFile, "-Dmaven.repo.local=" <> m2Repo]
      Nothing
      Nothing
      Nothing
      (Just stderr)
      Nothing
    ExitSuccess <- waitForProcess hProc

    _ <- hGetLine hTmpFile -- skip local package name
    (_, mavenNixs) <- runWriterT $ fix $ \loop -> do
      e <- liftIO $ hIsEOF hTmpFile
      unless e $ do
        line <- liftIO $ hGetLine hTmpFile
        let mavenGrArTyVr = dropWhile (not . isAlphaNum) line -- Skip leading symbols; we don't care about parsing this
            (groupId, ':':mavenArTyVr) = break (==':') mavenGrArTyVr
            (artifactId, ':':mavenTyVr) = break (==':') mavenArTyVr
            (fileType, ':':mavenVr) = break (==':') mavenTyVr -- File type that Maven has decided on
            version = takeWhile (/=':') mavenVr -- Version number
            maven = Maven groupId artifactId version
        unless (all isSpace line) $ do
          mMvnNix <- runReaderT (runMaybeT $ fetch fileType maven) havenEnv
          case mMvnNix of
            Just mvnNix -> tell $ Set.fromList mvnNix
            Nothing -> liftIO $ do
              hPutStrLn stderr $ "Failed for " <> unlines [show fileType, show maven]
              exitFailure
          loop
    putStrLn "["
    traverse_ (putStrLn . toNix) mavenNixs
    putStrLn "]"

parseRepos :: Element -> Map String String
parseRepos pom = Map.fromList $ do
  repoList <- findChildrenByTagName "repositories" pom
  repo <- findChildrenByTagName "repository" repoList
  repoId <- findChildrenByTagName "id" repo
  repoUrl <- findChildrenByTagName "url" repo
  return (strContent repoId, strContent repoUrl)

data Maven = Maven
  { _maven_groupId :: String
  , _maven_artifactId :: String
  , _maven_version :: String
  }
  deriving (Show, Read, Eq, Ord)

data MavenNix = MavenNix
  { _mavenNix_maven :: Maven
  , _mavenNix_repo :: String
  , _mavenNix_jarSha256 :: Maybe (Digest SHA256State)
  , _mavenNix_pomSha256 :: Maybe (Digest SHA256State)
  , _mavenNix_aarSha256 :: Maybe (Digest SHA256State)
  }
  deriving (Show, Eq, Ord)

-- | Create a nix record for a hashed maven package
toNix :: MavenNix -> String
toNix m =
  let mvn = _mavenNix_maven m
      showHash h = fromMaybe "null" $ (\x -> "\"" <> x <> "\"") . showDigest <$> h
  in unlines
      [ "  { artifactId = \"" <> _maven_artifactId mvn <> "\";"
      , "    groupId = \"" <> _maven_groupId mvn <> "\";"
      , "    version = \"" <> _maven_version mvn <> "\";"
      , "    repo = \"" <> _mavenNix_repo m <> "\";"
      , "    jarSha256 = " <> showHash (_mavenNix_jarSha256 m) <> ";"
      , "    pomSha256 = " <> showHash (_mavenNix_pomSha256 m) <> ";"
      , "    aarSha256 = " <> showHash (_mavenNix_aarSha256 m) <> "; }"
      ]

-- | Gets the repo with the given id, calling 'empty' when it's not present
getRepo :: (MonadReader HavenEnv m, MonadPlus m) => String -> m String
getRepo repoId = do
  repos <- asks _havenEnv_repos
  maybe empty pure $ Map.lookup repoId repos

m2Directory :: Maven -> String
m2Directory mvn = foldl (</>) ""
  [ (\x -> if x == '.' then '/' else x) <$> _maven_groupId mvn
  , _maven_artifactId mvn
  , _maven_version mvn
  ]

-- | Gets a given artifact for a 'Maven' and hashes it. It will first
-- check the local m2 dir, and then it will try to download it from
-- the online repo. If both fail, an error is logged to 'stderr', and
-- 'empty' is called.
getArtifactFile
  :: (MonadIO m, MonadPlus m, MonadReader HavenEnv m)
  => Maven
  -> String
  -> String
  -> m BL.ByteString
getArtifactFile mvn ext repo = do
  mgr <- asks _havenEnv_manager
  m2Repo <- asks _havenEnv_m2Local
  let m2Dir = m2Directory mvn
      m2Filename = _maven_artifactId mvn <> "-" <> _maven_version mvn <> ext
      path = m2Repo </> m2Dir </> m2Filename
  m2ArtifactExists <- liftIO $ doesFileExist path
  if m2ArtifactExists then liftIO (BL.readFile path) else do
    let url = repo </> m2Dir </> m2Filename
    req <- liftIO $ parseRequest url
    liftIO $ hPutStrLn stderr $ "Getting URL: " <> url
    rsp <- liftIO $ httpLbs req mgr
    when (responseStatus rsp /= status200) $ do
      liftIO $ hPutStrLn stderr $ "Failed to get URL: " <> url
      empty
    return $ responseBody rsp

-- | Hash a particular maven package's .pom and .jar files and parse the .pom file as xml
fetch
  :: (MonadIO m, MonadReader HavenEnv m)
  => String
  -> Maven
  -> MaybeT m [MavenNix]
fetch fileType mvn = do
  m2Repo <- asks _havenEnv_m2Local
  let m2Dir = m2Repo </> m2Directory mvn
      findRepoId = takeWhile (/='=') . drop 1 . dropWhile (/='>')

  repoId <- findRepoId <$> liftIO (readFile (m2Dir </> "_remote.repositories"))
  repo <- getRepo repoId

  pom <- runMaybeT $ getArtifactFile mvn ".pom" repo

  let noArtifacts = MavenNix
        { _mavenNix_maven = mvn
        , _mavenNix_repo = repo
        , _mavenNix_jarSha256 = Nothing
        , _mavenNix_pomSha256 = sha256 <$> pom
        , _mavenNix_aarSha256 = Nothing
        }

  parents <- fmap (fromMaybe []) $ for pom $ \pomContents -> do
    pomEl <- maybe empty pure $ parseXMLDoc pomContents
    fmap mconcat $ traverse (fetch "pom") $ do
      parent <- findChildrenByTagName "parent" pomEl
      groupId <- strContent <$> findChildrenByTagName "groupId" parent
      artifactId <- strContent <$> findChildrenByTagName "artifactId" parent
      version <- strContent <$> findChildrenByTagName "version" parent
      return $ Maven groupId artifactId version

  -- TODO: Match the 'type' to the correct file extension.
  -- The extension is _usually_ equal to the type, but it's not necessarily.
  -- See: https://maven.apache.org/pom.html#Dependencies
  mavenNix <- asum
    [ do
      guard (fileType == "jar")
      jarSha <- sha256 <$> getArtifactFile mvn ".jar" repo
      return $ noArtifacts { _mavenNix_jarSha256 = Just jarSha }
    , do
      guard (fileType == "aar")
      aarSha <- sha256 <$> getArtifactFile mvn ".aar" repo
      return $ noArtifacts { _mavenNix_aarSha256 = Just aarSha }
    , do
      guard (fileType == "pom") -- This is used when getting parents
      return noArtifacts
    ]
  return (mavenNix:parents)

-- | Retrieve an XML Element's children by tag name
findChildrenByTagName :: String -> Element -> [Element]
findChildrenByTagName n = filterChildren (\a -> qName (elName a) == n)

firstChildByTagName :: String -> Element -> Maybe Element
firstChildByTagName n = listToMaybe . findChildrenByTagName n