{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}

module Main (main) where

import qualified Algebra.Graph.AdjacencyMap.Algorithm as G
import qualified Algebra.Graph.Labelled.AdjacencyMap as GL
import Args
import Control.Monad (filterM, forM_, unless)
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as LBS
import Data.Conduit.Process (system)
import Data.Containers.ListUtils (nubOrd)
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.List.NonEmpty (toList)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Distribution.ArchHs.Aur (Aur, aurToIO, isInAur)
import Distribution.ArchHs.Core
import Distribution.ArchHs.Exception
import Distribution.ArchHs.ExtraDB
import Distribution.ArchHs.FilesDB
import Distribution.ArchHs.Hackage
import Distribution.ArchHs.Internal.Prelude
import Distribution.ArchHs.Local
import Distribution.ArchHs.Name
import Distribution.ArchHs.Options
import Distribution.ArchHs.PP
import qualified Distribution.ArchHs.PkgBuild as N
import Distribution.ArchHs.Types
import Distribution.ArchHs.Utils
import GHC.IO.Encoding (setLocaleEncoding, utf8)
import Json
import Network.HTTP.Client (Manager)
import Network.HTTP.Client.TLS (newTlsManager)
import System.Directory (createDirectoryIfMissing)
import System.Exit (ExitCode (ExitFailure, ExitSuccess))
import System.FilePath (takeFileName)

