# frozen_string_literal: true

require_relative 'pseudo_dsl/pseudo_model'
require_relative 'csv_mapper/edges_layer1_table'
require_relative 'csv_mapper/interface_prop_table'
require_relative 'csv_mapper/node_props_table'

module TopologyBuilder
  # L1 data builder
  class L1DataBuilder < PseudoDSL::DataBuilderBase
    # @param [String] target Target network (config) data name
    def initialize(target:, debug: false)
      super(debug: debug)
      @l1_edges = CSVMapper::EdgesLayer1Table.new(target)
      @intf_props = CSVMapper::InterfacePropertiesTable.new(target)
      @node_props = CSVMapper::NodePropsTable.new(target)
      validate_l1_edges
    end

    # @return [PNetworks] Networks contains only layer1 network topology
    def make_networks
      @network = @networks.network('layer1')
      @network.type = Netomox::NWTYPE_MDDO_L1
      setup_node_tp_link
      check_disconnected_node
      @networks
    end

    private

    # @param [CSVMapper:EdgeBase] edge Source/Destination of layer1 edge
    # @return [Boolean]
    def valid_edge_in_intf_props?(edge)
      props = @intf_props.find_all_records_by_node(edge.node)
      if props.empty?
        TopologyBuilder.logger.error("L1 edges have invalid node name: #{edge.node}")
        return false
      end

      return true if props.find { |p| p.interface == edge.interface }

      TopologyBuilder.logger.error("L1 edges have invalid interface name: #{edge.node}[#{edge.node}]")
      false
    end

    # @param [CSVMapper::EdgesLayer1TableRecord] edge Layer1 edge
    # @return [Boolean]
    def bidirectional_link?(edge)
      rev_edge = @l1_edges.find_link_by_src_node_intf(edge.dst.node, edge.dst.interface)
      unless rev_edge
        # NOTE: netomox cannot support unidirectional link
        TopologyBuilder.logger.error("Link #{edge} is unidirectional link")
        return false
      end

      rev_edge.dst == edge.src
    end

    # L1 edges table is converted from layer1-topology.json.
    # but, the json data are converted from interface-description in configs or handwriting data.
    # it must to validate all node and interface names are correct.
    # @return [void]
    def validate_l1_edges
      # find node name and interface name in other (batfish-generated) table
      names_check_results = @l1_edges.records.map do |l1_edge|
        valid_edge_in_intf_props?(l1_edge.src) && valid_edge_in_intf_props?(l1_edge.dst)
      end
      bd_check_results = @l1_edges.records.map { |l1_edge| bidirectional_link?(l1_edge) }
      return unless (names_check_results + bd_check_results).include?(false)

      TopologyBuilder.logger.fatal('Found invalid layer1 edges')
      exit 1
    end

    # @param [PLink] l1_link Layer1 link
    # @return [Boolean] true if the link is LAG link
    def lag_link?(l1_link)
      src_intf_props = @intf_props.find_record_by_node_intf(l1_link.src.node, l1_link.src.interface)
      dst_intf_props = @intf_props.find_record_by_node_intf(l1_link.dst.node, l1_link.dst.interface)
      src_intf_props && dst_intf_props && src_intf_props.lag_parent? && dst_intf_props.lag_parent?
    end

    # @param [EdgeBase] edge LAG port
    # @return [Array<String>] Interfaces of the LAG member
    # @raise [StandardError] If the lag term-point (edge) is not found.
    def lag_members(edge)
      intf_props = @intf_props.find_record_by_node_intf(edge.node, edge.interface)
      raise StandardError("LAG member props not found: #{edge}") unless intf_props

      intf_props.lag_member_interfaces
    end

    # @param [EdgesLayer1TableRecord] l1_link Layer1 link record
    # @return [void]
    def add_node_tp_link_for_lag_link(l1_link)
      src_lag_members = lag_members(l1_link.src)
      dst_lag_members = lag_members(l1_link.dst)
      # NOTE: if the `edges_layer1` table is generated by batfish `edges(layer1)` query,
      #   the table does NOT have connection information about LAG-members.
      #   make it a pair according to its sequence of LAG members list.
      src_lag_members.each_with_index do |src_intf, i|
        add_node_tp_link(l1_link.src.node, src_intf, l1_link.dst.node, dst_lag_members[i])
      end
    end

    # @param [String] node_name Node name
    # @return [PNode] added node
    def add_node(node_name)
      node = @network.node(node_name)
      node.attribute = { os_type: @node_props.find_record_by_node(node_name)&.config_format&.downcase }
      node
    end

    # @param [String] node_name Node name
    # @param [String] intf_name Interface name
    # @return [void]
    def add_node_tp(node_name, intf_name)
      node = add_node(node_name)
      tp = node.term_point(intf_name)
      tp_rec = @intf_props.find_record_by_node_intf(node_name, intf_name)
      tp.attribute = { description: tp_rec.description } if tp_rec
    end

    # @param [String] src_node Source node name
    # @param [String] src_intf Source interface name
    # @param [String] dst_node Destination node name
    # @param [String] dst_intf Destination interface name
    # @return [void]
    def add_node_tp_link(src_node, src_intf, dst_node, dst_intf)
      add_node_tp(src_node, src_intf)
      add_node_tp(dst_node, dst_intf)
      @network.link(src_node, src_intf, dst_node, dst_intf)
    end

    # make links
    # @return [void]
    def setup_node_tp_link
      # NOTE: link-centric data construction
      #   - Layer1 edge data is bidirectional link.
      #     A physical link is expressed a pair of reverse-directional link records.
      #   - link-based search : if there is a standalone node (node that don't have layer1 link),
      #     the node is not included layer1 topology data.
      @l1_edges.records.each do |l1_link|
        # NOTE: Only when `edges_layer1.csv` (`@l1_edges`) is a result of batfish `edges(layer1)` query.
        #   if the table is converted from layer1_topology.json converted data,
        #   it does not contains LAG link (physical link ONLY).
        if lag_link?(l1_link)
          add_node_tp_link_for_lag_link(l1_link)
          next
        end

        add_node_tp_link(l1_link.src.node, l1_link.src.interface, l1_link.dst.node, l1_link.dst.interface)
      end
    end

    # add layer1 standalone node
    # @return [void]
    def check_disconnected_node
      linked_nodes = @network.nodes.map(&:name)
      whole_nodes = @node_props.records.map(&:node)
      # subtract linked_nodes from whole_nodes (result = layer1 standalone nodes)
      standalone_nodes = whole_nodes - linked_nodes
      debug_print "diff? #{standalone_nodes}"
      standalone_nodes.each do |node_name|
        TopologyBuilder.logger.error("Found standalone node: #{node_name}")
        add_node(node_name)
      end
    end
  end
end
