//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012- OpenVPN Inc.
//
//    SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
//

// Client tun interface for Linux.

#ifndef OPENVPN_TUN_LINUX_CLIENT_TUNCLI_H
#define OPENVPN_TUN_LINUX_CLIENT_TUNCLI_H

#include <openvpn/asio/asioerr.hpp>
#include <openvpn/common/cleanup.hpp>
#include <openvpn/common/scoped_fd.hpp>
#include <openvpn/tun/builder/setup.hpp>
#include <openvpn/tun/tunio.hpp>
#include <openvpn/tun/persist/tunpersist.hpp>
#include <openvpn/tun/linux/client/tunmethods.hpp>

namespace openvpn::TunLinux {

struct PacketFrom
{
    typedef std::unique_ptr<PacketFrom> SPtr;
    BufferAllocated buf;
};

template <typename ReadHandler>
class Tun : public TunIO<ReadHandler, PacketFrom, openvpn_io::posix::stream_descriptor>
{
    typedef TunIO<ReadHandler, PacketFrom, openvpn_io::posix::stream_descriptor> Base;

  public:
    typedef RCPtr<Tun> Ptr;

    Tun(openvpn_io::io_context &io_context,
        ReadHandler read_handler_arg,
        const Frame::Ptr &frame_arg,
        const SessionStats::Ptr &stats_arg,
        const int socket,
        const std::string &name)
        : Base(read_handler_arg, frame_arg, stats_arg)
    {
        Base::name_ = name;
        Base::retain_stream = true;
        Base::stream = new openvpn_io::posix::stream_descriptor(io_context, socket);
        OPENVPN_LOG_TUN(Base::name_ << " opened");
    }

    ~Tun()
    {
        Base::stop();
    }
};

typedef TunPersistTemplate<ScopedFD> TunPersist;

class ClientConfig : public TunClientFactory
{
  public:
    typedef RCPtr<ClientConfig> Ptr;

    std::string dev_name;
    int txqueuelen = 200;

    TunProp::Config tun_prop;

    bool generate_tun_builder_capture_event = false;

    int n_parallel = 8;
    Frame::Ptr frame;
    SessionStats::Ptr stats;

    TunBuilderSetup::Factory::Ptr tun_setup_factory;
    TunPersist::Ptr tun_persist;

    void load(const OptionList &opt)
    {
        // set a default MTU
        if (!tun_prop.mtu)
            tun_prop.mtu = TUN_MTU_DEFAULT;

        // parse "dev" option
        if (dev_name.empty())
        {
            const Option *dev = opt.get_ptr("dev");
            if (dev)
                dev_name = dev->get(1, 64);
        }
    }

    static Ptr new_obj()
    {
        return new ClientConfig;
    }

    TunClient::Ptr new_tun_client_obj(openvpn_io::io_context &io_context,
                                      TunClientParent &parent,
                                      TransportClient *transcli) override;

    TunBuilderSetup::Base::Ptr new_setup_obj()
    {
        if (tun_setup_factory)
            return tun_setup_factory->new_setup_obj();
        else
            return new TunLinuxSetup::Setup<TUN_LINUX>();
    }

    bool supports_epoch_data() override
    {
        /* The normal tun implementation that uses the internal data channel */
        return true;
    }

  private:
    ClientConfig()
    {
    }
};

class Client : public TunClient
{
    friend class ClientConfig;                                                      // calls constructor
    friend class TunIO<Client *, PacketFrom, openvpn_io::posix::stream_descriptor>; // calls tun_read_handler

    typedef Tun<Client *> TunImpl;

