"""
Module used for defining the network class and it's functionality.
"""

# Relative pathing from project root
import sys
import datetime
from os import makedirs
from os.path import abspath, dirname, join, exists
from typing import Union, List, Dict

script_dir = dirname(abspath(__file__))

if script_dir in ['PKI_Practice', 'PKI Practice', 'app']:
    sys.path.append(abspath(script_dir))
elif script_dir == 'PKIPractice':
    sys.path.append(abspath(join(script_dir, '..')))
else:
    sys.path.append(abspath(join(script_dir, '../..')))

from PKIPractice.Simulation.Holder import PKIHolder


class PKIHub:
    """
    The "infrastructure" of the network. Will be used to direct communications between holders.

    Attributes:
        outer_network (PKINetwork): The network object to access information and logging.
        mirror_store (dict): The "mirror" of the environment hierarchy in literal name.
        loc_store (dict): The location of every holder in the environment.
        send_store (dict): Where to send information for each holder, split into sending "up" or "down" the PKI
            hierarchy.

    Methods:
        receive_log(holder_cat, holder_success, holder_act, holder_output, holder_name, holder_message) -> None:
            Takes a log from a holder and sends it to network logs.
    """
    def __init__(self, network, network_store: Dict[int, List[PKIHolder]]) -> None:
        self.outer_network = network

        # Create a mirror of network and location store
        self.mirror_store: dict = {}
        self.loc_store: dict = {}  # Sender = key, Receiver = value

        for level, holders in network_store.items():
            self.mirror_store[level] = []
            for holder in holders:
                # Add the address of the holder to the store
                self.mirror_store[level].append(holder.get_addr())

                # Add the location of the holder
                self.loc_store[holder.get_addr()] = (level, len(self.mirror_store[level]) - 1)

                # Set the hub for the holder
                holder.set_hub_conn(self)

        # Use mirror store to decide who to send things to.
        self.send_store: dict = {'up': {}, 'down': {}}
        for level in self.mirror_store.keys():
            # Skip first level
            if level == 1:
                continue

            prev_level = self.mirror_store[level-1]
            prev_level_index = 0
            PREV_LEVEL_MAX = len(prev_level)

            for holder_addr in self.mirror_store[level]:
                # Record where the holder will send the message to
                self.send_store['up'][holder_addr] = prev_level[prev_level_index]

                # Record where the receiver can send the message back
                if prev_level[prev_level_index] in self.send_store['down'].keys():
                    self.send_store['down'][prev_level[prev_level_index]].append(holder_addr)
                else:
                    self.send_store['down'][prev_level[prev_level_index]] = [holder_addr]

                prev_level_index += 1
                if prev_level_index == PREV_LEVEL_MAX:
                    prev_level_index = 0

    def receive_log(self, holder_cat: str, holder_success: bool, holder_act: str, holder_output: str,
                    holder_name: str, holder_message: str) -> None:
        """
        Receives a log from a holder and saved it to network logs.
        """
        self.outer_network.log_event(
            holder_cat, holder_success, 'Holder', holder_act, holder_output, holder_name, holder_message
        )

    def forward_message(self) -> None:
        """Sends a message from one holder to another."""
        ...


