module Network.Statsd.UdpClient (
  UdpClient
, fromURI
, send
) where

import Control.Monad
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BLazy
import qualified Data.ByteString.Char8 as BC
import Data.ByteString.Lazy.Builder (int64LE, toLazyByteString)
import Data.Byteable
import System.Time
import System.IO.Error
import Crypto.Hash
import Crypto.Random.DRBG
import qualified Network.Socket as Net hiding (send, sendTo, recv, recvFrom)
import qualified Network.Socket.ByteString as Net
import Network.URI

type Hostname = String
type Port = Int
type Key = String
type Namespace = String
type Payload = B.ByteString
type Nonce = B.ByteString

data UdpClient = UdpClient { getSocket :: Net.Socket
                           , getNamespace :: Namespace
                           , getSigningKey :: Maybe Key
                           }

fromURI :: URI -> IO UdpClient
fromURI (URI "statsd:" (Just (URIAuth auth regname port)) path _ _) =
  let regname' = uriRegName' regname
      port' = if null port
              then 8126
              else read $ stripLeading ':' port
      prefix = replace '/' '.' $ stripLeading '/' path
      key = case break (==':') (stripTrailing '@' auth) of
              (u, ':':p) -> Just p
              _          -> Nothing
   in client regname' port' prefix key

  where
    replace :: Eq a => a -> a -> [a] -> [a]
    replace from to list = (\a -> if a == from then to else a) <$> list

    uriRegName' :: String -> String
    uriRegName' = takeWhile (/=']') . dropWhile (=='[')

    stripLeading :: Eq a => a -> [a] -> [a]
    stripLeading x [] = []
    stripLeading x y@(y':ys')
      | x == y'   = ys'
      | otherwise = y

    stripTrailing :: Eq a => a -> [a] -> [a]
    stripTrailing x [] = []
    stripTrailing x xs = let init' = init xs
                             last' = last xs
                          in if last' == x
                             then init'
                             else xs

fromURI uri = error $ "invalid URI" ++ show uri

client :: Hostname -> Port -> Namespace -> Maybe Key -> IO UdpClient
client host port namespace key = do
  (addr:_) <- Net.getAddrInfo Nothing (Just host) (Just $ show port)
  sock <- Net.socket (Net.addrFamily addr) Net.Datagram Net.defaultProtocol
  Net.connect sock (Net.addrAddress addr)

  return $ UdpClient sock namespace key

send :: UdpClient -> String -> IO (Either IOError ())
send client datagram = do
  let namespace = getNamespace client
  let message = if null namespace then datagram else namespace ++ "." ++ datagram
  signedPayload <- signed (getSigningKey client) (BC.pack message)
  tryIOError . void $ Net.send (getSocket client) signedPayload

signed :: Maybe Key -> Payload -> IO Payload
signed Nothing payload = return payload
signed (Just key) payload = do
  (TOD sec _) <- getClockTime
  let timestamp = B.concat . BLazy.toChunks . toLazyByteString . int64LE $ fromIntegral sec

  gen <- newGenIO :: IO CtrDRBG
  let (nonce, _) = throwLeft $ genBytes 4 gen

  let newPayload = B.concat [timestamp, nonce, payload]

  return $ sign key newPayload

sign :: Key -> Payload -> Payload
sign key payload = let keyBytes = BC.pack key
                       signature = toBytes (hmac keyBytes payload :: HMAC SHA256)
                    in B.append signature payload