//    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
//

// Query/Set VPC routes, requires this policy:
//
// {
//     "Version": "2012-10-17",
//     "Statement": [
//         {
//             "Sid": "Stmt1478633458000",
//             "Effect": "Allow",
//             "Action": [
//                 "ec2:CreateRoute",
//                 "ec2:DescribeNetworkInterfaceAttribute",
//                 "ec2:DescribeNetworkInterfaces",
//                 "ec2:DescribeRouteTables",
//                 "ec2:ModifyNetworkInterfaceAttribute",
//                 "ec2:ReplaceRoute",
//                 "ec2:DeleteRoute"
//             ],
//             "Resource": [
//                 "*"
//             ]
//         }
//     ]
// }
//
// Creating and tagging route tables also require these policy actions:
//
//     ec2:CreateRouteTable
//     ec2:CreateTags
//

#pragma once

#include <openvpn/common/xmlhelper.hpp>
#include <openvpn/aws/awshttp.hpp>
#include <openvpn/aws/awspc.hpp>
#include <openvpn/aws/awsrest.hpp>

namespace openvpn::AWS {
class Route
{
  public:
    OPENVPN_EXCEPTION(aws_route_error);

    enum class RouteTargetType
    {
        INTERFACE_ID,
        INSTANCE_ID
    };

    class Context
    {
      public:
        Context(PCQuery::Info instance_info_arg,
                Creds creds_arg,
                StrongRandomAPI::Ptr rng,
                Stop *async_stop_arg,
                const int debug_level)
            : instance_info(std::move(instance_info_arg)),
              http_context(std::move(rng), debug_level),
              ts(http_context.transaction_set(ec2_host(instance_info))),
              creds(std::move(creds_arg)),
              async_stop(async_stop_arg)
        {
        }

        void reset()
        {
            if (ts)
                ts->hsc.reset();
        }

        std::string instance_id() const
        {
            return instance_info.instanceId;
        }

      private:
        friend class Route;
        PCQuery::Info instance_info;
        HTTPContext http_context;
        WS::ClientSet::TransactionSet::Ptr ts;
        Creds creds;
        Stop *async_stop;
    };

    // Query network_interface_id and route_table_id
    // from EC2 API.
    class Info
    {
      public:
        Info(std::string network_interface_id_arg,
             std::string route_table_id_arg)
            : network_interface_id(std::move(network_interface_id_arg)),
              route_table_id(std::move(route_table_id_arg))
        {
        }

        Info(Context &ctx)
        {
            // AWS IDs local to this constructor
            std::string subnet_id;

            // first request -- get the AWS network interface
            {
                // create API query
                {
                    REST::Query q;
                    q.emplace_back("Action", "DescribeNetworkInterfaces");
                    q.emplace_back("Filter.1.Name", "attachment.instance-id");
                    q.emplace_back("Filter.1.Value.1", ctx.instance_info.instanceId);
                    q.emplace_back("Filter.2.Name", "addresses.private-ip-address");
                    q.emplace_back("Filter.2.Value.1", ctx.instance_info.privateIp);
                    add_transaction(ctx, std::move(q));
                }

                // do transaction
                execute_transaction(ctx);

                // process reply
                {
                    // get the transaction
                    WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

                    // get reply
                    const std::string reply = t.content_in_string();

                    // check the reply status
                    if (!t.http_status_success())
                        OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaces: " << t.format_status(*ctx.ts));

                    // parse XML reply
                    const Xml::Document doc(reply, "DescribeNetworkInterfaces");
                    const tinyxml2::XMLElement *item = Xml::find(&doc,
                                                                 "DescribeNetworkInterfacesResponse",
                                                                 "networkInterfaceSet",
                                                                 "item");
                    if (!item)
                        OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaces: cannot locate <item> tag in returned XML:\n"
                                                           << reply);
                    network_interface_id = Xml::find_text(item, "networkInterfaceId");
                    vpc_id = Xml::find_text(item, "vpcId");
                    subnet_id = Xml::find_text(item, "subnetId");
                    if (network_interface_id.empty() || vpc_id.empty() || subnet_id.empty())
                        OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaces: cannot locate one of networkInterfaceId, vpcId, or subnetId in returned XML:\n"
                                                           << reply);
                }
            }

