//
// Created by jonathan.hanks on 2/20/20.
//

#ifndef PUB_SUB2_PUB_INTL_HH
#define PUB_SUB2_PUB_INTL_HH

#include <algorithm>
#include <cstdlib>
#include <deque>
#include <iostream>
#include <map>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <boost/asio.hpp>
#include <boost/smart_ptr/intrusive_ptr.hpp>

#include <cds-pubsub/pub_plugin.hh>
#include "pub_sub_intl.hh"
#include "pipeline_parser.hh"

namespace pub_sub
{
    namespace detail
    {
        /*!
         * @brief The MessageBlock class contains enough information to send
         * a Message and clean up after it.
         * @details MessageBlocks are immutable and only ever shared through
         * MessageBlockPtr.  They hold no links to other blocks, so holding
         * a block pins exactly that block's payload and nothing else.
         */
        class MessageBlock
        {
        public:
            using BuffersType = std::array< boost::asio::const_buffer, 2 >;

            /*!
             * @brief Construct a MessageBlock
             * @param key The application key value
             * @param msg The message to send
             */
            MessageBlock( KeyType key, Message msg )
                : header_{ key, msg.length }, msg_{ std::move( msg ) }
            {
                buffers_[ 0 ] =
                    boost::asio::const_buffer( &header_, sizeof( header_ ) );
                buffers_[ 1 ] =
                    boost::asio::const_buffer( msg_.data( ), msg_.length );
            }
            MessageBlock( const MessageBlock& ) = delete;
            MessageBlock& operator=( const MessageBlock& ) = delete;

            KeyType
            key( ) const noexcept
            {
                return header_.key;
            }

            std::size_t
            size( ) const noexcept
            {
                return msg_.length;
            }

            const void*
            data( ) const noexcept
            {
                return msg_.data( );
            }

            const Message&
            msg( ) const noexcept
            {
                return msg_;
            }

            /*!
             * @brief Return a series of asio buffers to allow the message
             * to be sent.
             * @details 2 buffers are returned, one for the basic header that
             * is sent, and one with the message body.  The buffers are stable
             * for the lifetime of the block.
             * @return A reference to a series of buffers.
             */
            const BuffersType&
            buffers( ) const noexcept
            {
                return buffers_;
            }

        private:
            BlockHeader header_;
            Message     msg_;
            BuffersType buffers_;
        };
        using MessageBlockPtr = std::shared_ptr< const MessageBlock >;

        /*!
         * @brief A bounded history of recently published messages.
         * @details Holds up to capacity shared, immutable MessageBlocks in
         * key order (keys must be pushed in increasing order).  Only
         * MessageBlockPtrs are handed out, never iterators or references,
         * so callers cannot pin more than one block at a time by accident.
         * @note This is not thread safe.
         */
        class MessageHistory
        {
        public:
            explicit MessageHistory( std::size_t capacity )
                : capacity_{ capacity }
            {
            }
            MessageHistory( const MessageHistory& ) = delete;
            MessageHistory( MessageHistory&& ) = delete;
            MessageHistory& operator=( const MessageHistory& ) = delete;
            MessageHistory& operator=( MessageHistory&& ) = delete;

            /*!
             * @brief Add a message to the history, evicting the oldest
             * message if the history is full.
             * @param key The application key value
             * @param msg The message to send
             * @return The newly created block
             */
            MessageBlockPtr
            push( KeyType key, Message msg )
            {
                if ( !blocks_.empty( ) && blocks_.size( ) >= capacity_ )
                {
                    evicted_up_to_ = blocks_.front( )->key( );
                    anything_evicted_ = true;
                    blocks_.pop_front( );
                }
                blocks_.push_back( std::make_shared< const MessageBlock >(
                    key, std::move( msg ) ) );
                return blocks_.back( );
            }

