/** @file

  Pre-Warming NetVConnection

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#pragma once

#include "proxy/http/PreWarmAlgorithm.h"

#include "iocore/eventsystem/EventSystem.h"
#include "iocore/net/NetVConnection.h"

// inknet
#include "iocore/net/PreWarm.h"
#include "iocore/net/SSLSNIConfig.h"
#include "iocore/net/YamlSNIConfig.h"

#include "iocore/hostdb/HostDB.h"
#include "iocore/net/NetTimeout.h"
#include "proxy/Milestones.h"

#include "tsutil/Metrics.h"

using ts::Metrics;

// tscore
#include "tscore/CryptoHash.h"
#include "tscore/ink_hrtime.h"

#include <array>
#include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <queue>
#include <string>
#include <unordered_map>

// PreWarm::Dst and PreWarm::SPtrConstDst are defined in iocore.
namespace PreWarm
{
struct DstHash {
  size_t
  operator()(const PreWarm::SPtrConstDst &dst) const
  {
    CryptoHash    hash;
    CryptoContext context{};

    context.update(dst->host.data(), dst->host.size());
    context.update(&dst->port, sizeof(in_port_t));
    context.update(&dst->type, sizeof(SNIRoutingType));
    context.update(&dst->alpn_index, sizeof(int));

    context.finalize(hash);

    return static_cast<size_t>(hash.fold());
  }
};

struct DstKeyEqual {
  bool
  operator()(const PreWarm::SPtrConstDst &x, const PreWarm::SPtrConstDst &y) const
  {
    return x->host == y->host && x->port == y->port && x->type == y->type && x->alpn_index == y->alpn_index;
  }
};

struct Conf {
  Conf(uint32_t min, int32_t max, double rate, ink_hrtime connect_timeout, ink_hrtime inactive_timeout, bool srv_enabled,
       YamlSNIConfig::Policy verify_server_policy, YamlSNIConfig::Property verify_server_properties, const std::string &sni)
    : min(min),
      max(max),
      rate(rate),
      connect_timeout(connect_timeout),
      inactive_timeout(inactive_timeout),
      srv_enabled(srv_enabled),
      verify_server_policy(verify_server_policy),
      verify_server_properties(verify_server_properties),
      sni(sni)
  {
  }

  uint32_t                min                      = 0;
  int32_t                 max                      = 0;
  double                  rate                     = 1.0;
  ink_hrtime              connect_timeout          = 0;
  ink_hrtime              inactive_timeout         = 0;
  bool                    srv_enabled              = false;
  YamlSNIConfig::Policy   verify_server_policy     = YamlSNIConfig::Policy::UNSET;
  YamlSNIConfig::Property verify_server_properties = YamlSNIConfig::Property::UNSET;
  std::string             sni;
};

using SPtrConstConf = std::shared_ptr<const Conf>;
using ParsedSNIConf = std::unordered_map<SPtrConstDst, SPtrConstConf, DstHash, DstKeyEqual>;

enum class CounterStat {
  HIT = 0,
  MISS,
  HANDSHAKE_TIME,
  HANDSHAKE_COUNT,
  RETRY,
  LAST_ENTRY,
};

enum class GaugeStat {
  INIT_LIST_SIZE = 0,
  OPEN_LIST_SIZE,
  LAST_ENTRY,
};

using CounterIds        = std::array<Metrics::Counter::AtomicType *, static_cast<size_t>(PreWarm::CounterStat::LAST_ENTRY)>;
using GaugeIds          = std::array<Metrics::Gauge::AtomicType *, static_cast<size_t>(PreWarm::GaugeStat::LAST_ENTRY)>;
using StatsIds          = std::tuple<CounterIds, GaugeIds>;
using SPtrConstStatsIds = std::shared_ptr<const StatsIds>;
using StatsIdMap        = std::unordered_map<SPtrConstDst, SPtrConstStatsIds, DstHash, DstKeyEqual>;
} // namespace PreWarm

class PreWarmSM;
class PreWarmManager;
class SNIConfigParams;

extern PreWarmManager prewarmManager;

/**
   @class PreWarmSM
   @brief A state machine to pre-warm connection

   @startuml
   hide empty description
   [*]              --> state_init       : new
   state_init       --> state_dns_lookup : start()
   state_init       --> state_closed     : stop()
   state_dns_lookup --> state_net_open   : HostDB lookup is done
   state_dns_lookup --> state_init       : retry()
   state_dns_lookup --> state_closed     : stop()
   state_net_open   --> state_open       : TCP/TLS Handshake is done
   state_net_open   --> state_init       : retry()
   state_net_open   --> state_closed     : stop()
   state_open       --> state_closed     : move_netvc()\nstop()
   state_closed     --> [*]              : delete
   @enduml
 */
class PreWarmSM : public Continuation
{
public:
  PreWarmSM(){};
  PreWarmSM(const PreWarm::SPtrConstDst &dst, const PreWarm::SPtrConstConf &conf, const PreWarm::SPtrConstStatsIds &stats_ids);
  ~PreWarmSM() override;

  // States
  int state_init(int event, void *data);
  int state_dns_lookup(int event, void *data);
  int state_net_open(int event, void *data);
  int state_open(int event, void *data);
  int state_closed(int event, void *data);

  // Controllers
  void start();
  void retry();
  void stop();
  void destroy();

