#ifndef INCLUDED_BOBCAT_MILTER_
#define INCLUDED_BOBCAT_MILTER_

#include <sys/types.h>
#include <sys/stat.h>
#include <libmilter/mfapi.h>
#include <unordered_map>
#include <string>

namespace FBB
{

class Milter
{
    static std::string s_name;
    static Milter *s_mp;
    static bool s_callClose;

    typedef std::unordered_map<SMFICTX *, Milter *>::iterator iterator;
    static std::unordered_map<SMFICTX *, Milter *> s_map;

    SMFICTX *d_ctx;                 // for local use only

    public:
        virtual ~Milter();          // empty

        typedef size_t callback_set;
        enum CallBack
        {
            CONNECT     = 1 << 0,
            HELO        = 1 << 1,
            SENDER      = 1 << 2,
            RECIPIENT   = 1 << 3,
            HEADER      = 1 << 4,
            EOH         = 1 << 5,
            BODY        = 1 << 6,
            EOM         = 1 << 7,
            ABORT       = 1 << 8,
            CLOSE       = 1 << 9,
            UNKNOWN     = 1 << 10, // version > 2
            DATA        = 1 << 11, // version > 3

            ALL_CALLBACKS = (DATA << 1) - 1
        };

        typedef unsigned long flag_set;
        enum Flags
        {
            NO_FLAGS = 0L,
            ADD_HEADERS     = SMFIF_ADDHDRS,    // filter may add headers
            ADD_RECIPIENTS  = SMFIF_ADDRCPT,    // filter may add recipients
            CHANGE_BODY     = SMFIF_CHGBODY,    // filter may replace body
            CHANGE_HEADERS  = SMFIF_CHGHDRS,    // filter may change/delete 
                                                // headers
            DELETE_RECIPIENTS = SMFIF_DELRCPT,  // filter may delete recipients
            QUARANTINE      = SMFIF_QUARANTINE, // filter may quarantine 
                                                // envelope

            ALL_FLAGS   =   ADD_HEADERS         | ADD_RECIPIENTS    |    
                            CHANGE_BODY         | CHANGE_HEADERS    |
                            DELETE_RECIPIENTS   | QUARANTINE
                            
        };

        enum Status
        {
            ACCEPT  = SMFIS_ACCEPT,
            CONTINUE = SMFIS_CONTINUE,
            DISCARD = SMFIS_DISCARD,
            REJECT = SMFIS_REJECT,
            TEMPFAIL = SMFIS_TEMPFAIL,
        };

        enum Return
        {
            FAILURE = MI_FAILURE,
            SUCCESS = MI_SUCCESS,
        };

        static void initialize(std::string const &name, Milter &milter,
                callback_set callbacks = CONNECT, flag_set flags = NO_FLAGS);

        static bool start()
        {
            return smfi_main() == MI_SUCCESS;
        }

        static void stop()
        {
            smfi_stop();
        }

        static std::string const &name()
        {
            return s_name;
        }

    protected:
        Milter() = default;
        Milter(Milter const &other) = delete;

        virtual sfsistat abort();

        bool addHeader(std::string const &hdrName, 
                        std::string const &hdrValue)
        {
            return smfi_addheader(d_ctx, 
                            const_cast<char *>(hdrName.c_str()), 
                            const_cast<char *>(hdrValue.c_str())) 
                    == SUCCESS;
        }
                                    // from eom() only
                                    // hdr-name: without :
                                    // hdr-value: \n and \t ok.
        bool addRecipient(std::string const &rcptName)
        {
            return smfi_addrcpt(d_ctx, 
                    const_cast<char *>(rcptName.c_str())) == SUCCESS;
        }
        virtual sfsistat body(unsigned char *text, size_t length);

        bool changeHeader(std::string const &hdrName, 
                        unsigned headerNr, std::string const &hdrValue)
        {
            return smfi_chgheader(d_ctx, 
                        const_cast<char *>(hdrName.c_str()), 
                        headerNr, 
                        hdrValue.length() ? 
                            const_cast<char *>(hdrValue.c_str())
                        :
                            0
                    ) == SUCCESS;
        }
        virtual Milter *clone() const = 0;       // cloning required
        virtual sfsistat close();
        virtual sfsistat connect(char *hostname, _SOCK_ADDR *hostaddr);
#if SMFI_VERSION > 3
        virtual sfsistat data();
#endif
        bool deleteRecipient(std::string const &rcptName)
        {
            return smfi_delrcpt(d_ctx, 
                    const_cast<char *>(rcptName.c_str())) == SUCCESS;
        }
        virtual sfsistat eoh();
        virtual sfsistat eom();
        virtual sfsistat header(char *headerf, char *headerv);
        virtual sfsistat helo(char *helohost);
        SMFICTX *id() const
        {
            return d_ctx;
        }

