module Network.IPFS.Add
  ( addRaw
  , addFile
  , addPath
  , addDir
  ) where

import           Network.IPFS.Prelude hiding (link)
import           Network.IPFS.Local.Class as IPFS

import           Data.ByteString.Lazy.Char8 as CL
import           Data.List                  as List

import qualified System.FilePath.Glob as Glob

import           RIO.Directory
import           RIO.FilePath
import qualified RIO.ByteString.Lazy as Lazy

import qualified Network.IPFS.Internal.UTF8 as UTF8

import           Network.IPFS.Add.Error      as IPFS.Add
import           Network.IPFS.Types          as IPFS
import           Network.IPFS.DAG.Node.Types as DAG
import           Network.IPFS.DAG.Link       as DAG.Link

import           Network.IPFS.DAG as DAG

addRaw ::
  MonadLocalIPFS m
  => Lazy.ByteString
  -> m (Either IPFS.Add.Error IPFS.CID)
addRaw raw =
  IPFS.runLocal ["add", "-HQ"] raw >>= \case
    Right result ->
      case CL.lines result of
        [cid] ->
          cid
            |> UTF8.textShow
            |> UTF8.stripN 1
            |> mkCID
            |> Right
            |> return

        bad ->
          return . Left . UnexpectedOutput <| UTF8.textShow bad

    Left err ->
      return . Left . UnknownAddErr <| UTF8.textShow err

addFile ::
  MonadLocalIPFS m
  => Lazy.ByteString
  -> IPFS.Name
  -> m (Either IPFS.Add.Error (IPFS.SparseTree, IPFS.CID))
addFile raw name =
  IPFS.runLocal opts raw >>= \case
    Right result ->
      case CL.lines result of
        [inner, outer] ->
          let
            sparseTree  = Directory [(Hash rootCID, fileWrapper)]
            fileWrapper = Directory [(fileName, Content fileCID)]
            rootCID     = CID <| UTF8.textShow outer
            fileCID     = CID . UTF8.stripN 1 <| UTF8.textShow inner
            fileName    = Key name
          in
            return <| Right (sparseTree, rootCID)

        bad ->
          return . Left . UnexpectedOutput <| UTF8.textShow bad


    Left err ->
      return . Left . UnknownAddErr <| UTF8.textShow err

    where
      opts = [ "add"
             , "-wq"
             , "--stdin-name"
             , unName name
             ]

addPath ::
  MonadLocalIPFS m
  => FilePath
  -> m (Either IPFS.Add.Error CID)
addPath path = IPFS.runLocal ["add", "-HQ", path] "" >>= pure . \case
  Right result ->
    case CL.lines result of
      [cid] -> Right . mkCID . UTF8.stripN 1 <| UTF8.textShow cid
      bad   -> Left . UnexpectedOutput <| UTF8.textShow bad

  Left err ->
    Left . UnknownAddErr <| UTF8.textShow err

addDir ::
  ( MonadIO m
  , MonadLocalIPFS m
  )
  => IPFS.Ignored
  -> FilePath
  -> m (Either IPFS.Add.Error IPFS.CID)
addDir ignored path = doesFileExist path >>= \case
  True -> addPath path
  False -> walkDir ignored path

walkDir ::
  ( MonadIO m
  , MonadLocalIPFS m
  )
  => IPFS.Ignored
  -> FilePath
  -> m (Either IPFS.Add.Error IPFS.CID)
walkDir ignored path = do
  files <- listDirectory path

  let
    toAdd = removeIgnored ignored files
    reducer = foldResults path ignored
    seed = Right <| Node
      { dataBlock = "CAE="
      , links = []
      }

  foldM reducer seed toAdd >>= \case
    Left err -> return <| Left err
    Right node -> DAG.putNode node

foldResults ::
  ( MonadIO m
  , MonadLocalIPFS m
  )
  => FilePath
  -> IPFS.Ignored
  -> Either IPFS.Add.Error Node
  -> FilePath
  -> m (Either IPFS.Add.Error Node)
foldResults _ _ (Left err) _ = return <| Left err
foldResults path ignored (Right node) filename = do
  addDir ignored (path </> filename) >>= \case
    Left err ->  return <| Left err
    Right cid ->
      DAG.Link.create cid (IPFS.Name filename) >>= \case
      Left err -> return . Left <| RecursiveAddErr err
      Right link ->
        return <| Right <| node { links = link:(links node) }

removeIgnored :: IPFS.Ignored -> [FilePath] -> [FilePath]
removeIgnored ignored files = List.filter (not . matchesAny ignored) files

matchesAny :: IPFS.Ignored -> FilePath -> Bool
matchesAny globs path = List.any (\x -> Glob.match x path) globs