import math
import os
import time
import json
import logging
import sys
import requests
import paho.mqtt.client as mqtt
import psutil
from time import gmtime, strftime, sleep
from datetime import datetime
from distutils.util import strtobool

from satellite_simulator import Satellite # Para hacerla bien
# from src.services.monitor.satellite_simulator import Satellite # Para el pycharm


class ResourcesMonitor:

    def __init__(self):
        os.environ['SATELLITE_NAME'] = "Ecosat-1234-5678"
        index_tag = "index_" + str(os.getenv('SATELLITE_NAME')) + "_metrics"

        self.mqtt_host = os.getenv('MQTT_HOST', 'localhost')
        self.mqtt_port = os.getenv('MQTT_PORT', 1883)
        # mqtt_topic = os.getenv('MQTT_TOPIC', 'test/topic')
        self.mqtt_topic = os.getenv('CLUSTER_NAME', 'test/topic')
        self.mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
        self.mqtt_client.connect(self.mqtt_host, self.mqtt_port, 60)

        self.initial_timestamp = time.time()
        self.initial_battery = 100
        self.charge_time = os.getenv("CHARGE_TIME", 0.5) # Charge time in hours
        self.discharge_time = os.getenv("DISCHARGE_TIME", 3) # Discharge time in hours
        self.charge_time_sec = self.charge_time*60*60
        self.discharge_time_sec = self.discharge_time*60*60
        self.battery = 100
        self.battery_state = False # False for discharge and True for charge
        self.restart_time = time.time()

        self.logger = logging.getLogger()
        self.logger.setLevel(logging.DEBUG)

        handler = logging.StreamHandler(sys.stdout)
        handler.setLevel(logging.DEBUG)

        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        handler.setFormatter(formatter)

        self.logger.addHandler(handler)

        # Crear el objeto Satelite
        self.load_configuration()
        if self.config['trajectory'] == "parabolic":
            self.satellite = Satellite(
                Satellite.PARABOLIC_TRAJECTORY,
                parabolic_vertix=self.parabolic_vertix if hasattr(self, 'parabolic_vertix') else [-3.7, 40.4],
                focal_distance=self.focal_distance if hasattr(self, 'focal_distance') else 100000,
                flight_height=self.flight_height if hasattr(self, 'flight_time') else 1000,
                flight_time=self.flight_time if hasattr(self, 'flight_time') else 10,
                time_interval=self.time_interval if hasattr(self, 'time_interval') else 0.5
            )
            # Iniciar el movimiento del satélite en un hilo
            self.satellite.iniciar_hilo()
        elif self.config['trajectory'] == 'elipsoidal':
            self.satellite = Satellite(
                Satellite.ELIPSE_TRAJECTORY,
                elipse_center_geo= self.elipse_center_geo if hasattr(self, 'elipse_center_geo') else [-3.7, 40.4],
                a_km=self.a_km if hasattr(self, 'a_km') else 500,
                b_km=self.b_km if hasattr(self, 'b_km')else 300,
                flight_height=self.flight_height if hasattr(self, 'flight_time') else 1000,
                time_interval=self.time_interval if hasattr(self, 'time_interval') else 1,
                random_center=strtobool(self.random_center) if hasattr(self, 'random_center') else True,
                num_points=self.num_points if hasattr(self, 'num_points') else 100
            )
            # Iniciar el movimiento del satélite en un hilo
            self.satellite.iniciar_hilo()
        elif self.config['trajectory'] == 'p2p':
            cities = self.cities if hasattr(self, 'cities') else {
                "Estella": "(-2.0310612055588173, 42.67367031611737)",
                "Muxia": "(-9.217775273649409, 43.10582940687305)",
                "Trujillo": "(-5.8824527238350655, 39.45804526503004)",
                "Granada": "(-3.599776119363134, 37.17421011902858)",
                "Madrid": "(-3.672190678958218, 40.49627408029931)"
            }
            self.satellite = Satellite(
                Satellite.P2P_TRAJECTORY,
                cities=cities,
                interpolate_factor=self.interpolate_factor if hasattr(self, 'interpolate_factor') else 100,
                flight_height=self.flight_height if hasattr(self, 'flight_time') else 1000,
                time_interval=self.time_interval if hasattr(self, 'time_interval') else 1
            )
            # Iniciar el movimiento del satélite en un hilo
            self.satellite.iniciar_hilo()
        else:
            self.latitude = self.config['lat']
            self.longitude = self.config['lon']
            self.altitude = self.config['alt']