            // second request -- get the VPC routing table
            {
                // create API query
                {
                    REST::Query q;
                    q.emplace_back("Action", "DescribeRouteTables");
                    q.emplace_back("Filter.1.Name", "vpc-id");
                    q.emplace_back("Filter.1.Value.1", vpc_id);
                    q.emplace_back("Filter.2.Name", "association.subnet-id");
                    q.emplace_back("Filter.2.Value.1", subnet_id);
                    add_transaction(ctx, std::move(q));
                }

                // do transaction
                execute_transaction(ctx);

                // process reply
                {
                    // get the transaction
                    WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

                    // get reply
                    const std::string reply = t.content_in_string();

                    // check the reply status
                    if (!t.http_status_success())
                        OPENVPN_THROW(aws_route_error, "DescribeRouteTables: " << t.format_status(*ctx.ts) << '\n'
                                                                               << reply);

                    // parse XML reply
                    const Xml::Document doc(reply, "DescribeRouteTables");
                    route_table_id = Xml::find_text(&doc,
                                                    "DescribeRouteTablesResponse",
                                                    "routeTableSet",
                                                    "item",
                                                    "routeTableId");
                    if (route_table_id.empty())
                        OPENVPN_THROW(aws_route_error, "DescribeRouteTables: cannot locate routeTableId in returned XML:\n"
                                                           << reply);
                }
            }
        }

        std::string to_string() const
        {
            return '[' + network_interface_id + '/' + route_table_id + ']';
        }

