#! /usr/bin/env python
# -*- coding: utf-8 -*-
import os
import json
import re
import httpx
import asyncio
import requests
import websockets
import socket
import uuid
from copy import deepcopy
from fastapi import FastAPI
from common.runtime import load_root_env, validate_env, as_bool, as_float, as_int, as_str
from datetime import datetime, timedelta
import logging 
from src.utils.jammer_state_flag import set_jammer_active

logging.basicConfig(level=logging.INFO)

app = FastAPI()

############################################################################
# VARIABLES
############################################################################
load_root_env(__file__)
validate_env("src/server_to_master.py", {
    "lochost": as_str,
    "locport": as_int,
    "jamhost": as_str,
    "jamport": as_int,
    "master_server_ip": as_str,
    "master_server_port": as_int,
    "freqs": as_str,
    "num_of_clear_packs": as_int,
    "threshold_to_alarm": as_int,
    "time_to_jam": as_int,
    "time_to_fresh": as_int,
    "active_interval_to_send": as_int,
    "passive_interval_to_send": as_int,
    "jammer_timeout": as_int,
    "master_timeout": as_int,
    "debug_module_flag": as_bool,
    "send_to_module_flag": as_bool,
    "send_to_master_flag": as_bool,
    "send_to_jammer_flag": as_bool,
    "latitude": as_float,
    "longitude": as_float,
})
lochost = os.getenv('lochost')
locport = os.getenv('locport')
jamhost = os.getenv('jamhost')
jamport = os.getenv('jamport')
master_server_ip = os.getenv('master_server_ip')
master_server_port = os.getenv('master_server_port')

freqs = [str(x) for x in os.getenv('freqs').split(',')]
num_of_clear_packs = int(os.getenv('num_of_clear_packs'))
threshold_to_alarm = int(os.getenv('threshold_to_alarm'))

time_to_jam = int(os.getenv('time_to_jam'))
time_to_fresh = int(os.getenv('time_to_fresh'))
active_interval_to_send = int(os.getenv('active_interval_to_send'))
passive_interval_to_send = int(os.getenv('passive_interval_to_send'))
jammer_timeout = int(os.getenv('jammer_timeout'))
master_timeout = int(os.getenv('master_timeout'))

debug_module_flag = as_bool(os.getenv('debug_module_flag', '0'))
send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0'))
send_to_master_flag = as_bool(os.getenv('send_to_master_flag', '0'))
send_to_jammer_flag = as_bool(os.getenv('send_to_jammer_flag', '0'))

latitude = float(os.getenv('latitude'))
longitude = float(os.getenv('longitude'))

flag = 0
max_len_bulk = 1
bulk_data = []

sending_data_task = None
jam_server_connect = None

alarm = False
jammer_event = False

data_queue = [None] * len(freqs)
freqs_alarm = {freq: 0 for freq in freqs}


#TODO:
# 1. Вырезать flag, если нужно и возможно.
# 2. Пофиксить костыль с asyncio.create_task(sending_data) - после jammer_event'a перестает подавать признаки жизни,
#    поэтому гасим, когда включается глушилка и запускаем заново, когда глушилка отключается.
# 3. Пофиксить момент с data_queue:из-за асинхронных функций и старой реализации сервака происходит так ,что
#    прилетает пакет аларма, система это видит, хочет его отправить, начинает отправку и из-за того, что это немного
#    долгий процесс, то успевает прилететь чистый пакет и на мастер улетает чистый пакет.
# 4. Добавить print, только если deub_module_flag.


def ensure_sending_data_task():
    global sending_data_task

    if sending_data_task is None or sending_data_task.done():
        sending_data_task = asyncio.create_task(sending_data())


async def stop_sending_data_task():
    global sending_data_task

    if sending_data_task is None:
        return

    task = sending_data_task
    sending_data_task = None

    if task.done():
        return

    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        pass