            /*!
             * @brief Find the oldest block with a key greater than key.
             * @return The block, or nullptr if there is none
             */
            MessageBlockPtr
            first_after( KeyType key ) const noexcept
            {
                auto it = std::find_if(
                    blocks_.begin( ),
                    blocks_.end( ),
                    [ key ]( const MessageBlockPtr& cur ) -> bool {
                        return cur->key( ) > key;
                    } );
                return ( it == blocks_.end( ) ? nullptr : *it );
            }

            /*!
             * @brief Find the block with exactly the given key.
             * @return The block, or nullptr if there is none
             */
            MessageBlockPtr
            find( KeyType key ) const noexcept
            {
                auto it = std::find_if(
                    blocks_.begin( ),
                    blocks_.end( ),
                    [ key ]( const MessageBlockPtr& cur ) -> bool {
                        return cur->key( ) == key;
                    } );
                return ( it == blocks_.end( ) ? nullptr : *it );
            }

            /*!
             * @brief Return the most recently pushed block.
             * @return The block, or nullptr if the history is empty
             */
            MessageBlockPtr
            newest( ) const noexcept
            {
                return ( blocks_.empty( ) ? nullptr : blocks_.back( ) );
            }

            /*!
             * @brief Check whether messages after last_key have been evicted.
             * @param last_key The last key a consumer has seen
             * @return True if the consumer can no longer be given a
             * gap-free stream of messages.
             */
            bool
            has_gap_after( KeyType last_key ) const noexcept
            {
                return anything_evicted_ && last_key < evicted_up_to_;
            }

            bool
            empty( ) const noexcept
            {
                return blocks_.empty( );
            }

            std::size_t
            size( ) const noexcept
            {
                return blocks_.size( );
            }

            std::size_t
            capacity( ) const noexcept
            {
                return capacity_;
            }

        private:
            std::deque< MessageBlockPtr > blocks_{ };
            std::size_t                   capacity_;
            KeyType                       evicted_up_to_{ 0 };
            bool                          anything_evicted_{ false };
        };

        /*!
         * @brief Options controlling the publisher history depth.
         */
        struct PublisherOptions
        {
            std::size_t max_depth{ 8 };

            static constexpr std::size_t
                             MIN_DEPTH( )
            {
                return 3;
            }
            static constexpr std::size_t
                             MAX_DEPTH( )
            {
                return 60;
            }

            /*!
             * @brief Parse and validate a max_depth value.
             * @details The single point of validation for max_depth,
             * shared by the environment and connection string paths.
             * @param text The value to parse
             * @return The depth
             * @throws std::runtime_error unless text is a whole-string
             * integer in [MIN_DEPTH, MAX_DEPTH].
             */
            static std::size_t
            parse_max_depth( const std::string& text )
            {
                const char* start = text.c_str( );
                char*       end = nullptr;
                long        depth = std::strtol( start, &end, 10 );
                if ( end == start || *end != '\0' ||
                     depth < static_cast< long >( MIN_DEPTH( ) ) ||
                     depth > static_cast< long >( MAX_DEPTH( ) ) )
                {
                    std::ostringstream os;
                    os << "Invalid max_depth '" << text
                       << "', it must be an integer in " << MIN_DEPTH( ) << "-"
                       << MAX_DEPTH( );
                    throw std::runtime_error( os.str( ) );
                }
                return static_cast< std::size_t >( depth );
            }

            /*!
             * @brief Read options from the environment.
             * @details An invalid PUBLISHER_MAX_DEPTH keeps the default
             * and prints a warning; the environment may be set outside the
             * application's control.
             */
            static PublisherOptions
            from_environment( )
            {
                PublisherOptions options{ };
                const char* depth_env = std::getenv( "PUBLISHER_MAX_DEPTH" );
                if ( depth_env )
                {
                    try
                    {
                        options.max_depth = parse_max_depth( depth_env );
                    }
                    catch ( const std::runtime_error& error )
                    {
                        std::cerr << "PUBLISHER_MAX_DEPTH: " << error.what( )
                                  << ", using the default depth of "
                                  << options.max_depth << std::endl;
                    }
                }
                return options;
            }

