#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2020 FABRIC Testbed
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Komal Thareja(kthare10@renci.org)
"""FABRIC site and resource information management.

This module provides classes for querying and managing information about
FABRIC testbed sites and their available resources. The Site and Resources
classes allow users to discover available compute, storage, and network
resources across the FABRIC infrastructure.
"""

from __future__ import annotations

import json
import logging
import traceback
from typing import Dict, List, Tuple

from fabrictestbed.slice_editor import Capacities
from fim.user import Component, node
from fim.user.composite_node import CompositeNode
from fim.view_only_dict import ViewOnlyDict

from fabrictestbed_extensions.fablib.constants import Constants
from fabrictestbed_extensions.utils.utils import Utils

log = logging.getLogger("fablib")


class ResourceConstants:
    attribute_name_mappings = {
        Constants.CORES.lower(): {
            Constants.NON_PRETTY_NAME: Constants.CORES.lower(),
            Constants.PRETTY_NAME: Constants.CORES,
            Constants.HEADER_NAME: Constants.CORES,
        },
        Constants.RAM.lower(): {
            Constants.NON_PRETTY_NAME: Constants.RAM.lower(),
            Constants.PRETTY_NAME: Constants.RAM,
            Constants.HEADER_NAME: f"{Constants.RAM} ({Capacities.UNITS[Constants.RAM.lower()]})",
        },
        Constants.DISK: {
            Constants.NON_PRETTY_NAME: Constants.DISK.lower(),
            Constants.PRETTY_NAME: Constants.DISK,
            Constants.HEADER_NAME: f"{Constants.DISK} ({Capacities.UNITS[Constants.DISK.lower()]})",
        },
        Constants.NIC_SHARED_CONNECTX_6: {
            Constants.NON_PRETTY_NAME: "nic_basic",
            Constants.PRETTY_NAME: "Basic NIC",
            Constants.HEADER_NAME: "Basic (100 Gbps NIC)",
        },
        Constants.P4_SWITCH: {
            Constants.NON_PRETTY_NAME: Constants.P4_SWITCH.lower(),
            Constants.PRETTY_NAME: Constants.P4_SWITCH,
            Constants.HEADER_NAME: Constants.P4_SWITCH,
        },
        Constants.SMART_NIC_CONNECTX_6: {
            Constants.NON_PRETTY_NAME: "nic_connectx_6",
            Constants.PRETTY_NAME: "ConnectX-6",
            Constants.HEADER_NAME: "ConnectX-6 (100 Gbps x2 NIC)",
        },
        Constants.SMART_NIC_CONNECTX_5: {
            Constants.NON_PRETTY_NAME: "nic_connectx_5",
            Constants.PRETTY_NAME: "ConnectX-5",
            Constants.HEADER_NAME: "ConnectX-5 (25 Gbps x2 NIC)",
        },
        Constants.SMART_NIC_CONNECTX_7_100: {
            Constants.NON_PRETTY_NAME: "nic_connectx_7_100",
            Constants.PRETTY_NAME: "ConnectX-7-100",
            Constants.HEADER_NAME: "ConnectX-7 (100 Gbps x2 NIC)",
        },
        Constants.SMART_NIC_CONNECTX_7_400: {
            Constants.NON_PRETTY_NAME: "nic_connectx_7_400",
            Constants.PRETTY_NAME: "ConnectX-7-400",
            Constants.HEADER_NAME: "ConnectX-7 (400 Gbps x2 NIC)",
        },
        Constants.SMART_NIC_BlueField2_CONNECTX_6: {
            Constants.NON_PRETTY_NAME: "nic_bluefield2_connectx_5",
            Constants.PRETTY_NAME: "BlueField2-ConnectX-6",
            Constants.HEADER_NAME: "BlueField2-ConnectX-6 (100 Gbps x2 NIC)",
        },
        Constants.NVME_P4510: {
            Constants.NON_PRETTY_NAME: "nvme",
            Constants.PRETTY_NAME: "NVMe",
            Constants.HEADER_NAME: "P4510 (NVMe 1TB)",
        },
        Constants.GPU_TESLA_T4: {
            Constants.NON_PRETTY_NAME: "tesla_t4",
            Constants.PRETTY_NAME: "Tesla T4",
            Constants.HEADER_NAME: "Tesla T4 (GPU)",
        },
        Constants.GPU_RTX6000: {
            Constants.NON_PRETTY_NAME: "rtx6000",
            Constants.PRETTY_NAME: "RTX6000",
            Constants.HEADER_NAME: "RTX6000 (GPU)",
        },
        Constants.GPU_A30: {
            Constants.NON_PRETTY_NAME: "a30",
            Constants.PRETTY_NAME: "A30",
            Constants.HEADER_NAME: "A30 (GPU)",
        },
        Constants.GPU_A40: {
            Constants.NON_PRETTY_NAME: "a40",
            Constants.PRETTY_NAME: "A40",
            Constants.HEADER_NAME: "A40 (GPU)",
        },
        Constants.FPGA_XILINX_U280: {
            Constants.NON_PRETTY_NAME: "fpga_u280",
            Constants.PRETTY_NAME: "U280",
            Constants.HEADER_NAME: "FPGA-Xilinx-U280",
        },
        Constants.FPGA_XILINX_SN1022: {
            Constants.NON_PRETTY_NAME: "fpga_sn1022",
            Constants.PRETTY_NAME: "SN1022",
            Constants.HEADER_NAME: "FPGA-Xilinx-SN1022",
        },
    }
    pretty_names = {
        "name": "Name",
        "state": "State",
        "address": "Address",
        "location": "Location",
        "ptp_capable": "PTP Capable",
        Constants.HOSTS.lower(): Constants.HOSTS,
        Constants.CPUS.lower(): Constants.CPUS,
    }
    for attribute, names in attribute_name_mappings.items():
        pretty_name = names.get(Constants.PRETTY_NAME)
        non_pretty_name = names.get(Constants.NON_PRETTY_NAME)
        if pretty_name not in pretty_names:
            pretty_names[f"{non_pretty_name}_{Constants.AVAILABLE.lower()}"] = (
                f"{pretty_name} {Constants.AVAILABLE}"
            )
            pretty_names[f"{non_pretty_name}_{Constants.ALLOCATED.lower()}"] = (
                f"{pretty_name} {Constants.ALLOCATED}"
            )
            pretty_names[f"{non_pretty_name}_{Constants.CAPACITY.lower()}"] = (
                f"{pretty_name} {Constants.CAPACITY}"
            )


