{-# LANGUAGE TemplateHaskell #-}

module JSONSchema.Draft4
    ( -- * Draft 4 Schema
      SchemaWithURI(..)
    , Schema(..)
    , SC.emptySchema

      -- * One-step validation (getting references over HTTP)
    , fetchHTTPAndValidate
    , HTTPValidationFailure(..)
    , FE.HTTPFailure(..)
    , SchemaInvalid(..)

      -- * One-step validation (getting references from the filesystem)
    , fetchFilesystemAndValidate
    , FilesystemValidationFailure(..)
    , FE.FilesystemFailure(..)

      -- * Validation failure
    , Invalid(..)
    , ValidatorFailure(..)

      -- * Fetching tools
    , URISchemaMap(..)
    , referencesViaHTTP
    , referencesViaFilesystem

      -- * Other Draft 4 things exported just in case
    , metaSchema
    , metaSchemaBytes
    , schemaValidity
    , referencesValidity
    , checkSchema
    , draft4FetchInfo
    ) where

import           Import

import qualified Data.ByteString           as BS
import           Data.FileEmbed            (embedFile,
                                            makeRelativeToProject)
import qualified Data.HashMap.Strict       as HM
import qualified Data.List.NonEmpty        as NE
import           Data.Maybe                (fromMaybe)

import           JSONSchema.Draft4.Failure (Invalid(..),
                                            SchemaInvalid(..),
                                            ValidatorFailure(..))
import           JSONSchema.Draft4.Schema  (Schema)
import qualified JSONSchema.Draft4.Schema  as SC
import qualified JSONSchema.Draft4.Spec    as Spec
import           JSONSchema.Fetch          (SchemaWithURI(..),
                                            URISchemaMap(..))
import qualified JSONSchema.Fetch          as FE

data HTTPValidationFailure
    = HVRequest FE.HTTPFailure
    | HVSchema  SchemaInvalid
    | HVData    Invalid
    deriving Show

-- | Fetch recursively referenced schemas over HTTP, check that both the
-- original and referenced schemas are valid, then validate then data.
fetchHTTPAndValidate
    :: SchemaWithURI Schema
    -> Value
    -> IO (Either HTTPValidationFailure ())
fetchHTTPAndValidate sw v = do
    res <- referencesViaHTTP sw
    pure (g =<< f =<< first HVRequest res)
  where
    f :: FE.URISchemaMap Schema
      -> Either HTTPValidationFailure (Value -> [ValidatorFailure])
    f references = first HVSchema (checkSchema references sw)

    g :: (Value -> [ValidatorFailure]) -> Either HTTPValidationFailure ()
    g val = case NE.nonEmpty (val v) of
                Nothing       -> Right ()
                Just failures -> Left (HVData Invalid
                                     { _invalidSchema   = _swSchema sw
                                     , _invalidInstance = v
                                     , _invalidFailures = failures
                                     })

data FilesystemValidationFailure
    = FVRead   FE.FilesystemFailure
    | FVSchema SchemaInvalid
    | FVData   Invalid
    deriving (Show, Eq)

-- | Fetch recursively referenced schemas from the filesystem, check
-- that both the original and referenced schemas are valid, then validate
-- the data.
fetchFilesystemAndValidate
    :: SchemaWithURI Schema
    -> Value
    -> IO (Either FilesystemValidationFailure ())
fetchFilesystemAndValidate sw v = do
    res <- referencesViaFilesystem sw
    pure (g =<< f =<< first FVRead res)
  where
    f :: FE.URISchemaMap Schema
      -> Either FilesystemValidationFailure (Value -> [ValidatorFailure])
    f references = first FVSchema (checkSchema references sw)

    g :: (Value -> [ValidatorFailure]) -> Either FilesystemValidationFailure ()
    g val = case NE.nonEmpty (val v) of
                Nothing      -> Right ()
                Just invalid -> Left (FVData Invalid
                                    { _invalidSchema   = _swSchema sw
                                    , _invalidInstance = v
                                    , _invalidFailures = invalid
                                    })

-- | An instance of 'JSONSchema.Fetch.FetchInfo' specialized for
-- JSON Schema Draft 4.
draft4FetchInfo :: FE.FetchInfo Schema
draft4FetchInfo = FE.FetchInfo Spec.embedded SC._schemaId SC._schemaRef

-- | Fetch the schemas recursively referenced by a starting schema over HTTP.
referencesViaHTTP
    :: SchemaWithURI Schema
    -> IO (Either FE.HTTPFailure (FE.URISchemaMap Schema))
referencesViaHTTP = FE.referencesViaHTTP' draft4FetchInfo

-- | Fetch the schemas recursively referenced by a starting schema from
-- the filesystem.
referencesViaFilesystem
    :: SchemaWithURI Schema
    -> IO (Either FE.FilesystemFailure (FE.URISchemaMap Schema))
referencesViaFilesystem = FE.referencesViaFilesystem' draft4FetchInfo

-- | Checks if a schema and a set of referenced schemas are valid.
--
-- Return a function to validate data.
checkSchema
    :: FE.URISchemaMap Schema
    -> SchemaWithURI Schema
    -> Either SchemaInvalid (Value -> [ValidatorFailure])
checkSchema sm sw =
    case NE.nonEmpty failures of
        Just fs -> Left (SchemaInvalid fs)
        Nothing -> Right (Spec.specValidate sm sw)
  where
    failures :: [(Maybe Text, NonEmpty ValidatorFailure)]
    failures =
        let refFailures = first Just <$> referencesValidity sm
        in case NE.nonEmpty (schemaValidity (_swSchema sw)) of
                     Nothing   -> refFailures
                     Just errs -> (Nothing,errs) : refFailures

metaSchema :: Schema
metaSchema =
      fromMaybe (panic "Schema decode failed (this should never happen)")
    . decodeStrict
    $ metaSchemaBytes

metaSchemaBytes :: BS.ByteString
metaSchemaBytes =
    $(makeRelativeToProject "src/draft4.json" >>= embedFile)

-- | Check that a schema itself is valid
-- (if so the returned list will be empty).
schemaValidity :: Schema -> [ValidatorFailure]
schemaValidity =
    Spec.specValidate schemaMap (SchemaWithURI metaSchema Nothing) . toJSON
  where
    schemaMap :: URISchemaMap Schema
    schemaMap =
        URISchemaMap (HM.singleton "http://json-schema.org/draft-04/schema"
                                   metaSchema)

-- | Check that a set of referenced schemas are valid
-- (if so the returned list will be empty).
referencesValidity
  :: FE.URISchemaMap Schema
  -> [(Text, NonEmpty ValidatorFailure)]
  -- ^ The first item of the tuple is the URI of a schema, the second
  -- is that schema's validation errors.
referencesValidity = HM.foldlWithKey' f mempty . FE._unURISchemaMap
  where
    f :: [(Text, NonEmpty ValidatorFailure)]
      -> Text
      -> Schema
      -> [(Text, NonEmpty ValidatorFailure)]
    f acc k v = case NE.nonEmpty (schemaValidity v) of
                    Nothing   -> acc
                    Just errs -> (k,errs) : acc