app ::
  (Members '[Embed IO, State (Set.Set PackageName), KnownGHCVersion, ExtraEnv, HackageEnv, FlagAssignmentsEnv, DependencyRecord, Trace, Aur, WithMyErr] r) =>
  PackageName ->
  FilePath ->
  Bool ->
  [String] ->
  Bool ->
  Bool ->
  Bool ->
  FilePath ->
  Bool ->
  (DBKind -> IO FilesDB) ->
  Sem r ()
app target path aurSupport skip uusi force installDeps jsonPath noSkipMissing loadFilesDB' = do
  (deps, sublibs, sysDeps) <- getDependencies (fmap mkUnqualComponentName skip) Nothing target

  inExtra <- isInExtra target

  when inExtra $
    if force
      then printWarn $ "Target has been provided by" <+> ppExtra <> comma <+> "but you specified --force"
      else throw $ TargetExist target ByExtra

  when aurSupport $ do
    inAur <- isInAur target
    when inAur $
      if force
        then printWarn $ "Target has been provided by" <+> ppAur <> comma <+> "but you specified --force"
        else throw $ TargetExist target ByAur

  let removeSublibs pkgs =
        pkgs ^.. each . filtered (\x -> x ^. pkgName `notElem` sublibs) & each %~ (\x -> x & pkgDeps %~ filter (\d -> d ^. depName `notElem` sublibs))
      grouped = removeSublibs $ groupDeps deps
      namesFromSolved x = x ^.. each . pkgName <> x ^.. each . pkgDeps . each . depName
      allNames = nubOrd $ namesFromSolved grouped
  extraProvideList <- (<> ghcLibList) <$> filterM (\x -> if x == target && force then return False else isInExtra x) allNames

  let providedPackages = filter (\x -> x ^. pkgName `elem` extraProvideList) grouped
      abnormalDependencies =
        mapMaybe
          ( \x -> case filter (`notElem` extraProvideList) (x ^. pkgDeps ^.. each . depName) of
              [] -> Nothing
              pkgs -> Just (x ^. pkgName, pkgs)
          )
          providedPackages
      -- all missing transitive dependencies, excluding direct dependencies of the target
      missingChildren = mconcat $ snd <$> filter (\x -> fst x /= target) abnormalDependencies

  embed $
    forM_ abnormalDependencies $ \(T.pack . unPackageName -> parent, children) -> do
      printWarn $ "Package" <+> dquotes (pretty parent) <+> "is provided without" <> colon
      forM_ children $ putStrLn . unPackageName

  unless (null abnormalDependencies || noSkipMissing) $
    printWarn "Above package(s) are ignored unless you specify --no-skip-missing"

  let fillProvidedPkgs provideList provider = map (\x -> if (x ^. pkgName) `elem` provideList then ProvidedPackage (x ^. pkgName) provider else x)
      fillProvidedDeps provideList provider = map (pkgDeps %~ each %~ (\y -> if y ^. depName `elem` provideList then y & depProvider ?~ provider else y))
      filledByExtra = fillProvidedPkgs extraProvideList ByExtra . fillProvidedDeps extraProvideList ByExtra $ grouped
      -- after filling extra
      toBePacked1 = filledByExtra ^.. each . filtered (not . isProvided)

  -- always keep the target
  when (target `elem` missingChildren) $
    printWarn "Target is in package(s) above, but we won't skip it"
  -- missingChildren should not appear after the next line
  let missingChildrenExcludedTarget = filter (/= target) missingChildren

  (filledByBoth, toBePacked2) <- do
    when aurSupport $ printInfo "Start searching AUR..."
    aurProvideList <-
      if aurSupport
        then -- after filling aur. toBePacked1 should not appear after the next line
          filterM (\n -> do printInfo ("Searching" <+> viaPretty n); isInAur n) $ filter (\x -> not $ x == target && force) $ toBePacked1 ^.. each . pkgName
        else return []
    let a = fillProvidedPkgs aurProvideList ByAur . fillProvidedDeps aurProvideList ByAur $ filledByExtra
        b = a ^.. each . filtered (not . isProvided)
    return (a, b)

  printInfo "Solved:"
  embed $ T.putStrLn . prettySolvedPkgs $ filledByBoth

  printInfo "Recommended package order:"
  -- remove missingChildren from the graph iff noSkipMissing is not enabled
  let vertexesToBeRemoved = (if noSkipMissing then [] else missingChildrenExcludedTarget) <> filledByBoth ^.. each . filtered isProvided ^.. each . pkgName
      removeSelfCycle g = foldr (\n acc -> GL.removeEdge n n acc) g $ toBePacked2 ^.. each . pkgName
      newGraph = GL.induce (`notElem` vertexesToBeRemoved) deps
  flattened <- case G.topSort . GL.skeleton $ removeSelfCycle newGraph of
    Left c -> throw . CyclicExist $ toList c
    Right x -> return $ filter (`notElem` sublibs) x

  -- after removing missing children
  -- toBePacked1 and toBePacked2 should appear after the next line
  let toBePacked3 = filter (\x -> x ^. pkgName `elem` flattened) toBePacked2

  -- add sign for missing children if we have
  embed . putDoc $ (prettyDeps . reverse $ map (\x -> (x, x `elem` missingChildrenExcludedTarget)) flattened) <> line <> line

  unless (null missingChildrenExcludedTarget || not noSkipMissing) $
    embed . putDoc $
      annotate italicized $
        yellowStarInParens <+> "indicates a missing package" <> line <> line

  let sysDepsToBePacked = Map.filterWithKey (\k _ -> k `elem` flattened) sysDeps

  unless (null sysDepsToBePacked) $ do
    printInfo "Detected pkgconfig or extraLib from target(s):"
    embed $ T.putStrLn $ ppSysDependencies sysDepsToBePacked

  sysDepsRef <- embed . newIORef $ toUnsolved <$> nubOrd (Map.foldMapWithKey (\_ x -> x) sysDepsToBePacked)

  embed $
    isAllSolvedM sysDepsRef >>= \b -> unless b $ do
      printInfo "Now finding corresponding system package(s) using files db:"
      coreFiles <- loadFilesDB' Core
      modifyIORef' sysDepsRef $ fmap (trySolve coreFiles)
      b' <- isAllSolvedM sysDepsRef
      unless b' $ do
        extraFiles <- loadFilesDB' Extra
        modifyIORef' sysDepsRef $ fmap (trySolve extraFiles)

  sysDepsResult <- embed $ readIORef sysDepsRef

  embed . unless (null sysDepsToBePacked) $ do
    printInfo "Done:"
    T.putStrLn . align2col $ ppEmergedSysDep <$> sysDepsResult
    unless (isAllSolved sysDepsResult) $ printWarn "Unable to obtain all required system packages"

  let sysDepsMapping = collectAllSolved sysDepsResult
      getSysDeps name = nubOrd $ catMaybes [sysDepsMapping Map.!? file | (SystemDependency file) <- fromMaybe [] $ sysDeps Map.!? name]

  flags <- filter (\(_, l) -> not $ null l) <$> mapM (\n -> (n,) <$> getPackageFlag n) flattened

  embed $
    unless (null flags) $ do
      printInfo "Detected flag(s) from targets:"
      putDoc $ prettyFlags flags <> line <> line

  let jsonOutput =
        ArchHSOutput
          (fromAbnormalDependency <$> abnormalDependencies)
          (fromSolvedPackage <$> filledByBoth)
          (reverse flattened)
          (fromEmergedSysDep <$> sysDepsResult)
          (fromFlag <$> flags)

  embed $
    unless (null jsonPath) $ do
      LBS.writeFile jsonPath $ A.encode jsonOutput
      printInfo $ "Write file" <> colon <+> pretty jsonPath

  unless (null path) $
    mapM_
      ( \solved -> do
          pkgBuild <- cabalToPkgBuild solved uusi $ getSysDeps (solved ^. pkgName)
          let pName = N._pkgName pkgBuild
              dir = path </> pName
              fileName = dir </> "PKGBUILD"
              txt = N.applyTemplate pkgBuild
          embed $ do
            createDirectoryIfMissing True dir
            writeFile fileName txt
            printInfo $ "Write file" <> colon <+> pretty fileName
      )
      toBePacked3

  when installDeps $ do
    let providedDepends pkg =
          pkg
            ^. pkgDeps
            ^.. each
              . filtered (\x -> depNotMyself (pkg ^. pkgName) x && depNotInGHCLib x && x ^. depProvider == Just ByExtra)
        toStr = unArchLinuxName . toArchLinuxName . _depName
        depends = unwords . nubOrd . fmap toStr . mconcat $ providedDepends <$> toBePacked3
        flattened' = filter (/= target) flattened
    case flattened' of
      [] -> pure ()
      [x] -> printWarn $ "The following dependency is missing in" <+> ppExtra <> colon <+> pretty (unPackageName x)
      xs -> printWarn $ "Following dependencies are missing in" <+> ppExtra <> colon <+> hsep (punctuate comma (pretty . unPackageName <$> xs))
    embed $ putDoc line
    case depends of
      [] -> printInfo "No extra dependency to install"
      xs ->
        embed (system $ "sudo pacman --needed -S " <> xs) >>= \case
          ExitSuccess -> printSuccess "Installed successfully"
          ExitFailure c -> printError $ "pacman exited with" <+> pretty c

-----------------------------------------------------------------------------
data EmergedSysDep = Solved File ArchLinuxName | Unsolved File
  deriving stock (Eq, Ord, Show)

toUnsolved :: SystemDependency -> EmergedSysDep
toUnsolved (SystemDependency x) = Unsolved x

trySolve :: FilesDB -> EmergedSysDep -> EmergedSysDep
trySolve db dep
  | (Unsolved x) <- dep,
    (pkg : _) <- lookupPkg x db =
      Solved x pkg
  | otherwise = dep

isAllSolved :: [EmergedSysDep] -> Bool
isAllSolved xs = null [() | (Unsolved _) <- xs]

isAllSolvedM :: IORef [EmergedSysDep] -> IO Bool
isAllSolvedM ref = isAllSolved <$> readIORef ref

collectAllSolved :: [EmergedSysDep] -> Map.Map File ArchLinuxName
collectAllSolved xs = Map.fromList [(file, name) | (Solved file name) <- xs]

ppEmergedSysDep :: EmergedSysDep -> (Doc AnsiStyle, Doc AnsiStyle)
ppEmergedSysDep (Solved file (ArchLinuxName name)) = (annGreen . pretty $ file, "   ⇒   " <> (annCyan . pretty $ name))
ppEmergedSysDep (Unsolved file) = (annYellow . annBold . pretty $ file, indent 19 cuo)

fromEmergedSysDep :: EmergedSysDep -> SysDepsS
fromEmergedSysDep (Unsolved file) = SysDepsS file Nothing
fromEmergedSysDep (Solved file pkg) = SysDepsS file (Just pkg)

-----------------------------------------------------------------------------

runApp ::
  HackageDB ->
  ExtraDB ->
  Map.Map PackageName FlagAssignment ->
  Bool ->
  FilePath ->
  IORef (Set.Set PackageName) ->
  Manager ->
  Sem '[ExtraEnv, HackageEnv, FlagAssignmentsEnv, DependencyRecord, Trace, State (Set.Set PackageName), Aur, WithMyErr, Embed IO, Final IO] a ->
  IO (Either MyException a)
runApp hackage extra flags traceStdout tracePath ref manager =
  runFinal
    . embedToFinal
    . errorToIOFinal
    . aurToIO manager
    . runStateIORef ref
    . runTrace traceStdout tracePath
    . evalState Map.empty
    . runReader flags
    . runReader hackage
    . runReader extra

runTrace :: (Member (Embed IO) r) => Bool -> FilePath -> Sem (Trace ': r) a -> Sem r a
runTrace stdout path = interpret $ \case
  Trace m -> do
    when stdout (embed $ putStrLn m)
    unless (null path) (embed $ appendFile path (m ++ "\n"))

-----------------------------------------------------------------------------

main :: IO ()
main = printHandledIOException $
  do
    setLocaleEncoding utf8
    Options {..} <- runArgsParser

    unless (null optFileTrace) $ do
      printInfo $ "Trace will be dumped to" <+> pretty optFileTrace
      writeFile optFileTrace ""

    unless (null optJson) $ do
      printInfo $ "Output will be dumped to" <+> pretty optJson <+> "as json"
      writeFile optJson ""

    let isFlagEmpty = Map.null optFlags
        isSkipEmpty = null optSkip

    unless isFlagEmpty $ do
      printInfo "You assigned flags:"
      putDoc $ prettyFlagAssignments optFlags <> line <> line

    unless isSkipEmpty $ do
      printInfo "You chose to skip:"
      putDoc $ prettySkip optSkip <> line <> line

    when optAur $ printInfo "You specified -a, searching AUR may takes a long time"

    when optUusi $ printInfo "You specified --uusi, uusi will become makedepends of each package"

    hackage <- loadHackageDBFromOptions optHackage

    let isExtraEmpty = null optExtraCabalDirs
    optExtraCabal <- mapM findCabalFile optExtraCabalDirs

    unless isExtraEmpty $
      printInfo $
        "You added" <+> hsep (punctuate comma $ pretty . takeFileName <$> optExtraCabal) <+> "as extra cabal file(s), starting parsing right now"

    parsedExtra <- mapM parseCabalFile optExtraCabal

    let newHackage = foldr insertDB hackage parsedExtra

    extra <- loadExtraDBFromOptions optExtraDB

    printInfo "Start running..."

    ref <- newIORef Set.empty

    manager <- newTlsManager

    runApp
      newHackage
      extra
      optFlags
      optStdoutTrace
      optFileTrace
      ref
      manager
      (subsumeGHCVersion $ app optTarget optOutputDir optAur optSkip optUusi optForce optInstallDeps optJson optNoSkipMissing (loadFilesDBFromOptions optFilesDB))
      & printAppResult

-----------------------------------------------------------------------------

groupDeps :: GL.AdjacencyMap (Set.Set DependencyType) PackageName -> [SolvedPackage]
groupDeps graph =
  fmap
    ( \(name, deps) ->
        SolvedPackage name $ fmap (uncurry . flip $ SolvedDependency Nothing) deps
    )
    $ result <> aloneChildren
  where
    result =
      fmap ((\(a, b, c) -> (head b, zip a c)) . unzip3)
        . groupBy (\x y -> uncurry (==) (getTwo _2 x y))
        . fmap (_1 %~ Set.toList)
        . GL.edgeList
        $ graph
    parents = fmap fst result
    children = mconcat $ fmap (\(_, ds) -> fmap snd ds) result
    -- Maybe 'G.vertexSet' is a better choice
    aloneChildren = nubOrd $ zip (filter (`notElem` parents) children) (repeat [])