############################################################################
# GPS MODULE - INACTIVE
############################################################################
# Создание планировщика
# scheduler = BackgroundScheduler(daemon=True)
# scheduler.start()
# @app.route('/get_gps', methods = ['POST'])
# @scheduler.scheduled_job(IntervalTrigger(minutes=1))
# def update_gps_coordinates():
#     # data_gps = request.json
#     result = {
#         'latitude': latitude,
#         'longitude': longitude
#     }
#     try:
#         url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address)
#         response = requests.post(url, json=result)
#         if response.status_code == 200:
#             print('gps успешно отправлен')
#         else:
#             print('gps не был отправлен')
#
#     except Exception:
#         print('gps не были отправлены из-за отстутствия сервера в поле видимости')
#     return result
#
#
# @scheduler.scheduled_job(IntervalTrigger(seconds=10))
# def send_gps_to_master():
#     try:
#         subprocess.run(["python3", "GPS_get_coords.py"])
#         #mac_address = get_mac_address()
#         data_gps = update_gps_coordinates()
#         url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address)
#         response = requests.post(url, json=data_gps)
#         if response.status_code == 200:
#             print('gps успешно отправлен')
#         else:
#             print('gps не был отправлены по какой-то причине')
#     except Exception:
#         print('gps не были отправлены по причине отсутствия сервера в поле видимости')
#


############################################################################
# MODULE RIGISTR
############################################################################
def _normalize_mac(value: str | None):
    if not value:
        return None
    value = value.strip().lower().replace('-', ':')
    if not re.fullmatch(r"[0-9a-f]{2}(:[0-9a-f]{2}){5}", value):
        return None
    return value


def get_mac_address(interface='enp5s0'):
    """
    Получить MAC текущего устройства.
    Приоритет: module_mac из env -> uuid.getnode().
    """
    env_mac = _normalize_mac(os.getenv('module_mac'))
    if env_mac:
        return env_mac

    try:
        mac_int = uuid.getnode()
        mac = ':'.join(f'{(mac_int >> shift) & 0xff:02x}' for shift in range(40, -1, -8))
        return _normalize_mac(mac)
    except Exception as e:
        print('Ошибка при получении MAC-адреса:' + str(e))
        return None


def get_ip_address(interface='enp5s0'):
    """
    Получить IP текущего устройства.
    Приоритет: module_ip из env -> исходящий IP до master_server.
    """
    env_ip = os.getenv('module_ip')
    if env_ip:
        return env_ip.strip()

    try:
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
            sock.connect((master_server_ip, int(master_server_port)))
            return sock.getsockname()[0]
    except Exception as e:
        print('Ошибка при получении IP-адреса:' + str(e))
        return None


def register_module():
    """
    Регистрация модуля на мастер сервере.
    """

    data = {'mac': get_mac_address(),
            'ip': get_ip_address()}
    try:
        url = f"http://{master_server_ip}:{master_server_port}/module/register"
        response = requests.post(url, json=data)
        response.raise_for_status()  # Проверка успешности запроса
        print("Модуль зарегистрирован успешно = ", data)
    except requests.exceptions.RequestException as e:
        flag = 1
        print("Ошибка при регистрации модуля:" + str(e), data)


############################################################################
# SEND DATA TO MASTER
############################################################################
async def send_to_master(ModuleDataSingleV2, flag):
    """
    Отправка данных на мастер по посту или через булк. По посту установлен лимит времени на отправку. В случае его
    превышения - данные не отправлены. В случа неудачи отправки по любому из методов - данные не отправлены.
    :param ModuleDataSingleV2: Пакет данных.
    :param flag:
    :return:
    """
    mac_address = get_mac_address()
    if not mac_address:
        print('MAC адрес не определен, отправка на master пропущена')
        return

    async with httpx.AsyncClient() as client:
        try:
            if flag == 0:
                url = f"http://{master_server_ip}:{master_server_port}/data/single/{mac_address}"
            else:
                url = f"http://{master_server_ip}:{master_server_port}/data/bulk/{mac_address}"
            response = await client.post(url, json=ModuleDataSingleV2, timeout=master_timeout)
            if response.status_code == 200:
                print('Данные успешно отправлены')
                flag = 0
                bulk_data.clear()
            else:
                flag = 1
                if len(bulk_data) > max_len_bulk:  # Если лимит bulk_data превышен, то удаляем первый элемент списка
                    bulk_data.pop(0)
                bulk_data.append(ModuleDataSingleV2)
                print('Данные не были отправлены по какой-то причине')
        except (httpx.RequestError, asyncio.TimeoutError) as e:
            if len(bulk_data) > max_len_bulk:  # Если лимит bulk_data превышен, то удаляем первый элемент списка
                bulk_data.pop(0)
            bulk_data.append(ModuleDataSingleV2)
            flag = 1
            print('Данные не были отправлены по причине отсутствия сервера в поле видимости')