class Switch:
    def __init__(self, switch: node.Node, fablib_manager):
        """
        Initialize a Switch object.

        :param switch: The node representing the switch.
        :type switch: node.Node

        :param fablib_manager: The manager for the Fabric library.
        :type fablib_manager: Any

        """
        self.switch = switch
        self.fablib_manager = fablib_manager

    def get_capacity(self) -> int:
        """
        Get the capacity of the switch.

        :return: The capacity of the switch.
        :rtype: int
        """
        try:
            return self.switch.capacities.unit
        except Exception:
            return 0

    def get_allocated(self) -> int:
        """
        Get the allocated capacity of the switch.

        :return: The allocated capacity of the switch.
        :rtype: int
        """
        try:
            return self.switch.capacity_allocations.unit
        except Exception:
            return 0

    def get_available(self) -> int:
        """
        Get the available capacity of the switch.

        :return: The available capacity of the switch.
        :rtype: int
        """
        return self.get_capacity() - self.get_allocated()

    def get_fim(self) -> node.Node:
        """
        Get the FIM object of the Switch.

        :return: The FIM of the Switch.
        :rtype: node.Node
        """
        return self.switch


class Host:
    def __init__(self, host: node.Node, state: str, ptp: bool, fablib_manager):
        """
        Initialize a Host object.

        :param host: The node representing the host.
        :type host: node.Node

        :param state: The state of the host.
        :type state: str

        :param ptp: Boolean indicating if the host is PTP capable.
        :type ptp: bool

        :param fablib_manager: The manager for the Fabric library.
        :type fablib_manager: Any

        :return: None
        """
        self.host = host
        self.state = state
        self.ptp = ptp
        self.fablib_manager = fablib_manager
        self.host_info = {}
        self.__load()

    def get_fablib_manager(self):
        """
        Get the Fabric library manager associated with the host.

        :return: The Fabric library manager.
        :rtype: Any
        """
        return self.fablib_manager

    def __str__(self):
        """
        Convert the Host object to a string representation in JSON format.

        :return: JSON string representation of the Host object.
        :rtype: str
        """
        return self.to_json()

    def to_dict(self) -> dict:
        """
        Convert the Host object to a dictionary.

        :return: Dictionary representation of the Host object.
        :rtype: dict
        """
        d = {
            "name": self.get_name(),
            "state": self.get_state(),
            "address": self.get_location_postal(),
            "location": self.get_location_lat_long(),
            "ptp_capable": self.get_ptp_capable(),
        }

        for attribute, names in ResourceConstants.attribute_name_mappings.items():
            if attribute in Constants.P4_SWITCH:
                continue
            capacity = self.host_info.get(attribute.lower(), {}).get(
                Constants.CAPACITY.lower(), 0
            )
            allocated = self.host_info.get(attribute.lower(), {}).get(
                Constants.ALLOCATED.lower(), 0
            )
            available = capacity - allocated
            d[
                f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.AVAILABLE.lower()}"
            ] = available
            d[
                f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.CAPACITY.lower()}"
            ] = capacity
            d[
                f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.ALLOCATED.lower()}"
            ] = allocated

        return d

    def to_json(self) -> str:
        """
        Convert the Host object to a JSON string.

        :return: JSON string representation of the Host object.
        :rtype: str
        """
        return json.dumps(self.to_dict(), indent=4)

    def get_state(self) -> str:
        """
        Get the state of the host.

        :return: The state of the host.
        :rtype: str
        """
        if not self.state:
            return ""
        return self.state

    def get_fim(self) -> node.Node:
        """
        Get the FIM object of the host.

        :return: The FIM of the host.
        :rtype: node.Node
        """
        return self.host

    def __load(self):
        """
        Load information about the host.

        :return: None
        """
        try:
            self.host_info[Constants.CORES.lower()] = {
                Constants.CAPACITY.lower(): self.get_core_capacity(),
                Constants.ALLOCATED.lower(): self.get_core_allocated(),
            }
            self.host_info[Constants.RAM.lower()] = {
                Constants.CAPACITY.lower(): self.get_ram_capacity(),
                Constants.ALLOCATED.lower(): self.get_ram_allocated(),
            }
            self.host_info[Constants.DISK.lower()] = {
                Constants.CAPACITY.lower(): self.get_disk_capacity(),
                Constants.ALLOCATED.lower(): self.get_disk_allocated(),
            }

            if self.host.components:
                for component_model_name, c in self.host.components.items():
                    comp_cap = self.host_info.setdefault(
                        component_model_name.lower(), {}
                    )
                    comp_cap.setdefault(Constants.CAPACITY.lower(), 0)
                    comp_cap.setdefault(Constants.ALLOCATED.lower(), 0)
                    comp_cap[Constants.CAPACITY.lower()] += c.capacities.unit
                    if c.capacity_allocations:
                        comp_cap[
                            Constants.ALLOCATED.lower()
                        ] += c.capacity_allocations.unit
        except Exception as e:
            # log.error(f"Failed to get {component_model_name} capacity {site}: {e}")
            pass

    def get_components(self) -> ViewOnlyDict:
        """
        Get the components associated with the host.

        :return: Dictionary-like view of the components associated with the host.
        :rtype: ViewOnlyDict
        """
        try:
            return self.host.components
        except Exception as e:
            pass

    def get_component(self, comp_model_type: str) -> Component:
        """
        Get a specific component associated with the host.

        :param comp_model_type: The type of component to retrieve.
        :type comp_model_type: str

        :return: The specified component.
        :rtype: Component
        """
        try:
            return self.host.components.get(comp_model_type)
        except Exception as e:
            pass

    def show(
        self,
        output: str = None,
        fields: list[str] = None,
        quiet: bool = False,
        pretty_names=True,
    ) -> str:
        """
        Creates a tabulated string of all the available resources at a specific host.

        Intended for printing available resources at a host.

        :param output: Output type
        :type output: str
        :param fields: List of fields to include
        :type fields: List
        :param quiet: flag indicating verbose or quiet display
        :type quiet: bool
        :param pretty_names: flag indicating if pretty names for the fields to be used or not
        :type pretty_names: bool

        :return: Tabulated string of available resources
        :rtype: String
        """

        data = self.to_dict()

        if pretty_names:
            pretty_names_dict = ResourceConstants.pretty_names
        else:
            pretty_names_dict = {}

        host_table = Utils.show_table(
            data,
            fields=fields,
            title="Host",
            output=output,
            quiet=quiet,
            pretty_names_dict=pretty_names_dict,
        )

        return host_table

    def get_location_postal(self) -> str:
        """
        Gets the location of a site by postal address

        :param site: site name or site object
        :type site: String or Node or NodeSliver
        :return: postal address of the site
        :rtype: String
        """
        try:
            return self.host.location.postal
        except Exception as e:
            # log.debug(f"Failed to get postal address for {site}")
            return ""

    def get_location_lat_long(self) -> Tuple[float, float]:
        """
        Gets gets location of a site in latitude and longitude

        :return: latitude and longitude of the site
        :rtype: Tuple(float,float)
        """
        try:
            return self.host.location.to_latlon()
        except Exception as e:
            # log.debug(f"Failed to get latitude and longitude for {site}")
            return 0, 0

    def get_ptp_capable(self) -> bool:
        """
        Gets the PTP flag of the site - if it has a native PTP capability
        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: boolean flag
        :rtype: bool
        """
        try:
            return self.ptp
        except Exception as e:
            # log.debug(f"Failed to get PTP status for {site}")
            return False

    def get_name(self):
        """
        Gets the host name

        :return: str
        """
        try:
            return self.host.name
        except Exception as e:
            # log.debug(f"Failed to get name for {host}")
            return ""

    def get_core_capacity(self) -> int:
        """
        Gets the total number of cores at the site

        :return: core count
        :rtype: int
        """
        try:
            return self.host.capacities.core
        except Exception as e:
            # log.debug(f"Failed to get core capacity {site}")
            return 0

    def get_core_allocated(self) -> int:
        """
        Gets the number of currently allocated cores at the site

        :return: core count
        :rtype: int
        """
        try:
            return self.host.capacity_allocations.core
        except Exception as e:
            # log.debug(f"Failed to get cores allocated {site}")
            return 0

    def get_core_available(self) -> int:
        """
        Gets the number of currently available cores at the site

        :return: core count
        :rtype: int
        """
        try:
            return self.get_core_capacity() - self.get_core_allocated()
        except Exception as e:
            # log.debug(f"Failed to get cores available {site}")
            return self.get_core_capacity()

    def get_ram_capacity(self) -> int:
        """
        Gets the total amount of memory at the site in GB

        :return: ram in GB
        :rtype: int
        """
        try:
            return self.host.capacities.ram
        except Exception as e:
            # log.debug(f"Failed to get ram capacity {site}")
            return 0

    def get_ram_allocated(self) -> int:
        """
        Gets the amount of memory currently  allocated the site in GB

        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: ram in GB
        :rtype: int
        """
        try:
            return self.host.capacity_allocations.ram
        except Exception as e:
            # log.debug(f"Failed to get ram allocated {site}")
            return 0

    def get_ram_available(self) -> int:
        """
        Gets the amount of memory currently  available the site in GB

        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: ram in GB
        :rtype: int
        """
        try:
            return self.get_ram_capacity() - self.get_ram_allocated()
        except Exception as e:
            # log.debug(f"Failed to get ram available {site_name}")
            return self.get_ram_capacity()

    def get_disk_capacity(self) -> int:
        """
        Gets the total amount of disk available the site in GB

        :return: disk in GB
        :rtype: int
        """
        try:
            return self.host.capacities.disk
        except Exception as e:
            # log.debug(f"Failed to get disk capacity {site}")
            return 0

    def get_disk_allocated(self) -> int:
        """
        Gets the amount of disk allocated the site in GB

        :return: disk in GB
        :rtype: int
        """
        try:
            return self.host.capacity_allocations.disk
        except Exception as e:
            # log.debug(f"Failed to get disk allocated {site}")
            return 0

    def get_disk_available(self) -> int:
        """
        Gets the amount of disk available the site in GB

        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: disk in GB
        :rtype: int
        """
        try:
            return self.get_disk_capacity() - self.get_disk_allocated()
        except Exception as e:
            # log.debug(f"Failed to get disk available {site_name}")
            return self.get_disk_capacity()

    def get_component_capacity(
        self,
        component_model_name: str,
    ) -> int:
        """
        Gets the total site capacity of a component by model name.

        :param component_model_name: component model name
        :type component_model_name: String
        :return: total component capacity
        :rtype: int
        """
        component_capacity = 0
        try:
            if component_model_name in self.host.components:
                component_capacity += self.host.components[
                    component_model_name
                ].capacities.unit
            return component_capacity
        except Exception as e:
            # log.error(f"Failed to get {component_model_name} capacity {site}: {e}")
            return component_capacity

    def get_component_allocated(
        self,
        component_model_name: str,
    ) -> int:
        """
        Gets gets number of currently allocated components on a the site
        by the component by model name.

        :param component_model_name: component model name
        :type component_model_name: String
        :return: currently allocated component of this model
        :rtype: int
        """
        component_allocated = 0
        try:
            if (
                component_model_name in self.host.components
                and self.host.components[component_model_name].capacity_allocations
            ):
                component_allocated += self.host.components[
                    component_model_name
                ].capacity_allocations.unit
            return component_allocated
        except Exception as e:
            # log.error(f"Failed to get {component_model_name} allocated {site}: {e}")
            return component_allocated

    def get_component_available(
        self,
        component_model_name: str,
    ) -> int:
        """
        Gets gets number of currently available components on the site
        by the component by model name.

        :param component_model_name: component model name
        :type component_model_name: String
        :return: currently available component of this model
        :rtype: int
        """
        try:
            return self.get_component_capacity(
                component_model_name
            ) - self.get_component_allocated(component_model_name)
        except Exception as e:
            # log.debug(f"Failed to get {component_model_name} available {site}")
            return self.get_component_capacity(component_model_name)