            /*!
             * @brief Overlay connection string arguments onto these options.
             * @details Unlike the environment, a connection string is
             * explicit configuration, so anything invalid throws instead of
             * falling back to a default — the connection cannot be made.
             * @param arguments The stage arguments (see parse_stage_address)
             * @return A copy of these options with the overrides applied.
             * @throws std::runtime_error on an unknown key or invalid value.
             */
            PublisherOptions
            with_arguments(
                const std::map< std::string, std::string >& arguments ) const
            {
                PublisherOptions options{ *this };
                for ( const auto& argument : arguments )
                {
                    if ( argument.first == "max_depth" )
                    {
                        options.max_depth = parse_max_depth( argument.second );
                    }
                    else
                    {
                        std::ostringstream os;
                        os << "Unknown publisher option '" << argument.first
                           << "'";
                        throw std::runtime_error( os.str( ) );
                    }
                }
                return options;
            }
        };

        /*!
         * @brief base class for a publisher internal
         */
        struct PublisherIntl
        {
            explicit PublisherIntl( PubDebugNotices* debug )
                : debug_hooks_{ safe_debug( debug ) }
            {
            }

            void
            load_plugin(
                std::shared_ptr< pub_sub::plugins::PublisherPluginApi > plugin )
            {
                plugins_.emplace_back( std::move( plugin ) );
            }
            void
            add_destination( const std::string& address )
            {
                auto pipeline_stages =
                    pub_sub::detail::parse_pipeline( address );

                if ( pipeline_stages.empty( ) )
                {
                    throw std::runtime_error( "Invalid connection string" );
                }
                std::reverse( pipeline_stages.begin( ),
                              pipeline_stages.end( ) );

                pub_sub::plugins::UniquePublisherInstance cur_stage = nullptr;

                for ( const auto& stage_name : pipeline_stages )
                {
                    auto it = std::find_if(
                        plugins_.begin( ),
                        plugins_.end( ),
                        [ &stage_name ](
                            const pub_sub::plugins::SharedPublisherPlugin&
                                plugin ) -> bool {
                            return stage_name.find( plugin->prefix( ) ) == 0;
                        } );
                    if ( it == plugins_.end( ) )
                    {
                        std::ostringstream os;
                        os << "Could not find plugin to handle " << stage_name;
                        throw std::runtime_error( os.str( ) );
                    }
                    cur_stage = ( *it )->publish(
                        stage_name, *debug_hooks_, std::move( cur_stage ) );
                }
                if ( cur_stage == nullptr )
                {
                    throw std::runtime_error( "Unconfigured publisher" );
                }
                publishers_.emplace_back( std::move( cur_stage ) );
            }

            /*!
             * @brief Insert a message to be published
             * @param key
             * @param msg
             * @note This is called from the main application thread and
             * sends its data in a thread safe manner to the background thread
             */
            void
            publish( KeyType key, Message msg )
            {
                std::for_each(
                    publishers_.begin( ),
                    publishers_.end( ),
                    [ key,
                      &msg ]( pub_sub::plugins::UniquePublisherInstance& pub ) {
                        pub->publish( key, msg );
                    } );
            }

        private:
            static PubDebugNotices*
            safe_debug( PubDebugNotices* debug )
            {
                return ( debug ? debug : detail::NullPubDebugNotices( ) );
            }

            std::vector< pub_sub::plugins::UniquePublisherInstance >
                publishers_{ };
            std::vector< pub_sub::plugins::SharedPublisherPlugin > plugins_{ };
            PubDebugNotices* debug_hooks_{ nullptr };
        };

        std::unique_ptr< detail::PublisherIntl >
        create_publisher_intl( PubDebugNotices* debug );

    } // namespace detail
} // namespace pub_sub

#endif // PUB_SUB2_PUB_INTL_HH