#-----------------------------------------------------monitor functions-------------------------------------------------
    def _update_sim_battery(self):
        # Discharge
        if not self.battery_state: # Not battery state is equal to the battery being used end discharged
            self.previous_battery_state = False
            self.battery = self.initial_battery - ((time.time() -  self.initial_timestamp) / self.discharge_time_sec) * 100
            #self.battery = 100 - (((time.time()-self.initial_timestamp) % (self.charge_time_sec+self.discharge_time_sec) - self.charge_time_sec)/(self.discharge_time_sec))*100
            if self.battery <= 1 and not self.previous_battery_state:
                self.battery_state = True
                self.initial_battery = 0
                self.initial_timestamp = time.time()
        # Charge
        else:
            self.previous_battery_state = True
            self.battery = self.initial_battery + ((time.time() - self.initial_timestamp)/self.charge_time_sec)*100
            # self.battery = (((time.time()-self.initial_timestamp) % (self.charge_time_sec + self.discharge_time_sec))/(self.charge_time_sec))*100
            if self.battery >= 99 and self.previous_battery_state:
                self.battery_state = False
                self.initial_battery = 100
                self.initial_timestamp = time.time()

    def _get_sim_battery(self):
        return round(self.battery)

    def _parse_tuple(self, named_tuple):
        ret = {}
        fields = named_tuple._fields
        for index, field in enumerate(fields):
            ret[field] = named_tuple[index]
        return ret

    def _get_all_resources_data(self, previous_resources_data=None):
        data = {"timestamp": time.time(),
                "cpu_times": self._get_cpu_times(),
                "cpu_stats": self._get_cpu_stats(),
                "cpu_usage": self._get_cpu_usage(),
                "virtual_mem": self._get_virtual_memory(),
                "swap_mem": self._get_swap_memory(),
                "battery": self._get_sensors_battery(),
                "sim_battery": {"plugged": self.battery_state, "percent": self._get_sim_battery()},
                "disk_usage": self._get_disk_usage(),
                "disk_rw": self._get_disk_io_counters(),
                "bytes_tx_rx": self._get_bytes_tx_and_rx(),
                "net_stats": self._get_net_stats(),
                "temperature": self._get_sensor_temperature()}
        data = self._update_speed(data, previous_resources_data)
        data = self._update_cpu_switches(data, previous_resources_data)
        data = self._update_virtual_memory(data)
        data = self._update_disk_rw(data, previous_resources_data)
        return data

    def _update_virtual_memory(self, data):
        data['virtual_mem'] = {
            **data['virtual_mem'],
            **{'total_gb': data['virtual_mem']['total'] / (1024 ** 3),
               'available_gb': data['virtual_mem']['available'] / (1024 ** 3),
               'used_gb': data['virtual_mem']['used'] / (1024 ** 3),
               'free_gb': data['virtual_mem']['free'] / (1024 ** 3)
               }
        }
        return data

    def _update_cpu_switches(self, data, previous_data):
        if previous_data is None:
            data['cpu_stats'] = {
                **data['cpu_stats'],
                **{'ctx_switches_per_sec': 0.0,
                   'interrupts_per_sec': 0.0,
                   'soft_interrupts_per_sec': 0.0,
                   'syscalls_per_sec': 0.0
                   }
            }
            return data
        else:
            time_interval =  data['timestamp'] - previous_data['timestamp']
            ctx_switches = data['cpu_stats']['ctx_switches'] - previous_data['cpu_stats']['ctx_switches']
            interrupts = data['cpu_stats']['interrupts'] - previous_data['cpu_stats']['interrupts']
            soft_interrupts = data['cpu_stats']['soft_interrupts'] - previous_data['cpu_stats']['soft_interrupts']
            syscalls = data['cpu_stats']['syscalls'] - previous_data['cpu_stats']['syscalls']
            data['cpu_stats'] = {
                **data['cpu_stats'],
                **{'ctx_switches_per_sec': ctx_switches / time_interval,
                   'interrupts_per_sec': interrupts / time_interval,
                   'soft_interrupts_per_sec': soft_interrupts / time_interval,
                   'syscalls_per_sec': syscalls / time_interval
                   }
            }
            return data

    def _update_speed(self, data, previous_data):
        if previous_data is None:
            for iface in data['bytes_tx_rx'].keys():
                data['bytes_tx_rx'][iface] = {
                    **data['bytes_tx_rx'][iface],
                    **{'upload_speed': 0.0, 'download_speed': 0.0, 'bytes_sent_per_sec': 0.0, 'bytes_recv_per_sec': 0.0,
                       'packets_sent_per_sec': 0.0, 'packets_recv_per_sec': 0.0}
                }
            return data
        else:
            time_interval =  data['timestamp'] - previous_data['timestamp']
            for iface in data['bytes_tx_rx'].keys():
                bytes_tx = data['bytes_tx_rx'][iface]['bytes_sent'] - previous_data['bytes_tx_rx'][iface]['bytes_sent']
                bytes_rx = data['bytes_tx_rx'][iface]['bytes_recv'] - previous_data['bytes_tx_rx'][iface]['bytes_recv']
                pkts_tx = data['bytes_tx_rx'][iface]['packets_sent'] - previous_data['bytes_tx_rx'][iface]['packets_sent']
                pkts_rx = data['bytes_tx_rx'][iface]['packets_recv'] - previous_data['bytes_tx_rx'][iface]['packets_recv']
                data['bytes_tx_rx'][iface] = {
                    **data['bytes_tx_rx'][iface],
                    **{'upload_speed': (bytes_tx / time_interval)/(1024**2),
                       'download_speed': (bytes_rx / time_interval)/(1024**2),
                       'bytes_sent_per_sec': (bytes_tx / time_interval),
                       'bytes_recv_per_sec': (bytes_rx / time_interval),
                       'packets_sent_per_sec': (pkts_tx / time_interval),
                       'packets_recv_per_sec': (pkts_rx / time_interval)
                       }
                }
            return data

    def _update_disk_rw(self, data, previous_data):
        if previous_data is None:
            for iface in data['disk_rw'].keys():
                data['disk_rw'][iface] = {
                    **data['disk_rw'][iface],
                    **{'read_speed': 0.0, 'write_speed': 0.0}
                }
            return data
        else:
            time_interval =  data['timestamp'] - previous_data['timestamp']
            for iface in data['disk_rw'].keys():
                bytes_red = data['disk_rw'][iface]['read_bytes'] - previous_data['disk_rw'][iface]['read_bytes']
                bytes_written = data['disk_rw'][iface]['write_bytes'] - previous_data['disk_rw'][iface]['write_bytes']
                data['disk_rw'][iface] = {
                    **data['disk_rw'][iface],
                    **{'read_speed': (bytes_red / time_interval)/(1024**2),
                       'write_speed': (bytes_written / time_interval)/(1024**2)
                    }
                }
            return data

    def _get_cpu_times(self):
        cpu_times = psutil.cpu_times()
        return self._parse_tuple(cpu_times) if cpu_times else {}

    def _get_cpu_usage(self):
        cpu_usage = psutil.cpu_percent(interval=2)
        return cpu_usage

    def _get_cpu_stats(self):
        cpu_stats = psutil.cpu_stats()
        return self._parse_tuple(cpu_stats) if cpu_stats else {}

    def _get_virtual_memory(self):
        virtual_mem = psutil.virtual_memory()
        return self._parse_tuple(virtual_mem) if virtual_mem else {}

    def _get_swap_memory(self):
        swap_mem = psutil.swap_memory()
        return self._parse_tuple(swap_mem) if swap_mem else {}

    def _get_sensors_battery(self):
        sensors_battery = psutil.sensors_battery()
        return self._parse_tuple(sensors_battery) if sensors_battery else {}

    def _get_disk_usage(self):
        disk_mem = psutil.disk_usage('..')
        return self._parse_tuple(disk_mem) if disk_mem else {}

    def _get_disk_io_counters(self):
        disk_io = psutil.disk_io_counters(perdisk=True, nowrap=True)
        ret = {}
        for disk, value in disk_io.items():
            ret[disk] = self._parse_tuple(value)
        return ret if disk_io else {}

    def _get_bytes_tx_and_rx(self):
        interfaces = psutil.net_io_counters(pernic=True)
        ret = {}
        if interfaces:
            for key, value in interfaces.items():
                ret[key] = self._parse_tuple(value)
        return ret

    def _get_net_stats(self):
        interfaces = psutil.net_if_stats()
        ret = {}
        if interfaces:
            for key, value in interfaces.items():
                ret[key] = self._parse_tuple(value)
        return ret

    def _get_network_bytes(self, interface):
        net_io = psutil.net_io_counters(pernic=True)
        bytes_sent = net_io[interface].bytes_sent
        bytes_recv = net_io[interface].bytes_recv
        return bytes_sent, bytes_recv

    def _get_network_bandwidth(self, interface):
        bytes_sent_1, bytes_recv_1 = self._get_network_bytes(interface)
        time.sleep(1)
        bytes_sent_2, bytes_recv_2 = self._get_network_bytes(interface)
        upload_speed = (bytes_sent_2 - bytes_sent_1) / 1024 / 1024
        download_speed = (bytes_recv_2 - bytes_recv_1) / 1024 / 1024
        return interface, {"upload_speed": upload_speed, "download_speed": download_speed}

    def _get_sensor_temperature(self):
        hw_sensors = psutil.sensors_temperatures()
        ret = {}
        if hw_sensors:
            for key, value in hw_sensors.items():
                ret[key] = {}
                temp_params = []
                for item in value:
                    aux = self._parse_tuple(item)
                    temp_params.append(aux)
                ret[key] = temp_params
        return ret

    def get_time(self):
        """
        Elasticsearch requires timestamps to be in GMT+0
        """
        ss = strftime(f"%Y-%m-%d %H:%M:%S.{datetime.now().microsecond}", gmtime())
        timestamp = datetime.strptime(ss, "%Y-%m-%d %H:%M:%S.%f")
        return timestamp

    def get_id_from_timestamp(self):
        """
            Elasticsearch requires different id for each log
        """
        ts = time.time()
        return math.trunc(ts)
    #---------------------------------------------------------------------------------------------------------------
    #------------------------------------------------Tracker functions----------------------------------------------
    def _get_satellite_position(self,
                                obs_lat=None,
                                obs_lon=None,
                                obs_alt=None,
                                sat_id=None,
                                future_samples=None,
                                url=None):
        if url:
            URL = url
        else:
            URL = "https://api.n2yo.com/rest/v1/satellite/"
        obs_lat = 41.702 if not obs_lat else obs_lat
        obs_lon = -76.014 if not obs_lon else obs_lon
        obs_alt = 0 if not obs_alt else obs_alt
        API_KEY = 'PFUNFH-BXYW5U-LJ7XME-4T7W'
        sat_id = 39084 if not sat_id else sat_id
        future_samples = 1 if not future_samples else future_samples
        operation = "positions/{}/{}/{}/{}/{}/&apiKey={}".format(sat_id,
                                                                 obs_lat,
                                                                 obs_lon,
                                                                 obs_alt,
                                                                 future_samples,
                                                                 API_KEY)
        try:
            r = requests.get(URL + operation, verify=False).json()
            if 'error' in r.keys() and r['error'] == 'You exceeded the number of transactions allowed per hour':
                time.sleep(3610 - (time.time() - self.restart_time))
                self.restart_time = time.time()
                self.logger.info(f"Max tries exceeded at {datetime.today().strftime('%Y-%m-%d %H:%M:%S')} --> Restart time")
        except requests.exceptions.SSLError as error_ex:
            self.logger.error(error_ex)
            self.logger.info(f"Exception at {datetime.today().strftime('%Y-%m-%d %H:%M:%S')} --> exception SSL")
            time.sleep(60)
        except Exception as ex:
            raise ex
        return r

    def load_configuration(self):
        # Primero intenta obtener la variable de entorno con el nombre del archivo JSON
        json_file = os.getenv('JSON_CONFIG')

        # Si no está la variable de entorno, revisa si se pasó como argumento
        if not json_file:
            if len(sys.argv) > 1:
                json_file = sys.argv[1]
            else:
                raise ValueError("A JSON file has not been provided either through an environment variable or as an argument.")
        # Leer el archivo JSON
        try:
            with open(json_file, 'r') as file:
                self.config = json.load(file)
                for key, value in self.config.items():
                    setattr(self, key, value)
        except FileNotFoundError:
            raise FileNotFoundError(f"The file {json_file} has not been found.")
        except json.JSONDecodeError as exc:
            raise Exception(f"Error reading JSON file: {exc}")
    #-------------------------------------------------------------------------------------------------------------
    def send_to_mqtt(self, payload):
        self.mqtt_client.publish(self.mqtt_topic, payload=payload, qos=0, retain=False)

    def start_loop(self, send_interval=None):
        previous_resources_data = None
        while True:
            # ----------------------------------Tracker----------------------------------------------------------
            try:
            #     urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
            #     inital_time = time.time()
            #     r = self._get_satellite_position()
            #     """local_date_from_timestamp(r)
            #     utc_from_timestamp(r)
            #     km_to_miles(r)
            #     format_ra(r)
            #     format_declination(r)
            #     # update_speed(r, prev_lat, prev_lon, prev_alt, prev_timestamp)
            #     r["positions"][0]["speed"] = 7.5
            #     r["positions"][0]["speed_miles"] = 4.66"""
            #     tracker_data = {
            #         # "SatName": r["info"]["satname"],
            #         "SatName": os.getenv('SATELLITE_NAME'),
            #         "SatId": os.getenv('SATELLITE_ID'),
            #         "LAtitude": r["positions"][0]["satlatitude"],
            #         "Longitude": r["positions"][0]["satlongitude"],
            #         "Altitude": r["positions"][0]["sataltitude"],
            #         "Azimuth": r["positions"][0]["azimuth"],
            #         "Elevation": r["positions"][0]["elevation"],
            #         "RA": r["positions"][0]["ra"],
            #         "Dec": r["positions"][0]["dec"],
            #         "Eclipsed": r["positions"][0]["eclipsed"]
            #     }
            #     self.api_call_interval = time.time() - inital_time
            #     # # print(f'A una velocidad de {r["positions"][0]["speed"]}')
            #     # prev_lat = r["positions"][0]["satlatitude"]
            #     # prev_lon = r["positions"][0]["satlongitude"]
            #     # prev_alt = r["positions"][0]["sataltitude"]
            #     # prev_timestamp = r["positions"][0]["timestamp"]
            # except Exception as ex:
            #     # rabbitmq_client.close_connection()
            #     raise ex
                if self.config['trajectory'] != 'fixed':
                    latitude, longitude, altitude = self.satellite.get_position()
                else:
                    latitude = self.latitude
                    longitude = self.longitude
                    altitude = self.altitude
                tracker_data = {
                    # "SatName": r["info"]["satname"],
                    "SatName": os.getenv('SATELLITE_NAME'),
                    "SatId": os.getenv('SATELLITE_ID'),
                    "Latitude": latitude,
                    "Longitude": longitude,
                    "Altitude": altitude
                    # "Altitude": r["positions"][0]["sataltitude"],
                    # "Azimuth": r["positions"][0]["azimuth"],
                    # "Elevation": r["positions"][0]["elevation"],
                    # "RA": r["positions"][0]["ra"],
                    # "Dec": r["positions"][0]["dec"],
                    # "Eclipsed": r["positions"][0]["eclipsed"]
                }
            except Exception as e:
                raise e
            #------------------------------------------------------------------------------------------------
            # ------------------------------------- monitor-------------------------------------------------------
            self._update_sim_battery()
            try:
                all_resources_data = self._get_all_resources_data(previous_resources_data = previous_resources_data)
            except KeyError:
                time.sleep(10)
            self.send_to_mqtt(json.dumps({**all_resources_data, **tracker_data}))
            previous_resources_data = all_resources_data
            # ---------------------------------------------------------------------------------------------------
            if send_interval is None:
                send_interval = float(os.getenv('SEND_INTERVAL', 0))
            time.sleep(send_interval)


if __name__ == "__main__":
    monitor = ResourcesMonitor()
    try:
        monitor.start_loop()
    except Exception as e:
        raise e
