module Web.Postie.Protocol(
    TlsStatus(..)
  , Mailbox
  , Event(..)
  , Command(..)
  , SmtpFSM
  , Reply
  , initSmtpFSM
  , step
  , reply
  , reply'
  , renderReply

  , parseCommand
  , parseHelo
  , parseMailFrom
  ) where

import Prelude hiding (takeWhile)

import Web.Postie.Address

import Data.Attoparsec.Char8
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS

import Control.Applicative

data TlsStatus = Active | Forbidden | Permitted | Required deriving (Eq)

data SessionState = Unknown
                  | HaveHelo
                  | HaveEhlo
                  | HaveMailFrom
                  | HaveRcptTo
                  | HaveData
                  | HaveQuit

type Mailbox = Address

data Event =  SayHelo BS.ByteString
           |SayHeloAgain BS.ByteString
           | SayEhlo BS.ByteString
           | SayEhloAgain BS.ByteString
           | SayOK
           | SetMailFrom Mailbox
           | AddRcptTo Mailbox
           | StartData
           | WantTls
           | WantReset
           | WantQuit
           | TlsAlreadyActive
           | TlsNotSupported
           | NeedStartTlsFirst
           | NeedHeloFirst
           | NeedMailFromFirst
           | NeedRcptToFirst
           deriving (Eq, Show)

data Command = Helo BS.ByteString
             | Ehlo BS.ByteString
             | MailFrom Mailbox
             | RcptTo Mailbox
             | StartTls
             | Data
             | Rset
             | Quit
             deriving (Eq, Show)

newtype SmtpFSM = SmtpFSM { step :: Command -> TlsStatus -> (Event, SmtpFSM) }

initSmtpFSM :: SmtpFSM
initSmtpFSM = SmtpFSM (handleSmtpCmd Unknown)

handleSmtpCmd :: SessionState -> Command -> TlsStatus -> (Event, SmtpFSM)
handleSmtpCmd st cmd tlsSt = match tlsSt st cmd
  where
    match :: TlsStatus -> SessionState -> Command -> (Event, SmtpFSM)
    match _         HaveQuit  _            = undefined
    match _         HaveData  Data         = undefined
    match _         _         Quit         = trans (HaveQuit, WantQuit)
    match _         Unknown   (Helo x)     = trans (HaveHelo, SayHelo x)
    match _         _         (Helo x)     = event(SayHeloAgain x)
    match _         Unknown   (Ehlo x)     = trans (HaveEhlo, SayEhlo x)
    match _         _         (Ehlo x)     = event (SayEhloAgain x)
    match Required  _         (MailFrom _) = event NeedStartTlsFirst
    match _         Unknown   (MailFrom _) = event NeedHeloFirst
    match _         _         (MailFrom x) = trans (HaveMailFrom, SetMailFrom x)
    match Required  _         (RcptTo _)   = event NeedStartTlsFirst
    match _         Unknown   (RcptTo _)   = event NeedHeloFirst
    match _         HaveHelo  (RcptTo _)   = event NeedMailFromFirst
    match _         HaveEhlo  (RcptTo _)   = event NeedMailFromFirst
    match _         _         (RcptTo x)   = trans (HaveRcptTo, AddRcptTo x)
    match Required  _            Data      = event NeedStartTlsFirst
    match _         Unknown      Data      = event NeedHeloFirst
    match _         HaveHelo     Data      = event NeedMailFromFirst
    match _         HaveEhlo     Data      = event NeedMailFromFirst
    match _         HaveMailFrom Data      = event NeedRcptToFirst
    match _         HaveRcptTo   Data      = trans (HaveData, StartData)
    match Required  _           Rset       = event NeedStartTlsFirst
    match _         _           Rset       = trans (HaveHelo, WantReset)
    match Active    _           StartTls   = event TlsAlreadyActive
    match Forbidden _           StartTls   = event TlsNotSupported
    match _         _           StartTls   = trans (Unknown, WantTls)

    event :: Event -> (Event, SmtpFSM)
    event e = (e, SmtpFSM (handleSmtpCmd st))

    trans :: (SessionState, Event) -> (Event, SmtpFSM)
    trans (st', e) = (e, SmtpFSM (handleSmtpCmd st'))


type StatusCode = Int

data Reply = Reply StatusCode [LBS.ByteString]

reply :: StatusCode -> LBS.ByteString -> Reply
reply c s = reply' c [s]

reply' :: StatusCode -> [LBS.ByteString] -> Reply
reply' = Reply

renderReply :: Reply -> LBS.ByteString
renderReply (Reply code msgs) = LBS.concat msg'
  where
    prefixCon = LBS.pack (show code ++ "-")
    prefixEnd = LBS.pack (show code ++ " ")
    fmt p l = LBS.concat [p, l, "\r\n"]
    (x:xs) = reverse msgs
    msgCon = map (fmt prefixCon) xs
    msgEnd = fmt prefixEnd x
    msg' = reverse (msgEnd:msgCon)

parseCommand :: Parser Command
parseCommand = commands <* crlf
  where
    commands = choice [
                        parseQuit
                      , parseData
                      , parseRset
                      , parseHelo
                      , parseEhlo
                      , parseStartTls
                      , parseMailFrom
                      , parseRcptTo
                      ]

crlf :: Parser ()
crlf = char '\r' >> char '\n' >> return ()

parseHello :: (BS.ByteString -> Command) -> BS.ByteString -> Parser Command
parseHello f s = f `fmap` parser
  where
    parser = stringCI s *> char ' ' *> takeWhile (notInClass "\r ")

parseHelo :: Parser Command
parseHelo = parseHello Helo "helo"

parseEhlo :: Parser Command
parseEhlo = parseHello Ehlo "ehlo"

parseMailFrom :: Parser Command
parseMailFrom = stringCI "mail from:<" *> (MailFrom `fmap` addrSpec) <* char '>'

parseRcptTo :: Parser Command
parseRcptTo = stringCI "rcpt to:<" *> (RcptTo `fmap` addrSpec) <* char '>'

parseStartTls :: Parser Command
parseStartTls = stringCI "starttls" *> pure StartTls

parseRset :: Parser Command
parseRset = stringCI "rset" *> pure Rset

parseData :: Parser Command
parseData = stringCI "data" *> pure Data

parseQuit :: Parser Command
parseQuit = stringCI "quit" *> pure Quit