  // Modifiers
  NetVConnection *move_netvc();
  IOBufferReader *server_buf_reader();

  // References
  bool has_data_from_origin_server() const;

  // NetTimeout
  // TODO: constify
  bool is_active_timeout_expired(ink_hrtime now);
  bool is_inactive_timeout_expired(ink_hrtime now);

  // HostDB inline completion functions
  void process_hostdb_info(HostDBRecord *r);
  void process_srv_info(HostDBRecord *r);

private:
  enum class Milestone {
    INIT = 0,
    DNS_LOOKUP_DONE,
    ESTABLISHED,
    CLOSED,
    LAST_ENTRY,
  };

  Action *_connect(const IpEndpoint &addr);
  void    _reset();
  void    _record_handshake_time();

  ////
  // Variables
  //
  NetTimeout                                                        _timeout{};
  Milestones<Milestone, static_cast<size_t>(Milestone::LAST_ENTRY)> _milestones;

  uint32_t _retry_counter = 0;

  PreWarm::SPtrConstDst      _dst;
  PreWarm::SPtrConstConf     _conf;
  PreWarm::SPtrConstStatsIds _stats_ids;

  NetVConnection *_netvc            = nullptr;
  Action         *_pending_action   = nullptr;
  MIOBuffer      *_read_buf         = nullptr;
  IOBufferReader *_read_buf_reader  = nullptr;
  MIOBuffer      *_write_buf        = nullptr;
  IOBufferReader *_write_buf_reader = nullptr;
  Event          *_retry_event      = nullptr;
};

extern ClassAllocator<PreWarmSM, false> preWarmSMAllocator;

/**
   @class PreWarmQueue
   @detail
   - Each ET_NET thread has this queue
   - Responsible for the life cycle of PreWarmSM until giving it to HttpSM

   @startuml
   hide empty description
   [*]              --> state_init    : new
   state_init       --> state_running : start pre-warming
   @enduml
 */
class PreWarmQueue : public Continuation
{
public:
  PreWarmQueue();
  ~PreWarmQueue() override;

  // States
  int state_init(int event, void *data);
  int state_running(int event, void *data);

  // Modifiers for queue
  void       push(const PreWarm::SPtrConstDst &dst, PreWarmSM *sm);
  PreWarmSM *dequeue(const PreWarm::SPtrConstDst &dst);

private:
  using Queue = std::deque<PreWarmSM *>;

  struct Stat {
    uint32_t miss = 0;
    uint32_t hit  = 0;
  };

  struct Info {
    Queue                     *init_list;
    Queue                     *open_list;
    PreWarm::SPtrConstConf     conf;
    PreWarm::SPtrConstStatsIds stats_ids;
    Stat                       stat;
  };

  using Map = std::unordered_map<PreWarm::SPtrConstDst, Info, PreWarm::DstHash, PreWarm::DstKeyEqual>;

  // construct/destruct PreWarmSM
  void _new_prewarm_sm(const PreWarm::SPtrConstDst &dst, const PreWarm::SPtrConstConf &conf,
                       const PreWarm::SPtrConstStatsIds &stats_ids);
  void _delete_prewarm_sm(PreWarmSM *sm);

  void _reconfigure();
  void _make_queue_empty(Queue *q);
  void _delete_closed_sm(Queue *q);

  // hooks for pre-warming pool size algorithm
  void _prewarm_on_event_interval(const PreWarm::SPtrConstDst &dst, const Info &info);
  void _prewarm_on_dequeue(const PreWarm::SPtrConstDst &dst, const Info &info);

  ////
  // Variables
  //
  PreWarm::Algorithm _algorithm = PreWarm::Algorithm::V1;

  Event     *_tick_event   = nullptr;
  ink_hrtime _event_period = HRTIME_SECONDS(1);

  // Force PreWarmSM to open new netvc to keep the connection warm periodically
  ActivityCop<PreWarmSM> _cop;
  DLL<PreWarmSM>         _cop_list;

  Map _map;
};

/**
   @class PreWarmManager
   @details
   - Global singleton object
   - Responsible for stats & configs management
 */
class PreWarmManager
{
public:
  PreWarmManager()
  {
    // We use the callback because it would introduce a circular dependency
    // for SNIConfig to explicitly call prewarmManager.reconfigure().
    // Because prewarmManager is global there's not a good place to set
    // this, but as long as there's only one instance of this class
    // there should not be an issue.
    SNIConfig::set_on_reconfigure_callback([this]() { this->reconfigure(); });
  }

  static void reconfigure_prewarming_on_threads();

  // Controllers
  void start();
  void reconfigure();
  void stop();

  // References
  const PreWarm::ParsedSNIConf &get_parsed_conf() const;
  const PreWarm::StatsIdMap    &get_stats_id_map() const;

private:
  void _parse_sni_conf(PreWarm::ParsedSNIConf &parsed_conf, const SNIConfigParams *sni_conf) const;
  void _register_stats(const PreWarm::ParsedSNIConf &parsed_conf);

  ////
  // Variables
  //
  // For the race of Main Thread (start up) vs Task Thread (config reload)
  Ptr<ProxyMutex> _mutex;

  PreWarm::ParsedSNIConf _parsed_conf;
  PreWarm::StatsIdMap    _stats_id_map;
};