############################################################################
# PROCESS DATA
############################################################################
async def check_alarm(amplitude: int):
    """
    Проверка амплитуды на превышение границы отработки системы.
    :param amplitude: Амплитуда.
    :return: Превышает/не превышает.
    """
    if amplitude > threshold_to_alarm:
        return True
    else:
        return False


async def agregate_data(data_to_agregate: list):
    """
    Сбор пакета для отправки на мастер сервер.
    :param data_to_agregate: Список из частотных пакетов. Длина списка = количесту обнаруживаемых частот. Может
    содержать None, если частота ничего не присылает на модуль сервер.
    :return: Пакет данных для отправки на мастер.
    """

    data = []

    if any(item is not None for item in data_to_agregate):
        for item in data_to_agregate:
            if item is not None:
                item['freq'] = int(item['freq'])
                data.append(item)

        now = datetime.utcnow() - timedelta(seconds=2)
        now = now.strftime("%Y-%m-%d %H:%M:%S")
        data = {
            "registeredAt": now,
            "data": data
        }

        for i in range(len(freqs)):
            data_queue[i] = None

        return data


async def sending_data():
    #TODO: Надо по-хорошему нормально эту функцию переписать
    """
    Отправка пакета данных на мастер сервер раз в некоторое время в определенном формате. Время отправки зависит
    от текущего статуса тревоги (аларм/не аларм).
    """

    global alarm
    global jammer_event

    while True:
        print('while true!')
        ModuleDataSingleV2 = await agregate_data(deepcopy(data_queue))
        if send_to_master_flag:
            print(f'На Мастер будет отправлена следующая информация: {ModuleDataSingleV2}')
            await send_to_master(ModuleDataSingleV2, flag)

        # Если перед отправкой на мастер все было чисто, то ждем 60 сек.
        # Если во время этих 60 сек. пришел пакет с алармом, то рассматриваем ситуации:
        if not alarm:
            for countdown in range(passive_interval_to_send, 0, -1):
                print('ТАЙМЕР ', countdown)
                await asyncio.sleep(1)
                if alarm:
                    break

        # Если стоит флаг отправить данные на джеммер и при этом еще не был получен ивент на глушилку, то
        # отправляем на джеммер данные.
        elif alarm and send_to_jammer_flag and not jammer_event:
            if await send_jam_server_alarm():
                print('Отправили на сервис подавления и все дошло успешно')
            else:
                print('Не смогли отправить на сервис подавления')

        # Сюда почему-то не заходит и вообще функция не подает признаков жизни после запуска подваителя((
        if alarm and jammer_event:
            print('ПОДАВИТЕЛЬ РАБОТАЕТ РАЗБЕГАЙСЯ ААААААААААААААААААА')

        # В случае аларма ждем секунду перед новой отправкой данных.
        if alarm:
            await asyncio.sleep(active_interval_to_send)


@app.post('/waterfall')
async def waterfall(data: dict):
    print('Received data: ', data)


@app.post('/process_data')
async def process_data(data: dict):
    """
    Прием данных со скриптов детекции в формате data = {"freq": freq,
                                                        "amplitude": amplitude
                                                        }
    где freq - строка, amplitude - int и их первичная обработка.
    :param data: словарь с двумя ключами и значениями.
    """

    global alarm

    print('Received data: ', data)
    data_dict = deepcopy(data)

    # Агрегируем  N пакетов данных от частот в один общий список, он используется в функции agregate data.
    # Каждая позиция списка фиксируется за отдельной частотой.
    freq = str(data_dict.get('freq'))
    for i in range(len(freqs)):
        if freq == freqs[i]:
            #Так делаем потому, что сервак является центром принятия решений по триггеру.
            trigger = await check_alarm(data_dict['amplitude'])
            data_dict.update({'triggered': trigger})

            data_queue[i] = deepcopy(data_dict)
            data_dict.clear()

            # Если прилетел триггер и глушилка не включена, то запускаем/обновляем счетчик чистых пакетов на этой
            # частоте.
            if trigger and not jammer_event:
                freqs_alarm[freq] = num_of_clear_packs
                print(f'freqs_alarm выглядит следующим обазом: {freqs_alarm}')

            #Если прилетел триггер и модуль еще не заалармлен, то алармим.
            if trigger and not alarm:
                print('Приелет триггерa со сканнера. Работаем, ребята!')
                alarm = True

            # Если прилетел триггер и модуль заалармлен, но при этом глушилка не работает и счетчик чистых пакетов
            # данной частоты не равен нулю, то уменьшаем его. А когда в словаре счетчиков все нули, то убираем alarm.
            elif not trigger and alarm and not jammer_event and freqs_alarm[freq] != 0:
                freqs_alarm[freq] -= 1
                print(f'Чистый пакет. Уменьшаем в выбранной частоте: {freqs_alarm}')

                if all(value == 0 for value in freqs_alarm.values()):
                    alarm = False
                    print(f'Прилетело {num_of_clear_packs}. Отключаем аларм и freqs_alarm выглядит так: {freqs_alarm}')

        else:
            continue

        print('После получения данных data_queue выглядит следующим образом: ', data_queue)


