{-# LANGUAGE OverloadedStrings, NoImplicitPrelude  #-}
module SuccessfulRequestLogger(initDB, logRequest, getTopNUserAgents, getTopNUrls) where
import qualified Data.Text as T
import qualified Data.Text.Encoding as E
import qualified Network.Wai as Wai
import qualified Database.SQLite.Simple as SQL
import System.IO (IO)
import Data.Int (Int)
import Prelude ((.), String, error)
import Data.Function (($))
import Control.Monad
import Data.Maybe
import Data.Monoid (mappend)

import Utils (getCurrentSeconds)
import HttpUtils (lookupHeader)
  
unspecifiedUAId :: Int
unspecifiedUAId = 1

unspecifiedUAOriginalName :: T.Text
unspecifiedUAOriginalName = "N/A"

unspecifiedUANormalizedName :: T.Text
unspecifiedUANormalizedName = T.toLower unspecifiedUAOriginalName
  
initDB :: SQL.Connection -> IO ()
initDB conn = do
  mapM_ (SQL.execute_ conn) [userAgentTable, pathTable, accessLogTable]
  SQL.execute conn insertUnspecifiedUserAgent (unspecifiedUAId, unspecifiedUAOriginalName, unspecifiedUANormalizedName)
  where
    userAgentTable :: SQL.Query
    userAgentTable = "CREATE TABLE IF NOT EXISTS UserAgent(\
\                     id INTEGER PRIMARY KEY NOT NULL,\
\                     original TEXT NOT NULL UNIQUE,\
\                     normalized TEXT NOT NULL\
\                     );"
    pathTable :: SQL.Query
    pathTable = "CREATE TABLE IF NOT EXISTS Path(\
\                id INTEGER PRIMARY KEY NOT NULL,\
\                value TEXT NOT NULL UNIQUE\
\);"
    accessLogTable :: SQL.Query
    accessLogTable = "CREATE TABLE IF NOT EXISTS AccessLog(\
\                     id INTEGER PRIMARY KEY NOT NULL,\
\                     userAgentId INTEGER NOT NULL REFERENCES UserAgent(id),\
\                     pathId INTEGER NOT NULL REFERENCES Path(id),\
\                     whenSeconds INTEGER NOT NULL\
\                     );"
    insertUnspecifiedUserAgent :: SQL.Query
    insertUnspecifiedUserAgent = "INSERT OR IGNORE INTO UserAgent (id, original, normalized) VALUES (?, ?, ?);"


type UserAgentId = Int

head :: String -> [a] -> a
head message [] = error message
head _ (x:_) = x

unpackOnly :: SQL.Only a -> a
unpackOnly (SQL.Only x) = x

singleOrThrow :: String -> [SQL.Only a] -> IO a
singleOrThrow name = return . head name . fmap unpackOnly

-- subject to race conditions
findOrCreateUserAgentId :: SQL.Connection -> T.Text -> IO UserAgentId
findOrCreateUserAgentId conn originalName =
  let normalizedName = T.toLower originalName in do
    SQL.execute conn "INSERT OR IGNORE INTO UserAgent (original, normalized) VALUES (?,?);" [originalName, normalizedName]
    result <- SQL.query conn "SELECT id FROM UserAgent WHERE original=?;" (SQL.Only originalName) 
    singleOrThrow "findOrCreateUserAgent" result

type PathId = Int

findOrCreatePathId :: SQL.Connection -> T.Text -> IO PathId
findOrCreatePathId conn path = do
  SQL.execute conn "INSERT OR IGNORE INTO Path (value) VALUES (?);" (SQL.Only path)
  result <- SQL.query conn "SELECT id FROM Path where value = ?;" (SQL.Only path)
  singleOrThrow "findoOrCreatePathId" result

lookupUA :: Wai.Request -> Maybe T.Text
lookupUA request = fmap E.decodeUtf8 $ lookupHeader request "User-Agent"

getUserAgentId :: SQL.Connection -> Wai.Request -> IO UserAgentId
getUserAgentId conn request =
  case lookupUA request of
    Nothing -> return unspecifiedUAId
    Just name -> findOrCreateUserAgentId conn name
    
logRequest :: SQL.Connection -> Wai.Request -> IO ()
logRequest conn request = do
  userAgentId <- getUserAgentId conn request
  pathId <- findOrCreatePathId conn $ "/" `mappend` (T.intercalate "/" $ Wai.pathInfo request)
  currSeconds <- getCurrentSeconds
  SQL.execute conn "INSERT INTO AccessLog (userAgentId, whenSeconds, pathId) VALUES (?, ?, ?);" (userAgentId, currSeconds, pathId)
  return ()

getTopNUserAgents :: SQL.Connection -> Int -> IO [(T.Text, Int)]
getTopNUserAgents conn limit = do
  SQL.query conn "SELECT UserAgent.original, count(userAgentId) AS freq  FROM AccessLog JOIN UserAgent ON AccessLog.userAgentId = UserAgent.id GROUP BY userAgentId ORDER BY freq DESC LIMIT ?;" (SQL.Only limit)

getTopNUrls :: SQL.Connection -> Int -> IO [(T.Text, Int)]
getTopNUrls conn limit = do
  SQL.query conn "SELECT Path.value, count(pathId) AS freq  FROM AccessLog JOIN Path ON AccessLog.pathId = Path.id GROUP BY pathId ORDER BY freq DESC LIMIT ?;" (SQL.Only limit)