  public:
    void tun_start(const OptionList &opt, TransportClient &transcli, CryptoDCSettings &) override
    {
        if (!impl)
        {
            halt = false;

            if (config->tun_persist)
            {
                OPENVPN_LOG("TunPersist: long-term session scope");
                tun_persist = config->tun_persist; // long-term persistent
            }
            else
            {
                OPENVPN_LOG("TunPersist: short-term connection scope");
                tun_persist.reset(new TunPersist(true, TunWrapObjRetain::NO_RETAIN, nullptr)); // short-term
            }

            try
            {
                const IP::Addr server_addr = transcli.server_endpoint_addr();

                int sd = -1;

                // Check if persisted tun session matches properties of to-be-created session
                if (tun_persist->use_persisted_tun(server_addr, config->tun_prop, opt))
                {
                    state = tun_persist->state();
                    sd = tun_persist->obj();
                    state = tun_persist->state();
                    OPENVPN_LOG("TunPersist: reused tun context");
                }
                else
                {
                    // notify parent
                    parent.tun_pre_tun_config();

                    // close old tun handle if persisted
                    tun_persist->close();

                    // parse pushed options
                    TunBuilderCapture::Ptr po(new TunBuilderCapture());
                    TunProp::configure_builder(po.get(),
                                               state.get(),
                                               config->stats.get(),
                                               server_addr,
                                               config->tun_prop,
                                               opt,
                                               nullptr,
                                               false);

                    OPENVPN_LOG("CAPTURED OPTIONS:" << std::endl
                                                    << po->to_string());

                    // create new tun setup object
                    tun_setup = config->new_setup_obj();

                    // create config object for tun setup layer
                    TunLinuxSetup::Setup<TUN_LINUX>::Config tsconf;
                    tsconf.layer = config->tun_prop.layer;
                    tsconf.dev_name = config->dev_name;
                    tsconf.txqueuelen = config->txqueuelen;
                    tsconf.add_bypass_routes_on_establish = true;

                    // open/config tun
                    {
                        std::ostringstream os;
                        auto os_print = Cleanup([&os]()
                                                { OPENVPN_LOG_STRING(os.str()); });
                        sd = tun_setup->establish(*po, &tsconf, nullptr, os);
                    }

#if defined(HAVE_JSON)
                    if (config->generate_tun_builder_capture_event)
                    {
                        // create an event with TunBuilderCapture data as JSON
                        parent.tun_event(new ClientEvent::InfoJSON("TUN_BUILDER_CAPTURE", po->to_json()));
                    }
#endif

                    // persist tun settings state
                    state->iface_name = tsconf.iface_name;
                    tun_persist->persist_tun_state(sd, state);

                    // enable tun_setup destructor
                    tun_persist->add_destructor(tun_setup);
                }

                // start tun
                impl.reset(new TunImpl(io_context,
                                       this,
                                       config->frame,
                                       config->stats,
                                       sd,
                                       state->iface_name));
                impl->start(config->n_parallel);

                // signal that we are connected
                parent.tun_connected();
            }
            catch (const std::exception &e)
            {
                if (tun_persist)
                    tun_persist->close();

                stop();
                parent.tun_error(Error::TUN_SETUP_FAILED, e.what());
            }
        }
    }

    bool tun_send(BufferAllocated &buf) override
    {
        return send(buf);
    }

    std::string tun_name() const override
    {
        if (impl)
            return impl->name();
        else
            return "UNDEF_TUN";
    }

    std::string vpn_ip4() const override
    {
        if (state->vpn_ip4_addr.specified())
            return state->vpn_ip4_addr.to_string();
        else
            return "";
    }

    std::string vpn_ip6() const override
    {
        if (state->vpn_ip6_addr.specified())
            return state->vpn_ip6_addr.to_string();
        else
            return "";
    }

    std::string vpn_gw4() const override
    {
        if (state->vpn_ip4_gw.specified())
            return state->vpn_ip4_gw.to_string();
        else
            return "";
    }

    std::string vpn_gw6() const override
    {
        if (state->vpn_ip6_gw.specified())
            return state->vpn_ip6_gw.to_string();
        else
            return "";
    }

    int vpn_mtu() const override
    {
        return state->mtu;
    }

    void set_disconnect() override
    {
    }

    void stop() override
    {
        stop_();
    }
    virtual ~Client()
    {
        stop_();
    }

    void apply_push_update(const OptionList &opt, TransportClient &transcli) override
    {
        stop_();
        if (impl)
        {
            impl.reset();
        }

        // this has to be done in post, because we need to wait
        // for all async tun read requests to complete
        openvpn_io::post(io_context, [self = Ptr(this), opt, &transcli]
                         {
            OPENVPN_ASYNC_HANDLER;
            CryptoDCSettings c{};
            self->tun_start(opt, transcli, c); });
    }

  private:
    Client(openvpn_io::io_context &io_context_arg,
           ClientConfig *config_arg,
           TunClientParent &parent_arg)
        : io_context(io_context_arg),
          config(config_arg),
          parent(parent_arg),
          state(new TunProp::State()),
          halt(false)
    {
    }

    bool send(Buffer &buf)
    {
        if (impl)
            return impl->write(buf);
        else
            return false;
    }

    void tun_read_handler(PacketFrom::SPtr &pfp) // called by TunImpl
    {
        parent.tun_recv(pfp->buf);
    }

    void tun_error_handler(const Error::Type errtype, // called by TunImpl
                           const openvpn_io::error_code *error)
    {
    }

    void stop_()
    {
        if (!halt)
        {
            halt = true;

            // stop tun
            if (impl)
                impl->stop();

            tun_persist.reset();
        }
    }

    openvpn_io::io_context &io_context;
    TunPersist::Ptr tun_persist;
    ClientConfig::Ptr config;
    TunClientParent &parent;
    TunImpl::Ptr impl;
    TunProp::State::Ptr state;
    TunBuilderSetup::Base::Ptr tun_setup;
    bool halt;
};

inline TunClient::Ptr ClientConfig::new_tun_client_obj(openvpn_io::io_context &io_context,
                                                       TunClientParent &parent,
                                                       TransportClient *transcli)
{
    return TunClient::Ptr(new Client(io_context, this, parent));
}

} // namespace openvpn::TunLinux

#endif // OPENVPN_TUN_LINUX_CLIENT_TUNCLI_H
