# Copyright (C) 2017-2025 Pier Carlo Chiodi
# Copyright (C) 2021 Ene Alin Gabriel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import argparse
from collections import OrderedDict
import datetime
import json
import logging
import os
import sys
import yaml

from .base import ARouteServerCommand
from ..config.asns import ConfigParserASNS
from ..config.clients import ConfigParserClients
from ..config.program import program_config
from ..errors import ARouteServerError, MissingFileError, IXFMemberListFromClientsMergeFileError

class IXFMemberListFromClientsCommand(ARouteServerCommand):

    COMMAND_NAME = "ixf-member-export"
    COMMAND_HELP = ("Build an IX-F Member Export JSON file "
                    "from the clients.")
    NEEDS_CONFIG = True

    @classmethod
    def add_arguments(cls, parser):
        super(IXFMemberListFromClientsCommand, cls).add_arguments(parser)

        parser.add_argument(
            "shortname",
            help="Short name of the IXP.")

        parser.add_argument(
            "ixf_id",
            type=int,
            help="IXP ID from the IX-F database.",
            metavar="IXF_ID")

        parser.add_argument(
            "--clients",
            help="Route server clients configuration file. "
                 "Default: the one set in the program configuration "
                 "file will be used.",
            metavar="FILE",
            dest="cfg_clients")

        parser.add_argument(
            "--ixp_id", "--ixp-id",
            type=int,
            default=0,
            help="The numeric identifier used by the IX to identify the "
                 "infrastructure for which the list of clients is generated. "
                 "Default: 0",
            dest="ixp_id")

        parser.add_argument(
            "--vlan-id",
            type=int,
            default=0,
            help="The VLAN ID used to set the connection_list.vlan_list "
                 "objects within the generated file. "
                 "Please note: this is not necessarily the 802.1q "
                 "tag but, generally speaking, it is the numeric identifier "
                 "used by the IX to identify the VLAN in its IX-F Member "
                 "Export JSON file.",
            dest="vlan_id")

        parser.add_argument(
            "--merge-file",
            type=argparse.FileType('r'),
            help="An optional JSON or YML file whose content will be merged "
                 "into the output generated by this command. This can be used "
                 "to add extra information to the JSON file generated by "
                 "ARouteServer, for example to include attributes that are "
                 "required on PeeringDB but that are not present in "
                 "ARouteServer (like IX country, full name, URL...). Check "
                 "https://arouteserver.readthedocs.io/en/latest/USAGE.html"
                 "#ixf-member-export-command for more details on how to use "
                 "it.",
            dest="merge_file")

        parser.add_argument(
            "-o", "--output",
            type=argparse.FileType('w'),
            help="Output file. Default: stdout.",
            default=sys.stdout,
            dest="output_file")

    @staticmethod
    def get_member_list(asns, clients, ixp_id, vlan_id):
        members = {}
        for client in clients:
            asn = str(client["asn"])
            if asn not in members:
                members[asn] = {"vlan_list": []}

            if client["description"]:
                members[asn]["descr"] = client["description"]

            vlan_obj = OrderedDict()
            vlan_obj["vlan_id"] = vlan_id

            ipv4_ipv6 = "ipv6" if ":" in client["ip"] else "ipv4"

            vlan_obj[ipv4_ipv6] = {
                "address": client["ip"],
                "routeserver": True
            }

            max_pref = client["cfg"]["filtering"]["max_prefix"][
                "limit_{}".format(ipv4_ipv6)]

            if max_pref:
                vlan_obj[ipv4_ipv6]["max_prefix"] = max_pref

            as_macros = client["cfg"]["filtering"]["irrdb"]["as_sets"]
            if not as_macros:
                if "AS{}".format(asn) in asns:
                    as_macros = asns["AS{}".format(asn)]["as_sets"]

            if as_macros:
                if len(as_macros) > 1:
                    logging.warning(
                        "Client {ip} (AS{asn}) is configured with more than "
                        "one AS-SETs: since the destination format supports "
                        "only one item, only the first one ({as_set}) will "
                        "be exported.".format(
                            ip=client["ip"], asn=client["asn"],
                            as_set=as_macros[0]
                        )
                    )
                vlan_obj[ipv4_ipv6]["as_macro"] = as_macros[0]

            members[asn]["vlan_list"].append(vlan_obj)

        res = []
        for member_asn in members:
            member = {"asnum": int(member_asn)}

            if "descr" in members[member_asn]:
                member["name"] = members[member_asn]["descr"]

            connection_list_entry = OrderedDict()
            connection_list_entry["ixp_id"] = ixp_id
            connection_list_entry["vlan_list"] = members[member_asn]["vlan_list"]
            member["connection_list"] = [connection_list_entry]
            res.append(member)

        return res

    @staticmethod
    def load_config_from_path(path):
        clients = ConfigParserClients()
        asns = ConfigParserASNS()
        try:
            if not os.path.isfile(path):
                raise MissingFileError(path)

            clients.load(path)
            asns.load(path)
        except ARouteServerError as e:
            raise ARouteServerError(
                "One or more errors occurred while loading "
                "clients file{}".format(
                    ": {}".format(str(e)) if str(e) else ""
                )
            )

        return asns, clients

    @staticmethod
    def build_json(path, ixp_id, shortname, vlan_id, ixf_id):
        asns, clients = \
            IXFMemberListFromClientsCommand.load_config_from_path(path)

        res = OrderedDict()
        res["version"] = "1.0"
        res["timestamp"] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')

        ixp_list_entry = OrderedDict()
        ixp_list_entry["ixp_id"] = ixp_id
        ixp_list_entry["ixf_id"] = ixf_id
        ixp_list_entry["shortname"] = shortname
        ixp_list_entry["vlan"] = [{"id": vlan_id}]
        res["ixp_list"] = [ixp_list_entry]

        res["member_list"] = IXFMemberListFromClientsCommand.get_member_list(
            asns, clients, ixp_id, vlan_id
        )
        return res

    @staticmethod
    def apply_merge_file(dic, merge_file, args):
        ixp_id_from_args = args["ixp_id"]
        ixf_id_from_args = args["ixf_id"]
        vlan_from_args = args["vlan"]

        # If the 'ixp_list' key is set in the merge file, let's assume that
        # it's better configured and more comprehensive than the one that
        # is generated by ARouteServer. So, get rid of the one present in
        # the dictionary built above, and use the one provided by the user.
        if "ixp_list" in merge_file:
            # Perform some sanity checks to be sure that the file that we're going
            # to produce is still valid. This is needed because we're using
            # content provided by the user, that may be partial or invalid.

            ixp_list = merge_file["ixp_list"]

            if not ixp_list:
                raise IXFMemberListFromClientsMergeFileError("ixp_list is empty", **args)
            if not isinstance(ixp_list, list):
                raise IXFMemberListFromClientsMergeFileError("ixp_list is not a list", **args)
            if not len(ixp_list) == 1:
                raise IXFMemberListFromClientsMergeFileError("ixp_list contains more than one dictionary", **args)

            ixp_list_entry = ixp_list[0]

            for key in ("ixf_id", "ixp_id", "shortname"):
                if key not in ixp_list_entry:
                    raise IXFMemberListFromClientsMergeFileError(
                        "the '{}' key cannot be found in the ixp_list dictionary".format(key), **args
                    )

                if not ixp_list_entry[key]:
                    raise IXFMemberListFromClientsMergeFileError(
                        "the value of the '{}' key in the ixp_list dictionary is empty or not set".format(key), **args
                    )

            for key in ("ixf_id", "ixp_id"):
                if not isinstance(ixp_list_entry[key], int):
                    raise IXFMemberListFromClientsMergeFileError(
                        "the value of the '{}' key in the ixp_list dictionary must be an integer".format(key), **args
                    )

            if ixp_list_entry["ixp_id"] != ixp_id_from_args:
                raise IXFMemberListFromClientsMergeFileError(
                    "the value of 'ixp_id' from the merge file ({ixp_id_from_merge_file}) doesn't match "
                    "the value from the command line arguments ({ixp_id_from_args}); please be sure that "
                    "both correspond, either adjusting the content of the merge file or passing the "
                    "--ixp_id {ixp_id_from_merge_file} option to the arouteserver ixf-member-export "
                    "command +".format(
                        ixp_id_from_merge_file=ixp_list_entry["ixp_id"],
                        ixp_id_from_args=ixp_id_from_args
                    ), **args
                )

            if ixp_list_entry["ixf_id"] != ixf_id_from_args:
                raise IXFMemberListFromClientsMergeFileError(
                    "the value of 'ixf_id' from the merge file ({ixf_id_from_merge_file}) doesn't match "
                    "the value from the command line arguments ({ixf_id_from_args}); please be sure that "
                    "both correspond, either adjusting the content of the merge file or passing the "
                    "{ixf_id_from_merge_file} value to the arouteserver ixf-member-export "
                    "command +".format(
                        ixf_id_from_merge_file=ixp_list_entry["ixf_id"],
                        ixf_id_from_args=ixf_id_from_args
                    ), **args
                )

            if "vlan" in ixp_list_entry:
                if not isinstance(ixp_list_entry["vlan"], list):
                    raise IXFMemberListFromClientsMergeFileError(
                        "the 'vlan' key is set, but it's not a list of dictionaries", **args
                    )

                if not all(isinstance(_, dict) for _ in ixp_list_entry["vlan"]):
                    raise IXFMemberListFromClientsMergeFileError(
                        "the 'vlan' key is set, but it's not a list of dictionaries", **args
                    )

                vlan_ids_from_merge_file = set([
                    _["id"] for _ in ixp_list_entry["vlan"] if "id" in _
                ])

                if args["vlan"] not in vlan_ids_from_merge_file:
                    raise IXFMemberListFromClientsMergeFileError(
                        "the VLAN ID {vlan_from_args} that was passed via the command line option --vlan-id "
                        "is not present in the list of VLANs from the merge file; please be sure that "
                        "the VLAN ID that is supplied as the command argument is present inside the "
                        "'ixp_list.vlan' key of the merge file (VLAN IDs found in the merge file: "
                        "{vlan_ids_from_merge_file})".format(
                            vlan_from_args=vlan_from_args,
                            vlan_ids_from_merge_file=", ".join(map(str, vlan_ids_from_merge_file))
                        ), **args
                    )

            # Now, we use the 'ixp_list' provided via the merge-file to replace
            # the content that was generated by ARouteServer. We do that by
            # removing "our" ixp_list, so that the one from the merge file will
            # be used instead.
            del dic["ixp_list"]

        # Merge "our" dictionary into the merge-file, to be sure that the content
        # from the former will be preferred over the content provided by the user.
        return {**merge_file, **dic}

    def run(self):
        path = self.args.cfg_clients or program_config.get("cfg_clients")

        dic = self.build_json(path, self.args.ixp_id, self.args.shortname,
                              self.args.vlan_id, self.args.ixf_id)

        if "--ixp_id" in sys.argv:
            logging.warning(
                "Deprecation notice: the '--ixp_id' option is now deprecated in favor "
                "of '--ixp-id' (dash instead of underscore). Please adjust your tooling "
                "to use the new version of the option. In the future, the '--ixp_id' "
                "will be removed."
            )

        if self.args.merge_file:
            merge_file_raw_content = self.args.merge_file.read()
            try:
                merge_file = yaml.safe_load(merge_file_raw_content)
            except Exception as e:
                yaml_exception = str(e)
                try:
                    merge_file = json.loads(merge_file_raw_content)
                except Exception as e:
                    json_exception = str(e)

                    raise ARouteServerError(
                        "The --merge-file is not in a valid YML or JSON format.\n\n"
                        "YAML error: {}\n\nJSON error: {}".format(
                            yaml_exception, json_exception
                        )
                    )

            args = {
                "ixp_id": self.args.ixp_id,
                "ixf_id": self.args.ixf_id,
                "shortname": self.args.shortname,
                "vlan": self.args.vlan_id
            }

            output_dic = self.apply_merge_file(dic, merge_file, args)
        else:
            output_dic = dic

        json.dump(output_dic, self.args.output_file, indent=2)
        return True
