{-# LANGUAGE ScopedTypeVariables #-}

module Data.JsonSchema.Fetch where

import           Control.Arrow            (left)
import           Control.Exception        (catch)
import qualified Data.ByteString.Lazy     as LBS
import qualified Data.ByteString          as BS
import qualified Data.HashMap.Strict      as H
import qualified Data.Text                as T
import           Network.HTTP.Client

import           Data.Validator.Reference (resolveReference,
                                           updateResolutionScope)
import           Import

-- For GHCs before 7.10:
import           Prelude                  hiding (concat, sequence)

--------------------------------------------------
-- * Types
--------------------------------------------------

data Spec schema = Spec
  { _ssEmbedded :: schema -> [schema]
  , _ssGetId    :: schema -> Maybe Text
  , _ssGetRef   :: schema -> Maybe Text
  }

data SchemaWithURI schema = SchemaWithURI
  { _swSchema :: !schema
  , _swURI    :: !(Maybe Text)
  -- ^ Must not include a URI fragment, e.g. use
  -- "http://example.com/foo" not "http://example.com/foo#bar".
  --
  -- This is the URI identifying the document containing the schema.
  -- It's different than the schema's "id" field, which controls scope
  -- when resolving references contained in the schema.

  -- TODO: Make the no URI fragment requirement unnecessary.
  } deriving (Eq, Show)

-- | Keys are URIs (without URI fragments).
type URISchemaMap schema = HashMap Text schema

data ReferencedSchemas schema = ReferencedSchemas
  { _rsStarting  :: !schema
  -- ^ Used to resolve relative references.
  , _rsSchemaMap :: !(URISchemaMap schema)
  } deriving (Eq, Show)

--------------------------------------------------
-- * Fetch via HTTP
--------------------------------------------------

data HTTPFailure
  = HTTPParseFailure   Text
  | HTTPRequestFailure HttpException
  deriving Show

-- | Take a schema. Retrieve every document either it or its subschemas
-- include via the "$ref" keyword.
referencesViaHTTP'
  :: forall schema. FromJSON schema
  => Spec schema
  -> SchemaWithURI schema
  -> IO (Either HTTPFailure (ReferencedSchemas schema))
referencesViaHTTP' spec sw = do
  manager <- newManager defaultManagerSettings
  let f = referencesMethodAgnostic (get manager) spec sw
  catch (left HTTPParseFailure <$> f) handler
  where
    get :: Manager -> Text -> IO LBS.ByteString
    get man url = do
      request <- parseUrl (T.unpack url)
      responseBody <$> httpLbs request man

    handler
      :: HttpException
      -> IO (Either HTTPFailure (ReferencedSchemas schema))
    handler = pure . Left . HTTPRequestFailure

--------------------------------------------------
-- * Fetch via Filesystem
--------------------------------------------------

data FilesystemFailure
  = FSParseFailure Text
  | FSReadFailure  IOError
  deriving Show

referencesViaFilesystem'
  :: forall schema. FromJSON schema
  => Spec schema
  -> SchemaWithURI schema
  -> IO (Either FilesystemFailure (ReferencedSchemas schema))
referencesViaFilesystem' spec sw = catch (left FSParseFailure <$> f) handler
  where
    f :: IO (Either Text (ReferencedSchemas schema))
    f = referencesMethodAgnostic readFile' spec sw

    readFile' :: Text -> IO LBS.ByteString
    readFile' = fmap LBS.fromStrict . BS.readFile . T.unpack

    handler
      :: IOError
      -> IO (Either FilesystemFailure (ReferencedSchemas schema))
    handler = pure . Left . FSReadFailure

--------------------------------------------------
-- * Method Agnostic Fetching Tools
--------------------------------------------------

-- | A version of 'fetchReferencedSchema's where the function to fetch
-- schemas is provided by the user. This allows restrictions to be added,
-- e.g. rejecting non-local URIs.
referencesMethodAgnostic
  :: forall schema. FromJSON schema
  => (Text -> IO LBS.ByteString)
  -> Spec schema
  -> SchemaWithURI schema
  -> IO (Either Text (ReferencedSchemas schema))
referencesMethodAgnostic fetchRef spec sw =
  (fmap.fmap) (ReferencedSchemas (_swSchema sw))
              (foldFunction fetchRef spec mempty sw)

foldFunction
  :: forall schema. FromJSON schema
  => (Text -> IO LBS.ByteString)
  -> Spec schema
  -> URISchemaMap schema
  -> SchemaWithURI schema
  -> IO (Either Text (URISchemaMap schema))
foldFunction fetchRef spec@(Spec _ _ getRef) referenced sw =
  foldlM f (Right referenced) (includeSubschemas spec sw)
  where
    f :: Either Text (URISchemaMap schema)
      -> SchemaWithURI schema
      -> IO (Either Text (URISchemaMap schema))
    f (Left e) _                            = pure (Left e)
    f (Right g) (SchemaWithURI schema mUri) =
      case newRef of
        Nothing  -> pure (Right g)
        Just uri -> do
          bts <- fetchRef uri
          case eitherDecode bts of
            Left e     -> pure . Left . T.pack $ e
            Right schm -> foldFunction fetchRef spec (H.insert uri schm g)
                                       (SchemaWithURI schm (Just uri))
      where
        newRef :: Maybe Text
        newRef
          | Just (Just uri,_) <- resolveReference mUri <$> getRef schema
            = case H.lookup uri g of
                Nothing -> Just uri
                Just _  -> Nothing
          | otherwise = Nothing

-- | Return the schema passed in as an argument, as well as every
-- subschema contained within it.
includeSubschemas
  :: forall schema.
     Spec schema
  -> SchemaWithURI schema
  -> [SchemaWithURI schema]
includeSubschemas spec@(Spec embedded getId _) (SchemaWithURI schema mUri) =
  SchemaWithURI schema mUri
  : (includeSubschemas spec =<< subSchemas)
  where
    subSchemas :: [SchemaWithURI schema]
    subSchemas =
      (\a -> SchemaWithURI a (updateResolutionScope mUri (getId schema)))
        <$> embedded schema