        bool insertHeader(size_t hdrIdx, std::string const &hdrName, 
                          std::string const &hdrValue)
        {
            return smfi_insheader(d_ctx, hdrIdx,
                    const_cast<char *>(hdrName.c_str()), 
                    const_cast<char *>(hdrValue.c_str()));
        }
        static bool openSocket(bool removeIfTrue = true)
        {
            return smfi_opensocket(removeIfTrue) == SUCCESS;
        }
        bool quarantine(std::string const &reason)
        {
            return  smfi_quarantine(d_ctx, 
                    const_cast<char *>(reason.c_str())) == SUCCESS;
        }
        virtual sfsistat recipient(char **argv);
        bool replaceBody(std::string const &body)
        {
            return smfi_replacebody(d_ctx, 
                        reinterpret_cast<unsigned char *>
                        (
                            const_cast<char *>(body.c_str())
                        ),
                        body.length()) == SUCCESS;
        }
        virtual sfsistat sender(char **argv);
        static bool setBacklog(size_t backlog = 5)
        {
            return smfi_setbacklog(backlog) == SUCCESS;
        }

        static void setConnection(std::string const &socketName)
        {
            smfi_setconn(const_cast<char *>(socketName.c_str()));
        }
        bool setReply(std::string const &rcode, std::string const &xcode = "",
                      std::string const &msg = "")
        {
            return smfi_setreply(d_ctx, 
                        const_cast<char *>(rcode.c_str()),
                        xcode.length() ? 
                            const_cast<char *>(xcode.c_str()) 
                        :
                            0,
                        const_cast<char *>(msg.c_str()));

                // rcode: The three-digit (RFC 821/2821) SMTP reply code, as a
                // null-terminated string. rcode cannot be NULL, and must be a
                // valid 4XX or 5XX reply code.

                // xxcode The extended (RFC 1893/2034) reply code. If xcode is
                // NULL, no extended code is used. Otherwise, xcode must
                // conform to RFC 1893/2034.  

    

                // * Values passed to smfi_setreply are not checked for
                // standards compliance.

                // * The message parameter should contain only printable
                // characters, other characters may lead to undefined
                // behavior. For example, CR or LF will cause the call to
                // fail, single '%' characters will cause the text to be
                // ignored (if there really should be a '%' in the string, use
                // '%%' just like for printf(3)).

                // * If the reply code (rcode) given is a '4XX' code but
                // SMFI_REJECT is used for the message, the custom reply is
                // not used.

                // * Similarly, if the reply code (rcode) given is a '5XX'
                // code but SMFI_TEMPFAIL is used for the message, the custom
                // reply is not used.

                // Note: in neither of the last two cases an error is returned
                // to the milter, libmilter silently ignores the reply code.

                // * If the milter returns SMFI_TEMPFAIL and sets the reply
                // code to '421', then the SMTP server will terminate the SMTP
                // session with a 421 error code.
        }
        // can't do smfi_setmlreply(ctx, ...) 
        // since there is no smfi_Vsetmlreply(ctx, ...)
        static void setTimeout(size_t seconds = 7210)
        {
            smfi_settimeout(seconds);
        }
        char const *symval(std::string const &name) const
        {
            return smfi_getsymval(d_ctx, 
                    const_cast<char *>(name.c_str()));
        }
#if SMFI_VERSION > 2
        sfsistat unknown(char const *ptr);
#endif
        bool wait()                           // from eom() only
        {
            return smfi_progress(d_ctx) == SUCCESS;
        }

    private:
        static Milter *install(SMFICTX *ctx);

        static sfsistat mConnect(SMFICTX *ctx, char *hostname, 
                                             _SOCK_ADDR *hostaddr)
        {
            return install(ctx)->connect(hostname, hostaddr);
        }

        static sfsistat mAbort(SMFICTX *ctx)
        {
            return install(ctx)->abort();
        }
        static sfsistat mBody(SMFICTX *ctx, unsigned char *body, size_t len)
        {
            return install(ctx)->body(body, len);
        }
        static sfsistat mSender(SMFICTX *ctx, char **argv)
        {
            return install(ctx)->sender(argv);
        }
        static sfsistat mRecipient(SMFICTX *ctx, char **argv)
        {
            return install(ctx)->recipient(argv);
        }
        static sfsistat mEoh(SMFICTX *ctx)
        {
            return install(ctx)->eoh();
        }
        static sfsistat mEom(SMFICTX *ctx)
        {
            return install(ctx)->eom();
        }
        static sfsistat mHeader(SMFICTX *ctx, char *headerf, char *headerv)
        {
            return install(ctx)->header(headerf, headerv);
        }
        static sfsistat mHelo(SMFICTX *ctx, char *helohost)
        {
            return install(ctx)->helo(helohost);
        }
        static sfsistat mClose(SMFICTX *ctx);

#if SMFI_VERSION > 2
        static sfsistat mUnknown(SMFICTX *ctx, char const *ptr)
        {
            return install(ctx)->unknown(ptr);
        }
#endif /* SMFI_VERSION > 2 */

#if SMFI_VERSION > 3
        static sfsistat mData(SMFICTX *ctx)
        {
            return install(ctx)->data();
        }
#endif /* SMFI_VERSION > 3 */
};

}

#endif