class PKINetwork:
    """
    Represents a Public Key Infrastructure (PKI) network, including its configuration,
    hierarchy, and associated holders. This class provides functionality for managing
    the network, logging events, and filling configuration gaps.

    Attributes:
        network_name (str): Unique identifier for the network.
        log_save_fp (str): Where the logs will be saved to on storage drive.
        network_level_count (int): Total levels in the network hierarchy.
        network_count_by_level (List[int]): Number of holders at each level in the hierarchy.
        network_total_count (int): Total number of holders in the network.
        env_uid_hash (str): Unique identifier hash for the network environment.
        env_sig_hash (str): Signature hash used in the network.
        env_encrypt_alg (dict): Encryption algorithms supported by the network.
        env_revoc_probs (List[float]): Revocation probabilities by network level.
        env_cert_valid_durs (List[str]): Certificate validity durations by level.
        env_cache_durs (List[str]): Cache durations by network level.
        env_cooldown_durs (List[str]): Cooldown durations by network level.
        env_timeout_durs (List[str]): Timeout durations by network level.
        network (dict): Dictionary mapping network levels to their associated holders.
        network_log (List[str]): Log of events that occurred in the network.
        network_hub (PKIHub): Central hub for managing network communications.

    Methods:
        log_event(message) -> None:
            Prints the event that had happened and saves the information to network logs.
        save_logs() -> None:
            Saves the logs generated by the network to a CSV file.
        add_to_network(holder_name, holder_config, auto_config) -> bool:
            Adds a new holder to the network and returns a success status.
        get_network() -> List[PKIHolder]:
            Returns a flat list of all holders in network ordered by highest level.
        set_root_certificates() -> None:
            Sets up the certificate of root holders.
    """
    def __init__(self, name: str, auto_config: Union[dict, None], manual_config: Union[dict, None] = None) -> None:
        # Unique identifier
        self.network_name: str = name

        # Log filepath
        self.log_save_fp: str = auto_config['log_save_filepath']

        # Network counts
        self.network_level_count: int = auto_config['level_count']
        self.network_count_by_level: List[int] = auto_config['count_by_level']
        self.network_total_count: int = 0

        # Environment variables
        self.env_uid_hash: str = auto_config['uid_hash']
        self.env_sig_hash: str = auto_config['sig_hash']
        self.env_encrypt_alg: dict = auto_config['encrypt_alg']
        self.env_revoc_probs: List[float] = auto_config['revoc_probs']
        self.env_cert_valid_durs: List[str] = auto_config['cert_valid_durs']
        self.env_cache_durs: List[str] = auto_config['cache_durs']
        self.env_cooldown_durs: List[str] = auto_config['cooldown_durs']
        self.env_timeout_durs: List[str] = auto_config['timeout_durs']

        # Network hierarchy
        self.network: dict = {}
        for i in range(self.network_level_count):
            self.network[i+1] = []

        self.network_log: List[str] = ['ID, Timestamp, Category, Success, Subject, Act, Output, Origin, Message\n']

        # Log events that have already happened
        self.log_event(
            'Operations', True, 'Network', 'Initialization', 'Network',
            self.network_name, f'Network {self.network_name} created.'
        )
        self.log_event(
            'Operations', True, 'Network', 'Initialization', 'Variables',
            self.network_name, 'Environmental variables set.'
        )
        self.log_event(
            'Operations', True, 'Network', 'Initialization', 'Hierarchy',
            self.network_name, 'Empty network hierarchy created.'
        )
        self.log_event(
            'Operations', True, 'Network', 'Initialization', 'Log',
            self.network_name, 'Network log created and started.'
        )

        # Manual configuration
        if manual_config is not None:
            for holder_name, holder_config in manual_config.items():
                result: bool = self.add_to_network(holder_name, holder_config, auto_config)
                if result:
                    self.log_event(
                        'Operations', True, 'Network', 'Addition', holder_name,
                        self.network_name, f'Holder {holder_name} added to network.'
                    )
                    self.network_total_count += 1
                else:
                    self.log_event(
                        'Operations', False, 'Network', 'Omission', holder_name,
                        self.network_name, f'Invalid location configuration. {holder_name} was ignored.'
                    )

        # Filling in gaps
        auto_holder_count = 1
        for i in range(self.network_level_count):
            while len(self.network[i+1]) < self.network_count_by_level[i]:
                self.add_to_network(
                    f'holder_l{i+1}_c{auto_holder_count}',
                    {'location': {'level': i+1}},
                    auto_config
                )
                self.log_event(
                    'Operations', True, 'Network', 'Completion',
                    f'holder_l{i+1}_c{auto_holder_count}', self.network_name,
                    f'Gap found. Filler Holder #{auto_holder_count} at level {i+1} added to network.'
                )
                auto_holder_count += 1
                self.network_total_count += 1

        # Network hub
        self.network_hub = PKIHub(self, self.network)
        self.log_event(
            'Operations', True, 'Network', 'Initialization', 'Hub', self.network_name,
            'Network hub and connection information created.'
        )

    def log_event(self, category: str, is_success: bool, subject: str, act: str, output: str, origin: str,
                  message: str) -> None:
        """
        Takes a message, prints it, and saves its entry to network log with other elements.

        Categories that are used are:
            Operations - General Network activities
            PKI - Activities specific to PKI Practices.
            Communication - Activities specific to Communication.

        Args:
            category: str - The category of the message. One word.
            is_success: bool - The success status of the message.
            subject: str - The subject of the message. One word.
            act: str - The noun as an act the subject did. One word.
            output: str - The thing that resulted from the act. One word.
            origin: str - Who called the logging function. One word.
            message: str - String value to save.
        """

        id_num = len(self.network_log) - 1
        timestamp = datetime.datetime.now()
        entry = f'{id_num}, {timestamp}, {category}, {is_success}, {subject}, {act}, {output}, {origin}, {message}\n'

        print(message)
        self.network_log.append(entry)

    def save_logs(self) -> None:
        """
        Saves the logs to a csv file.
        """

        # Create one last log event
        self.log_event(
            'Operations', True, 'Network', 'Retention', 'CSV', self.network_name,
            f'Logs of network saved to {self.log_save_fp}.'
        )

        # Make a directory if it doesn't exist
        if not exists(dirname(self.log_save_fp)):
            makedirs(dirname(self.log_save_fp))

        # Save the log file
        with open(self.log_save_fp, 'w') as log_file:
            log_file.writelines(self.network_log)

    def add_to_network(self, holder_name: str, holder_config: dict, auto_config: dict) -> bool:
        """
        Takes a given holder name, holder configuration dictionary, and auto_config elements and creates a holder.
        Adds the holder to the growing network.

        Args:
            holder_name: str - The name of the manually created holder.
            holder_config: dict - The configuration settings of the holder.
            auto_config: dict - The configuration settings of the environment.

        Returns:
            bool - Success status on operation.
        """

        # Check if location is valid
        proper_keys: bool = all(
            isinstance(holder_config['location'][key], int) for key in holder_config['location'].keys()
        )
        enough_keys: bool = len(holder_config['location'].keys()) == 1

        if not proper_keys or not enough_keys:
            print(f'Invalid location configuration. {holder_name} configuration will be ignored.')
            return False

        # Create holder
        holder: PKIHolder = PKIHolder(holder_name, holder_config, auto_config)

        # Add holder to network
        level = holder_config['location']['level']
        self.network[level].append(holder)

        return True

    def get_network(self) -> List[PKIHolder]:
        """
        Returns a flat list of all holders in the environment in order of level.

        Returns:
            List[PKIHolder]: A list of all holder objects.
        """

        flat_network = [holder for level in self.network.values() for holder in level]
        return flat_network

    def set_root_certificates(self) -> None:
        """
        Sets up the root certificates for the top level them passes them all to all holders.
        """

        # Generate self-signed certificates
        for root_holder in self.network[1]:
            root_holder.gen_self_cert()