class Site:
    def __init__(self, site: CompositeNode, fablib_manager):
        """
        Initialize a Site object.

        :param site: The node representing the site.
        :type site: node.Node

        :param fablib_manager: The manager for the Fabric library.
        :type fablib_manager: Any

        :return: None
        """
        super().__init__()
        self.site = site
        self.fablib_manager = fablib_manager
        self.hosts = {}
        self.switches = {}
        self.site_info = {}
        self.__load()

    def get_hosts(self) -> Dict[str, Host]:
        """
        Get the hosts associated with the site.

        :return: Dictionary of hosts associated with the site.
        :rtype: Dict[str, Host]
        """
        return self.hosts

    def __load(self):
        """
        Load information about the site.

        :return: None
        """
        self.__load_hosts()
        self.__load_site_info()

    def __load_hosts(self):
        """
        Load Hosts and Switches for a site.

        :return: None
        """
        try:
            from fim.user import NodeType

            for c_name, child in self.site.children.items():
                if child.type == NodeType.Server:
                    self.hosts[child.name] = Host(
                        host=child,
                        state=self.get_state(child.name),
                        ptp=self.get_ptp_capable(),
                        fablib_manager=self.fablib_manager,
                    )
                elif child.type == NodeType.Switch:
                    self.switches[child.name] = Switch(
                        switch=child, fablib_manager=self.get_fablib_manager()
                    )
        except Exception as e:
            log.error(f"Error occurred - {e}")
            log.error(traceback.format_exc())

    def to_json(self) -> str:
        """
        Convert the Site object to a JSON string.

        :return: JSON string representation of the Site object.
        :rtype: str
        """
        return json.dumps(self.to_dict(), indent=4)

    def get_fablib_manager(self):
        """
        Get the Fabric library manager associated with the site.

        :return: The Fabric library manager.
        :rtype: Any
        """
        return self.fablib_manager

    def to_row(self) -> Tuple[list, list]:
        """
        Convert the Site object to a row for tabular display.

        :return: Tuple containing headers and row for tabular display.
        :rtype: Tuple[list, list]
        """
        headers = [
            "Name",
            "PTP Capable",
            Constants.CPUS,
        ]
        row = [
            self.get_name(),
            self.get_ptp_capable(),
            self.get_cpu_capacity(),
        ]

        for attribute, names in ResourceConstants.attribute_name_mappings.items():
            allocated = self.site_info.get(attribute, {}).get(
                Constants.ALLOCATED.lower(), 0
            )
            capacity = self.site_info.get(attribute, {}).get(
                Constants.CAPACITY.lower(), 0
            )
            available = capacity - allocated
            row.append(f"{available}/{capacity}")
            headers.append(names.get(Constants.HEADER_NAME))
        return headers, row

    def get_host(self, name: str) -> Host:
        """
        Get a specific host associated with the site.

        :param name: The name of the host to retrieve.
        :type name: str

        :return: The specified host.
        :rtype: Host
        """
        return self.hosts.get(name)

    def __str__(self) -> str:
        """
        Convert the Site object to a string representation in JSON format.

        :return: JSON string representation of the Site object.
        :rtype: str
        """
        return self.to_json()

    def get_name(self) -> str:
        """
        Gets the site name

        :return: str(MaintenanceState)
        """
        try:
            return self.site.name
        except Exception as e:
            # log.debug(f"Failed to get name for {site}")
            return ""

    def get_state(self, host: str = None):
        """
        Gets the maintenance state of the node

        :return: str(MaintenanceState)
        """
        try:
            if not host:
                return str(self.site.maintenance_info.get(self.site.name).state)
            else:
                if self.site.maintenance_info.get(host):
                    return str(self.site.maintenance_info.get(host).state)
                else:
                    return "Active"
        except Exception as e:
            # log.debug(f"Failed to get maintenance state for {site}")
            return ""

    def get_location_postal(self) -> str:
        """
        Gets the location of a site by postal address

        :param site: site name or site object
        :type site: String or Node or NodeSliver
        :return: postal address of the site
        :rtype: String
        """
        try:
            return self.site.location.postal
        except Exception as e:
            # log.debug(f"Failed to get postal address for {site}")
            return ""

    def get_location_lat_long(self) -> Tuple[float, float]:
        """
        Gets gets location of a site in latitude and longitude

        :return: latitude and longitude of the site
        :rtype: Tuple(float,float)
        """
        try:
            return self.site.location.to_latlon()
        except Exception as e:
            # log.debug(f"Failed to get latitude and longitude for {site}")
            return 0, 0

    def get_ptp_capable(self) -> bool:
        """
        Gets the PTP flag of the site - if it has a native PTP capability

        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: boolean flag
        :rtype: bool
        """
        try:
            return self.site.flags.ptp
        except Exception as e:
            # log.debug(f"Failed to get PTP status for {site}")
            return False

    def get_host_capacity(self) -> int:
        """
        Gets the number of hosts at the site

        :param site: site name or site object
        :type site: String or Node or NodeSliver
        :return: host count
        :rtype: int
        """
        try:
            return self.site.capacities.unit
        except Exception as e:
            # log.debug(f"Failed to get host count {site}")
            return 0

    def get_cpu_capacity(self) -> int:
        """
        Gets the total number of cpus at the site

        :param site: site name or site object
        :type site: String or node.Node or NodeSliver
        :return: cpu count
        :rtype: int
        """
        try:
            return self.site.capacities.cpu
        except Exception as e:
            # log.debug(f"Failed to get cpu capacity {site}")
            return 0

    def to_dict(self) -> dict:
        """
        Convert site information into a dictionary
        """
        d = {
            "name": self.get_name(),
            "state": self.get_state(),
            "address": self.get_location_postal(),
            "location": self.get_location_lat_long(),
            "ptp_capable": self.get_ptp_capable(),
            "hosts": self.get_host_capacity(),
            "cpus": self.get_cpu_capacity(),
        }

        for attribute, names in ResourceConstants.attribute_name_mappings.items():
            capacity = self.site_info.get(attribute.lower(), {}).get(
                Constants.CAPACITY.lower(), 0
            )
            allocated = self.site_info.get(attribute.lower(), {}).get(
                Constants.ALLOCATED.lower(), 0
            )
            available = capacity - allocated
            d[
                f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.AVAILABLE.lower()}"
            ] = available
            d[
                f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.CAPACITY.lower()}"
            ] = capacity
            d[
                f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.ALLOCATED.lower()}"
            ] = allocated

        return d

    def __load_site_info(self):
        """
        Load the total site capacity of all components for a site
        """
        try:
            self.site_info[Constants.CORES.lower()] = {
                Constants.CAPACITY.lower(): self.get_core_capacity(),
                Constants.ALLOCATED.lower(): self.get_core_allocated(),
            }
            self.site_info[Constants.RAM.lower()] = {
                Constants.CAPACITY.lower(): self.get_ram_capacity(),
                Constants.ALLOCATED.lower(): self.get_ram_allocated(),
            }
            self.site_info[Constants.DISK.lower()] = {
                Constants.CAPACITY.lower(): self.get_disk_capacity(),
                Constants.ALLOCATED.lower(): self.get_disk_allocated(),
            }

            for h in self.hosts.values():
                if h.get_components():
                    for component_model_name, c in h.get_components().items():
                        comp_cap = self.site_info.setdefault(
                            component_model_name.lower(), {}
                        )
                        comp_cap.setdefault(Constants.CAPACITY.lower(), 0)
                        comp_cap.setdefault(Constants.ALLOCATED.lower(), 0)
                        comp_cap[Constants.CAPACITY.lower()] += c.capacities.unit
                        if c.capacity_allocations:
                            comp_cap[
                                Constants.ALLOCATED.lower()
                            ] += c.capacity_allocations.unit

            p4_mappings = ResourceConstants.attribute_name_mappings.get(
                Constants.P4_SWITCH
            )
            for s in self.switches.values():
                self.site_info[p4_mappings.get(Constants.NON_PRETTY_NAME)] = {
                    Constants.CAPACITY.lower(): s.get_capacity(),
                    Constants.ALLOCATED.lower(): s.get_allocated(),
                }

        except Exception as e:
            # log.error(f"Failed to get {component_model_name} capacity {site}: {e}")
            pass

    def show(
        self,
        output: str = None,
        fields: list[str] = None,
        quiet: bool = False,
        pretty_names=True,
    ) -> str:
        """
        Creates a tabulated string of all the available resources at a specific site.

        Intended for printing available resources at a site.

        :param output: Output type
        :type output: str
        :param fields: List of fields to include
        :type fields: List
        :param quiet: flag indicating verbose or quiet display
        :type quiet: bool
        :param pretty_names: flag indicating if pretty names for the fields to be used or not
        :type pretty_names: bool

        :return: Tabulated string of available resources
        :rtype: String
        """

        data = self.to_dict()

        if pretty_names:
            pretty_names_dict = ResourceConstants.pretty_names
        else:
            pretty_names_dict = {}

        site_table = Utils.show_table(
            data,
            fields=fields,
            title="Site",
            output=output,
            quiet=quiet,
            pretty_names_dict=pretty_names_dict,
        )

        return site_table

    def get_component_capacity(
        self,
        component_model_name: str,
    ) -> int:
        """
        Gets the total site capacity of a component by model name.

        :param component_model_name: component model name
        :type component_model_name: String
        :return: total component capacity
        :rtype: int
        """
        component_capacity = 0
        try:
            for h in self.hosts.values():
                component_capacity += h.get_component_capacity(
                    component_model_name=component_model_name
                )
            return component_capacity
        except Exception as e:
            # log.error(f"Failed to get {component_model_name} capacity {site}: {e}")
            return component_capacity

    def get_component_allocated(
        self,
        component_model_name: str,
    ) -> int:
        """
        Gets gets number of currently allocated components on a the site
        by the component by model name.

        :param component_model_name: component model name
        :type component_model_name: String
        :return: currently allocated component of this model
        :rtype: int
        """
        component_allocated = 0
        try:
            for h in self.hosts.values():
                component_allocated += h.get_component_allocated(
                    component_model_name=component_model_name
                )
            return component_allocated
        except Exception as e:
            # log.error(f"Failed to get {component_model_name} allocated {site}: {e}")
            return component_allocated

    def get_component_available(
        self,
        component_model_name: str,
    ) -> int:
        """
        Gets gets number of currently available components on the site
        by the component by model name.

        :param component_model_name: component model name
        :type component_model_name: String
        :return: currently available component of this model
        :rtype: int
        """
        try:
            return self.get_component_capacity(
                component_model_name
            ) - self.get_component_allocated(component_model_name)
        except Exception as e:
            # log.debug(f"Failed to get {component_model_name} available {site}")
            return self.get_component_capacity(component_model_name)

    def get_fim(self) -> node.Node:
        """
        Get the FIM object of the site.

        :return: The FIM of the site.
        :rtype: node.Node
        """
        return self.site

    def get_core_capacity(self) -> int:
        """
        Gets the total number of cores at the site

        :return: core count
        :rtype: int
        """
        try:
            return self.site.capacities.core
        except Exception as e:
            # log.debug(f"Failed to get core capacity {site}")
            return 0

    def get_core_allocated(self) -> int:
        """
        Gets the number of currently allocated cores at the site

        :return: core count
        :rtype: int
        """
        try:
            return self.site.capacity_allocations.core
        except Exception as e:
            # log.debug(f"Failed to get cores allocated {site}")
            return 0

    def get_core_available(self) -> int:
        """
        Gets the number of currently available cores at the site

        :return: core count
        :rtype: int
        """
        try:
            return self.get_core_capacity() - self.get_core_allocated()
        except Exception as e:
            # log.debug(f"Failed to get cores available {site}")
            return self.get_core_capacity()

    def get_ram_capacity(self) -> int:
        """
        Gets the total amount of memory at the site in GB

        :return: ram in GB
        :rtype: int
        """
        try:
            return self.site.capacities.ram
        except Exception as e:
            # log.debug(f"Failed to get ram capacity {site}")
            return 0

    def get_ram_allocated(self) -> int:
        """
        Gets the amount of memory currently  allocated the site in GB

        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: ram in GB
        :rtype: int
        """
        try:
            return self.site.capacity_allocations.ram
        except Exception as e:
            # log.debug(f"Failed to get ram allocated {site}")
            return 0

    def get_ram_available(self) -> int:
        """
        Gets the amount of memory currently  available the site in GB

        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: ram in GB
        :rtype: int
        """
        try:
            return self.get_ram_capacity() - self.get_ram_allocated()
        except Exception as e:
            # log.debug(f"Failed to get ram available {site_name}")
            return self.get_ram_capacity()

    def get_disk_capacity(self) -> int:
        """
        Gets the total amount of disk available the site in GB

        :return: disk in GB
        :rtype: int
        """
        try:
            return self.site.capacities.disk
        except Exception as e:
            # log.debug(f"Failed to get disk capacity {site}")
            return 0

    def get_disk_allocated(self) -> int:
        """
        Gets the amount of disk allocated the site in GB

        :return: disk in GB
        :rtype: int
        """
        try:
            return self.site.capacity_allocations.disk
        except Exception as e:
            # log.debug(f"Failed to get disk allocated {site}")
            return 0

    def get_disk_available(self) -> int:
        """
        Gets the amount of disk available the site in GB

        :param site: site name or object
        :type site: String or Node or NodeSliver
        :return: disk in GB
        :rtype: int
        """
        try:
            return self.get_disk_capacity() - self.get_disk_allocated()
        except Exception as e:
            # log.debug(f"Failed to get disk available {site_name}")
            return self.get_disk_capacity()

    def get_host_names(self) -> List[str]:
        """
        Gets a list of all currently available hosts

        :return: list of host names
        :rtype: List[String]
        """
        return list(self.hosts.keys())