############################################################################
# JAMMER
############################################################################
async def jammer_active():
    """
    Включение подавителя.
    Отменяем таску на отправку данных на мастер. Зануляем словарь чистых пакетов и объявляем о прилете ивента с сервиса
    подавления.
    """

    global jammer_event
    global freqs_alarm

    await stop_sending_data_task()

    freqs_alarm = {freq: 0 for freq in freqs}
    jammer_event = True
    set_jammer_active(True)

    print('АКТИВИРУЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААА!!!!')
    print('-' * 20)
    print('Статус по переменным:')
    print(f'freqs_alarm: {freqs_alarm}')
    print(f'jammer_event: {jammer_event}')
    print(f'alarm: {alarm}')
    print('-' * 20)


async def jammer_deactive():
    """
    Отключение подавителя.
    Отрубаем аларм на модуле, отрубаем ивент сервера подавителей и запускаем таску отправки данных на мастер.
    :return:
    """

    global jammer_event
    global alarm
    alarm = False
    jammer_event = False
    set_jammer_active(False)
    ensure_sending_data_task()

    print('ОТКЛЮАЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААААА!!!!')
    print('-' * 20)
    print('Статус по переменным:')
    print(f'freqs_alarm: {freqs_alarm}')
    print(f'jammer_event: {jammer_event}')
    print(f'alarm: {alarm}')
    print('-' * 20)


async def send_jam_server_alarm():
    """
    Отправка алармовского пакета на сервер подавления по вебсокету. На отправку дается jammer_timeout секунд.
    При неудаче - данные не отправлены.
    """

    global jam_server_connect

    msg = {'type': 'freq_alarm',
           'data': True}

    if jam_server_connect:
        try:
            await jam_server_connect.send(json.dumps(msg))
            await asyncio.wait_for(jam_server_connect.recv(), jammer_timeout)
            return True
        except (asyncio.TimeoutError, websockets.exceptions.WebSocketException) as e:
            print(f"WebSocket error or timeout: {e}")
            return False
    else:
        return False


async def jam_server():
    """
    Прием данных по вебсокету с сервера подавления и их обработка. Включение/отключение подавителя.
    При разрыве соединения принудительно отключаем подавитель.
    """

    uri = f'ws://{jamhost}:{jamport}/ws'
    global jam_server_connect
    while True:
        try:
            jam_server_connect = await websockets.connect(uri)
            while True:
                data_from_jam_server = await jam_server_connect.recv()
                data_from_jam_server = json.loads(data_from_jam_server)
                print('Принял с сервера глушилок: ', data_from_jam_server)
                if data_from_jam_server['type'] == 'run':
                    alarm_status = (data_from_jam_server['data'])['state']
                    print(alarm_status)
                    if alarm_status and not jammer_event:
                        await jammer_active()
                    elif not alarm_status and jammer_event:
                        await jammer_deactive()
        except Exception as e:
            jam_server_connect = None
            set_jammer_active(False)
            if jammer_event:
                await jammer_deactive()



@app.on_event("startup")
async def startup_event():
    """
    Запускаем параллельно задачи jam_server и sending_data.
    """

    set_jammer_active(False)
    asyncio.create_task(jam_server())
    ensure_sending_data_task()


if __name__ == '__main__':
    import uvicorn
    # update_gps_coordinates()
    register_module()  # Регистрация модуля на сервере
    uvicorn.run(app, host=lochost, port=int(locport))