        std::string network_interface_id;
        std::string route_table_id;
        std::string vpc_id;
    };

    // Set sourceDestCheck flag on AWS network interface.
    static void set_source_dest_check(Context &ctx,
                                      const std::string &network_interface_id,
                                      const bool source_dest_check)
    {
        const std::string sdc = source_dest_check ? "true" : "false";

        // first get the attribute and see if it is already set
        // the way we want it
        {
            REST::Query q;
            q.emplace_back("Action", "DescribeNetworkInterfaceAttribute");
            q.emplace_back("NetworkInterfaceId", network_interface_id);
            q.emplace_back("Attribute", "sourceDestCheck");
            add_transaction(ctx, std::move(q));
        }

        // do transaction
        execute_transaction(ctx);

        // process reply
        {
            // get the transaction
            WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

            // get reply
            const std::string reply = t.content_in_string();

            // check the reply status
            if (!t.http_status_success())
                OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaceAttribute: " << t.format_status(*ctx.ts) << '\n'
                                                                                     << reply);

            // parse XML reply
            const Xml::Document doc(reply, "DescribeNetworkInterfaceAttribute");
            const std::string retval = Xml::find_text(&doc,
                                                      "DescribeNetworkInterfaceAttributeResponse",
                                                      "sourceDestCheck",
                                                      "value");
            // already set to desired value?
            if (retval == sdc)
                return;
        }

        // create API query
        {
            REST::Query q;
            q.emplace_back("Action", "ModifyNetworkInterfaceAttribute");
            q.emplace_back("NetworkInterfaceId", network_interface_id);
            q.emplace_back("SourceDestCheck.Value", sdc);
            add_transaction(ctx, std::move(q));
        }

        // do transaction
        execute_transaction(ctx);

        // process reply
        {
            // get the transaction
            WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

            // get reply
            const std::string reply = t.content_in_string();

            // check the reply status
            if (!t.http_status_success())
                OPENVPN_THROW(aws_route_error, "ModifyNetworkInterfaceAttribute: " << t.format_status(*ctx.ts) << '\n'
                                                                                   << reply);

            // parse XML reply
            const Xml::Document doc(reply, "ModifyNetworkInterfaceAttribute");
            const std::string retval = Xml::find_text(&doc,
                                                      "ModifyNetworkInterfaceAttributeResponse",
                                                      "return");
            if (retval != "true")
                OPENVPN_THROW(aws_route_error, "ModifyNetworkInterfaceAttribute: returned failure status: " << '\n'
                                                                                                            << reply);

            OPENVPN_LOG("AWS EC2 ModifyNetworkInterfaceAttribute " << network_interface_id << " SourceDestCheck.Value=" << sdc);
        }
    }

    static void delete_route(Context &ctx,
                             const std::string &route_table_id,
                             const std::string &cidr,
                             bool ipv6)
    {
        {
            REST::Query q;
            q.emplace_back("Action", "DeleteRoute");
            q.emplace_back(ipv6 ? "DestinationIpv6CidrBlock" : "DestinationCidrBlock", cidr);
            q.emplace_back("RouteTableId", route_table_id);
            add_transaction(ctx, std::move(q));
        }

        // do transaction
        execute_transaction(ctx);

        // process reply
        {
            // get the transaction
            WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

            // get reply
            const std::string reply = t.content_in_string();

            // check the reply status
            if (!t.http_status_success())
                OPENVPN_THROW(aws_route_error, "DeleteRoute: " << t.format_status(*ctx.ts) << '\n'
                                                               << reply);

            // parse XML reply
            const Xml::Document doc(reply, "DeleteRoute");
            const std::string retval = Xml::find_text(&doc,
                                                      "DeleteRouteResponse",
                                                      "return");
            if (retval != "true")
                OPENVPN_THROW(aws_route_error, "DeleteRoute: returned failure status: " << '\n'
                                                                                        << reply);

            OPENVPN_LOG("AWS EC2 DeleteRoute " << cidr << " -> table " << route_table_id);
        }
    }

    static std::string create_route_table(Context &ctx,
                                          const std::string &vpc_id,
                                          const std::string &name)
    {
        std::string route_table_id;

        // create route table
        {
            REST::Query q;
            q.emplace_back("Action", "CreateRouteTable");
            q.emplace_back("VpcId", vpc_id);
            add_transaction(ctx, std::move(q));
        }

        execute_transaction(ctx);

        // process reply
        {
            // get the transaction
            WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

            auto reply = t.content_in_string();

            // check the reply status
            if (!t.http_status_success())
                OPENVPN_THROW(aws_route_error, "CreateRouteTable: " << t.format_status(*ctx.ts) << '\n'
                                                                    << reply);

            const Xml::Document doc(reply, "CreateRouteTable");
            route_table_id = Xml::find_text(&doc,
                                            "CreateRouteTableResponse",
                                            "routeTable",
                                            "routeTableId");

            OPENVPN_LOG("AWS EC2 CreateRouteTable -> RouteTableId " << route_table_id);
        }

        // tag route table with provided name
        {
            REST::Query q;
            q.emplace_back("Action", "CreateTags");
            q.emplace_back("ResourceId.1", route_table_id);
            q.emplace_back("Tag.1.Key", "Name");
            q.emplace_back("Tag.1.Value", name);
            add_transaction(ctx, std::move(q));
        }

        execute_transaction(ctx);

        // process reply
        {
            // get the transaction
            WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

            // get reply
            const std::string reply = t.content_in_string();

            if (!t.request_status_success())
                OPENVPN_THROW(aws_route_error, "CreateTags: " << t.format_status(*ctx.ts) << '\n'
                                                              << reply);
        }

        return route_table_id;
    }

    // Create/replace a VPC route
    static void replace_create_route(Context &ctx,
                                     const std::string &route_table_id,
                                     const std::string &route,
                                     RouteTargetType target_type,
                                     const std::string &target_value,
                                     bool ipv6)
    {
        std::string target_type_str;

        switch (target_type)
        {
        case RouteTargetType::INSTANCE_ID:
            target_type_str = "InstanceId";
            break;

        case RouteTargetType::INTERFACE_ID:
            target_type_str = "NetworkInterfaceId";
            break;

        default:
            OPENVPN_THROW(aws_route_error,
                          "replace_create_route: unknown RouteTargetType " << (int)target_type);
        }

        const std::string dest_cidr_block_name = ipv6 ? "DestinationIpv6CidrBlock" : "DestinationCidrBlock";

        // create API query
        {
            REST::Query q;
            q.emplace_back("Action", "ReplaceRoute");
            q.emplace_back(dest_cidr_block_name, route);
            q.emplace_back(target_type_str, target_value);
            q.emplace_back("RouteTableId", route_table_id);
            add_transaction(ctx, std::move(q));
        }

        // we expect to get 400 if route doesn't exist, so no need to retry
        ctx.ts->retry_on_http_4xx = false;

        // do transaction
        execute_transaction(ctx);

        // process reply
        {
            // get the transaction
            WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

            // get reply
            const std::string reply = t.content_in_string();

            // Check the reply status.  We only throw on communication failure,
            // since ReplaceRoute will legitimately fail if the route doesn't
            // exist yet.
            if (!t.comm_status_success())
                OPENVPN_THROW(aws_route_error, "ReplaceRoute: " << t.format_status(*ctx.ts) << '\n'
                                                                << reply);

            // ReplaceRoute succeeded?
            if (t.request_status_success())
            {
                // parse XML reply
                const Xml::Document doc(reply, "ReplaceRoute");
                const std::string retval = Xml::find_text(&doc,
                                                          "ReplaceRouteResponse",
                                                          "return");
                if (retval == "true")
                {
                    OPENVPN_LOG("AWS EC2 ReplaceRoute " << route << " -> table " << route_table_id);
                    return;
                }
            }
        }

        // Now try CreateRoute
        {
            REST::Query q;
            q.emplace_back("Action", "CreateRoute");
            q.emplace_back(dest_cidr_block_name, route);
            q.emplace_back(target_type_str, target_value);
            q.emplace_back("RouteTableId", route_table_id);
            add_transaction(ctx, std::move(q));
        }

        ctx.ts->retry_on_http_4xx = true;

        // do transaction
        execute_transaction(ctx);

        // process reply
        {
            // get the transaction
            WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

            // get reply
            const std::string reply = t.content_in_string();

            // check the reply status
            if (!t.http_status_success())
                OPENVPN_THROW(aws_route_error, "CreateRoute: " << t.format_status(*ctx.ts) << '\n'
                                                               << reply);

            // parse XML reply
            const Xml::Document doc(reply, "CreateRoute");
            const std::string retval = Xml::find_text(&doc,
                                                      "CreateRouteResponse",
                                                      "return");
            if (retval != "true")
                OPENVPN_THROW(aws_route_error, "CreateRoute: returned failure status: " << '\n'
                                                                                        << reply);

            OPENVPN_LOG("AWS EC2 CreateRoute " << route << " -> table " << route_table_id);
        }
    }

    static std::string get_route_table_by_name(Context &ctx, std::string &name)
    {
        REST::Query q;
        q.emplace_back("Action", "DescribeRouteTables");
        q.emplace_back("Filter.1.Name", "tag:Name");
        q.emplace_back("Filter.1.Value.1", name);
        add_transaction(ctx, std::move(q));

        // do transaction
        execute_transaction(ctx);

        // process reply

        // get the transaction
        WS::ClientSet::Transaction &t = ctx.ts->first_transaction();

        // get reply
        const std::string reply = t.content_in_string();

        // check the reply status
        if (!t.http_status_success())
            OPENVPN_THROW(aws_route_error, "DescribeRouteTables: " << t.format_status(*ctx.ts) << '\n'
                                                                   << reply);

        // parse XML reply
        const Xml::Document doc(reply, "DescribeRouteTables");
        auto route_table_id = Xml::find_text(&doc,
                                             "DescribeRouteTablesResponse",
                                             "routeTableSet",
                                             "item",
                                             "routeTableId");

        OPENVPN_LOG("AWS EC2 DescribeRouteTables " << name << " -> routeTableId " << (route_table_id.empty() ? "<none>" : route_table_id));

        return route_table_id;
    }

  private:
    static void execute_transaction(Context &ctx)
    {
        WS::ClientSet::new_request_synchronous(ctx.ts, ctx.async_stop, ctx.http_context.rng(), true);
    }

    static void add_transaction(const Context &ctx, REST::Query &&q)
    {
        std::unique_ptr<WS::ClientSet::Transaction> t(new WS::ClientSet::Transaction);
        t->req.uri = ec2_uri(ctx, std::move(q));
        t->req.method = "GET";
        t->ci.keepalive = true;
        ctx.ts->transactions.clear();
        ctx.ts->transactions.push_back(std::move(t));
    }

    static std::string ec2_uri(const Context &ctx, REST::Query &&q)
    {
        REST::QueryBuilder qb;
        qb.date = REST::amz_date();
        qb.expires = 300;
        qb.region = ctx.instance_info.region;
        qb.service = "ec2";
        qb.method = "GET";
        qb.host = ec2_host(ctx.instance_info);
        qb.uri = "/";
        qb.parms = std::move(q);
        qb.parms.emplace_back("Version", "2016-11-15");
        qb.add_amz_parms(ctx.creds);
        qb.sort_parms();
        qb.add_amz_signature(ctx.http_context.digest_factory(), ctx.creds);
        return qb.uri_query();
    }

    static std::string ec2_host(const PCQuery::Info &instance_info)
    {
        return "ec2." + instance_info.region + ".amazonaws.com";
    }
};
} // namespace openvpn::AWS
