{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-
The code in this module is modified from that found in the
broch-server/broch.hs file in the 'broch' library, which is under the
following copyright and license:

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

Copyright (c) 2014, Luke Taylor

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. Neither the name of the author nor the names of his contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-}
module ExampleProvider where

import ClassyPrelude
import ExampleProviderOpts

import Broch.Model
import Broch.Server
import Broch.Server.Config
import Broch.Server.Internal
import Broch.Server.Session (defaultKey, defaultLoadSession)
import qualified Broch.SQLite as BS
import Broch.URI
import Crypto.KDF.BCrypt (hashPassword)
import Data.Aeson
import qualified Data.Map as M
import Data.Pool (createPool, withResource)
import qualified Database.SQLite.Simple as SQLite
import Network.Wai.Application.Static (defaultWebAppSettings, staticApp)
import Network.Wai.Handler.Warp (run)
import Network.Wai.Middleware.RequestLogger (logStdoutDev)
import System.Directory
import qualified Web.Routing.Combinators as R
import qualified Web.Routing.SafeRouting as R
import Yesod.Auth.OIDC (ClientId(..), ClientSecret(..))

-- Adapted from Broch.SQLite
toJSONField :: ToJSON a => Maybe a -> SQLite.SQLData
toJSONField = maybe SQLite.SQLNull (SQLite.SQLText . decodeUtf8 . toStrict . encode)

-- Adapted from Broch.SQLite
insertClient :: SQLite.Connection -> Client -> IO ()
insertClient conn Client{..} =
    void $ SQLite.execute conn "INSERT INTO oauth2_client (id, secret, redirect_uri, allowed_scope, authorized_grant_types, access_token_validity, refresh_token_validity, auth_method, auth_alg, keys_uri, keys, id_token_algs, user_info_algs, request_obj_algs, sector_identifier, auto_approve) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" ((clientId, clientSecret, redirectURIs, allowedScope, authorizedGrantTypes, accessTokenValidity, refreshTokenValidity, clientAuthMethodName tokenEndpointAuthMethod, fmap tshow tokenEndpointAuthAlg, clientKeysUri) SQLite.:. (toJSONField clientKeys, toJSONField idTokenAlgs, toJSONField userInfoAlgs, toJSONField requestObjAlgs, sectorIdentifier, autoapprove))

initDB :: BrochOptions -> SQLite.Connection -> IO ()
initDB BrochOptions{..} c = do
  void $ flip M.traverseWithKey boClients $ \(ClientId clientId) (ClientSecret secret, host, callback) ->
    insertClient c $ Client
      { clientId = clientId
      , clientSecret = Just secret
      , authorizedGrantTypes = [AuthorizationCode]
      , redirectURIs = case parseURI callback of
          Right url -> [url]
          Left e -> error $ "Can't initialise tests due to bad callback URL: " <> show e
      , accessTokenValidity = 3600
      , refreshTokenValidity = 7200
      , allowedScope = [OpenID, Profile, Email]
      , autoapprove = False
      , tokenEndpointAuthMethod = ClientSecretPost
      , tokenEndpointAuthAlg    = Nothing -- :: Maybe JwsAlg
      , clientKeysUri  = Nothing -- :: Maybe Text
      , clientKeys     = Just [] -- :: Maybe [Jwk]
      , idTokenAlgs    = Nothing -- :: Maybe AlgPrefs
      , userInfoAlgs   = Nothing -- :: Maybe AlgPrefs
      , requestObjAlgs = Nothing -- :: Maybe AlgPrefs
      , sectorIdentifier = host
      }
  void $ flip M.traverseWithKey boUsers $ \userId (emailAddr, pw) -> do
    pwHash :: ByteString <- hashPassword 6 (encodeUtf8 pw)
    void $ SQLite.execute c "INSERT OR REPLACE INTO op_user VALUES (?, ?, ?, 'key')" ((userId, userId, pwHash))
    void $ SQLite.execute c "INSERT OR REPLACE INTO user_info VALUES (?, 'name', 'first', 'last', 'middle', 'nick', 'name', 'http://placeholder', 'http://placeholder', 'http://placeholder', ?, 0, null, '2000-01-01', 'Europe/Paris', 'en-US', '+33 12 34 56 78', 0, '25 My Street, Village, 1234567, France', '25 My Street', 'Vilage', 'Shire', '1234567', 'EN', datetime('now'))" ((userId, emailAddr))


runBroch :: BrochOptions -> IO ()
runBroch opts@BrochOptions{..} = do
  sessionKey <- defaultKey
  kr <- defaultKeyRing
  rotateKeys kr True
  let dbFile = "broch.sqlite3"
  dbExists <- doesFileExist dbFile
  when dbExists $ removeFile dbFile
  pool <- createPool (SQLite.open dbFile) SQLite.close 1 60 20
  withResource pool BS.createSchema
  config <- BS.sqliteBackend pool <$> inMemoryConfig boIssuerUri kr Nothing
  let app = staticApp (defaultWebAppSettings "webroot")
      baseRouter = brochServer config defaultApprovalPage authenticatedSubject authenticateSubject
      authenticate username password = pure $ M.lookup username boUsers >>= \case
        (_, pw) | pw == password -> Just username
        _ -> Nothing
      extraRoutes =
          [ ("/home",   text "Hello, I'm the home page")
          , ("/login",  passwordLoginHandler defaultLoginPage authenticate)
          , ("/logout", invalidateSession >> text "You have been logged out")
          ]
      router = foldl' (\pathMap (r, h) -> R.insertPathMap' (R.toInternalPath (R.static r)) (const h) pathMap) baseRouter extraRoutes
      broch = routerToMiddleware (defaultLoadSession 3600 sessionKey) boIssuerUri router

  withResource pool $ initDB opts

  run boPort (logStdoutDev (broch app))