import socket
import struct
import time
import os
import sys
from typing import Optional, Tuple, Dict
from datetime import datetime

# =================== Форматы структур ===================

# Формат для входящей структуры from_dev_bcst_t (начальный пакет)
INPUT_STRUCT_FORMAT = "<IIIIII6s2s4sIII128sI"
INPUT_PACKET_SIZE = struct.calcsize(INPUT_STRUCT_FORMAT)

# Формат для отправляемой структуры from_serv_update_t (команда)
CMD_STRUCT_FORMAT = "<IIIIIII256sI"
CMD_PACKET_SIZE = struct.calcsize(CMD_STRUCT_FORMAT)

# Формат для ответной структуры from_dev_update_t (результат команды)
RESPONSE_STRUCT_FORMAT = "<II6s2s4sIIIII256sI"
RESPONSE_PACKET_SIZE = struct.calcsize(RESPONSE_STRUCT_FORMAT)

# =================== Константы ===================

CRC32_POLY = 0xEDB88320
MAGIC_NUMER = 0x2313E27B
SU_PORT = 5008  # Порт, на который сервер ждет ответ

# Команды
CMD_NOP = 0
CMD_ACTIVATE_BL = 1
CMD_UNBLOCK_FLASH = 2
CMD_ERASE_UP_FLASH = 3
CMD_SET_PASS_BIN = 4
CMD_WRITE_UP_FLASH = 5
CMD_GETCRC_FLASH = 6
CMD_JUMP_TO_APP = 7
CMD_GET_CONFIG = 9

# Коды результатов выполнения команд
OP_ERR = 0
OP_OK = 1
OP_FLASH_LOCK = 2
OP_ADDRESS_ERR = 3
OP_PAGE_NO_ERASE = 4
OP_PAGE_NO_WRITE = 5
OP_CRC_BAD = 6
OP_NOT_VALID_PASS = 7
OP_OB_NO_ERASE = 8
OP_OB_NO_WRITE = 9
OP_ERR_PASS = 10
OP_BL_LOCK_PIN = 11

# Константы для программирования Flash
SIZE_PAGE_FLASH = 0x100  # 256 байт - размер блока для программирования flash
APPLICATION_BEGIN = 0x8010000  # адрес старта пользовательского приложения UP1
APPLICATION_SIZE = 0x400 * 120  # 120кБ - размер пользовательского приложения (0x1E000 = 122,880 байт)
TOTAL_PAGES = APPLICATION_SIZE // SIZE_PAGE_FLASH  # 480 страниц

STR_VERSION = 1  # Версия структуры

# Глобальные переменные для массового обновления
processed_devices = {}  # Словарь для хранения обработанных устройств

# =================== Вспомогательные функции ===================

def calculate_crc32(data: bytes, init_value: int = 0xFFFFFFFF) -> int:
    """Вычисление CRC-32 по алгоритму из спецификации"""
    crc = init_value
    
    for byte in data:
        val = (crc ^ byte) & 0xFF
        
        for _ in range(8):
            if val & 1:
                val = (val >> 1) ^ CRC32_POLY
            else:
                val = val >> 1
        
        crc = val ^ (crc >> 8)
    
    return crc ^ 0xFFFFFFFF


def format_mac(mac_bytes: bytes) -> str:
    """Форматирование MAC-адреса в читаемый вид"""
    if isinstance(mac_bytes, bytes) and len(mac_bytes) == 6:
        return ":".join(f"{b:02}" for b in mac_bytes)
    else:
        return f"Некорректный MAC: {mac_bytes!r}"


def format_ip(ip_bytes: bytes) -> str:
    """Форматирование IP-адреса в читаемый вид"""
    if isinstance(ip_bytes, bytes) and len(ip_bytes) == 4:
        return ".".join(str(b) for b in ip_bytes)
    else:
        return f"Некорректный IP: {ip_bytes!r}"


def is_broadcast_packet_quick_check(data: bytes) -> bool:
    """
    Быстрая проверка пакета на признаки broadcast.
    Проверяет magic_number без полного парсинга.
    """
    if len(data) < 8:  # Нужно как минимум 8 байт для проверки magic_number
        return False
    
    try:
        # В little-endian формате magic_number находится по смещению 4 байта
        # (после vers который занимает 4 байта)
        magic_number = struct.unpack('<I', data[4:8])[0]
        return magic_number == 0  # magic_number = 0 означает broadcast
    except:
        return False


def parse_config_ext_area(ext_area: bytes) -> dict:
    """Парсинг ext_area для команды CMD_GET_CONFIG"""
    config = {}
    
    if not isinstance(ext_area, bytes) or len(ext_area) != 256:
        return {"error": "Некорректный формат ext_area"}
    
    try:
        if len(ext_area) >= 4:
            jumpers_mask = struct.unpack('<I', ext_area[0:4])[0]
            config['jumpers_mask'] = jumpers_mask
            config['jumpers_binary'] = f"{jumpers_mask:032b}"
            config['jumpers_hex'] = f"0x{jumpers_mask:08X}"
            
            bit0 = (jumpers_mask >> 0) & 1
            config['bit0'] = bit0
            config['bit0_description'] = "Нулевой бит установлен (1)" if bit0 == 1 else "Нулевой бит сброшен (0)"
        
        return config
        
    except Exception as e:
        return {"error": f"Ошибка парсинга конфигурации: {e}", "raw_data": ext_area.hex()}


def get_result_description(get_cmd: int, get_result: int) -> str:
    """Получение описания результата выполнения команды"""
    result_codes = {
        OP_ERR: "OP_ERR - Общая ошибка выполнения",
        OP_OK: "OP_OK - Операция выполнена успешно",
        OP_FLASH_LOCK: "OP_FLASH_LOCK - Flash память заблокирована",
        OP_ADDRESS_ERR: "OP_ADDRESS_ERR - Ошибка адреса",
        OP_PAGE_NO_ERASE: "OP_PAGE_NO_ERASE - Не удалось стереть страницу",
        OP_PAGE_NO_WRITE: "OP_PAGE_NO_WRITE - Не удалось записать страницу",
        OP_CRC_BAD: "OP_CRC_BAD - Неверная контрольная сумма",
        OP_NOT_VALID_PASS: "OP_NOT_VALID_PASS - Неверный пароль",
        OP_OB_NO_ERASE: "OP_OB_NO_ERASE - Не удалось стереть Option Bytes",
        OP_OB_NO_WRITE: "OP_OB_NO_WRITE - Не удалось записать Option Bytes",
        OP_ERR_PASS: "OP_ERR_PASS - Ошибка пароля",
        OP_BL_LOCK_PIN: "OP_BL_LOCK_PIN - Bootloader заблокирован пин-кодом"
    }
    
    activate_bl_results = {
        OP_OK: "OK: Bootloader активирован",
        OP_FLASH_LOCK: "ОШИБКА: Flash память заблокирована",
        OP_NOT_VALID_PASS: "ОШИБКА: Неверный пароль",
        OP_ERR_PASS: "ОШИБКА: Ошибка пароля",
        OP_BL_LOCK_PIN: "ОШИБКА: Bootloader заблокирован пин-кодом",
    }
    
    base_desc = result_codes.get(get_result, f"Неизвестный код результата: {get_result}")
    
    if get_cmd == CMD_ACTIVATE_BL:
        specific_desc = activate_bl_results.get(get_result)
        if specific_desc:
            return specific_desc
    
    return base_desc


def get_command_name(cmd_code: int) -> str:
    """Получение имени команды по коду"""
    cmd_names = {
        CMD_NOP: "CMD_NOP",
        CMD_ACTIVATE_BL: "CMD_ACTIVATE_BL",
        CMD_UNBLOCK_FLASH: "CMD_UNBLOCK_FLASH",
        CMD_ERASE_UP_FLASH: "CMD_ERASE_UP_FLASH",
        CMD_SET_PASS_BIN: "CMD_SET_PASS_BIN",
        CMD_WRITE_UP_FLASH: "CMD_WRITE_UP_FLASH",
        CMD_GETCRC_FLASH: "CMD_GETCRC_FLASH",
        CMD_JUMP_TO_APP: "CMD_JUMP_TO_APP",
        CMD_GET_CONFIG: "CMD_GET_CONFIG",
    }
    return cmd_names.get(cmd_code, f"Неизвестная команда ({cmd_code})")


def find_bct_file() -> Optional[Tuple[str, bytes, int]]:
    """Поиск файла .bct в текущей директории"""
    try:
        files = os.listdir('.')
        bct_files = [f for f in files if f.lower().endswith('.bct')]
        
        if not bct_files:
            print("Файлы .bct не найдены в текущей директории")
            return None
        
        filename = bct_files[0]
        
        with open(filename, 'rb') as f:
            file_data = f.read()
        
        if len(file_data) != APPLICATION_SIZE:
            print(f"Размер файла {filename} ({len(file_data)} байт) не соответствует ожидаемому ({APPLICATION_SIZE} байт)")
            return None
        
        print(f"Найден файл: {filename}")
        print(f"Размер: {len(file_data)} байт")
        print(f"Ожидаемое количество страниц: {TOTAL_PAGES} по {SIZE_PAGE_FLASH} байт")
        
        file_crc = calculate_crc32(file_data)
        expected_crc_str = f"{file_crc:08X}"
        
        file_name_without_ext = os.path.splitext(filename)[0].upper()
        
        if file_name_without_ext == expected_crc_str:
            print(f"CRC32 файла совпадает с именем: 0x{expected_crc_str}")
        else:
            print(f"Имя файла ({file_name_without_ext}) не совпадает с вычисленным CRC32 (0x{expected_crc_str})")
            print(f"Вычисленный CRC32 файда: 0x{expected_crc_str}")
        
        return filename, file_data, file_crc
        
    except Exception as e:
        print(f"Ошибка при поиске/чтении файла .bct: {e}")
        return None


def print_progress_bar(iteration: int, total: int, prefix: str = '', suffix: str = '', 
                       length: int = 40, fill: str = '=', print_end: str = "\r"):
    """
    Вывод прогресс-бара
    """
    percent = ("{0:.1f}").format(100 * (iteration / float(total)))
    filled_length = int(length * iteration // total)
    bar = fill * filled_length + '-' * (length - filled_length)
    
    print(f'\r{prefix} |{bar}| {percent}% {suffix}', end=print_end, flush=True)
    
    if iteration == total:
        print()


# =================== Функции для работы со структурами ===================

def parse_initial_packet(data: bytes) -> Optional[dict]:
    """Парсинг начального пакета from_dev_bcst_t"""
    if len(data) < INPUT_PACKET_SIZE:
        return None
    
    try:
        unpacked = struct.unpack(INPUT_STRUCT_FORMAT, data[:INPUT_PACKET_SIZE])
        
        packet = {
            'vers': unpacked[0],
            'magic_number': unpacked[1],
            'ver_hw': unpacked[2],
            'ver_bl': unpacked[3],
            'num_key': unpacked[4],
            'crc_bl': unpacked[5],
            'my_mac': unpacked[6],
            'tmp': unpacked[7],
            'my_ip': unpacked[8],
            'my_port': unpacked[9],
            'crc_uflash': unpacked[10],
            'n_frame': unpacked[11],
            'ext_area': unpacked[12],
            'crc_struct': unpacked[13]
        }
        
        return packet
    except struct.error:
        return None


def parse_response_packet(data: bytes) -> Optional[dict]:
    """Парсинг ответного пакета from_dev_update_t"""
    if len(data) < RESPONSE_PACKET_SIZE:
        if len(data) >= INPUT_PACKET_SIZE:
            initial_packet = parse_initial_packet(data)
            if initial_packet:
                return {
                    'vers': initial_packet['vers'],
                    'magic_number': initial_packet['magic_number'],
                    'my_mac': initial_packet['my_mac'],
                    'tmp': initial_packet['tmp'],
                    'my_ip': initial_packet['my_ip'],
                    'my_port': initial_packet['my_port'],
                    'n_frame': initial_packet['n_frame'],
                    'get_cmd': CMD_NOP,
                    'get_result': OP_ERR,
                    'time_proc': 0,
                    'ext_area': initial_packet['ext_area'],
                    'crc_struct': initial_packet['crc_struct']
                }
        return None
    
    try:
        unpacked = struct.unpack(RESPONSE_STRUCT_FORMAT, data[:RESPONSE_PACKET_SIZE])
        
        packet = {
            'vers': unpacked[0],
            'magic_number': unpacked[1],
            'my_mac': unpacked[2],
            'tmp': unpacked[3],
            'my_ip': unpacked[4],
            'my_port': unpacked[5],
            'n_frame': unpacked[6],
            'get_cmd': unpacked[7],
            'get_result': unpacked[8],
            'time_proc': unpacked[9],
            'ext_area': unpacked[10],
            'crc_struct': unpacked[11]
        }
        
        return packet
    except struct.error:
        return None


def create_command_packet(device_info: dict, cmd: int = CMD_ACTIVATE_BL, 
                          f_address: int = 0, size: int = 0, ext_area: bytes = None) -> Tuple[bytes, dict]:
    """Создание команды структуры from_serv_update_t"""
    if not hasattr(create_command_packet, 'counter_frames'):
        create_command_packet.counter_frames = 0
    
    vers = STR_VERSION
    magic_number = MAGIC_NUMER
    my_port = SU_PORT
    n_frame = create_command_packet.counter_frames
    
    if ext_area is None:
        ext_area = bytes(256)
    
    create_command_packet.counter_frames += 1
    
    cmd_data_without_crc = struct.pack(
        CMD_STRUCT_FORMAT[:-1],
        vers,
        magic_number,
        my_port,
        n_frame,
        cmd,
        f_address,
        size,
        ext_area
    )
    
    crc_struct = calculate_crc32(cmd_data_without_crc)
    
    cmd_data = struct.pack(
        CMD_STRUCT_FORMAT,
        vers,
        magic_number,
        my_port,
        n_frame,
        cmd,
        f_address,
        size,
        ext_area,
        crc_struct
    )
    
    cmd_info = {
        'vers': vers,
        'magic_number': magic_number,
        'my_port': my_port,
        'n_frame': n_frame,
        'cmd': cmd,
        'cmd_name': get_command_name(cmd),
        'f_address': f_address,
        'size': size,
        'ext_area_size': len(ext_area),
        'crc_struct': crc_struct,
        'device_ip': device_info['public_ip'],  # Публичный IP устройства
        'device_port': SU_PORT,                 # Фиксированный порт SU_PORT!
        'device_local_ip': device_info.get('local_ip', ''),
        'device_local_port': device_info.get('local_port', 0),
        'device_n_frame': device_info['n_frame'],
        'packet_size': len(cmd_data)
    }
    
    return cmd_data, cmd_info


# =================== Функции обновления устройства ===================

def send_command_and_wait(sock, device_info, cmd: int, cmd_name: str, 
                          max_attempts: int = 3, timeout: float = 3.0,
                          custom_ext_area: bytes = None, verbose: bool = True) -> Tuple[bool, Optional[dict]]:
    """Универсальная функция для отправки команды и ожидания ответа"""
    cmd_data, cmd_info = create_command_packet(
        device_info,  # Передаем весь device_info
        cmd,
        0,
        0,
        custom_ext_area
    )
    
    response_received = False
    attempt = 0
    response_packet = None
    
    while not response_received and attempt < max_attempts:
        attempt += 1
        
        if verbose:
            print(f"Попытка {attempt}/{max_attempts}...")
        
        try:
            # Отправляем на публичный IP устройства, порт SU_PORT
            sock.sendto(cmd_data, (device_info['public_ip'], SU_PORT))
            
            try:
                sock.settimeout(timeout)
                data, address = sock.recvfrom(65535)
                
                response_packet = parse_response_packet(data)
                
                if response_packet:
                    if len(data) == INPUT_PACKET_SIZE:
                        data_without_crc = data[:INPUT_PACKET_SIZE-4]
                        calculated_crc = calculate_crc32(data_without_crc)
                        crc_valid = (response_packet['crc_struct'] == calculated_crc)
                    else:
                        data_without_crc = data[:RESPONSE_PACKET_SIZE-4]
                        calculated_crc = calculate_crc32(data_without_crc)
                        crc_valid = (response_packet['crc_struct'] == calculated_crc)
                    
                    if crc_valid:
                        if response_packet['get_cmd'] == cmd:
                            response_received = True
                            result = response_packet['get_result']
                            
                            if result == OP_OK:
                                if verbose:
                                    print(f"Команда {cmd_name} выполнена успешно")
                            else:
                                result_desc = get_result_description(cmd, result)
                                print(f"Ошибка {cmd_name}: {result_desc}")
                        else:
                            if response_packet['get_cmd'] == CMD_NOP and len(data) == INPUT_PACKET_SIZE:
                                if verbose:
                                    print(f"Устройство отправило начальный пакет вместо ответа на {cmd_name}")
                                return False, response_packet
                            continue
                            
            except socket.timeout:
                if verbose:
                    print(f"Таймаут ожидания ответа на команду {cmd_name} (попытка {attempt}/{max_attempts})")
                
                if attempt < max_attempts:
                    backoff_delay = min(2 ** attempt, 10)
                    if verbose:
                        print(f"Повторная отправка через {backoff_delay} сек...")
                    time.sleep(backoff_delay)
                
        except socket.error as e:
            if verbose:
                print(f"Ошибка отправки команды {cmd_name}: {e} (попытка {attempt}/{max_attempts})")
            
            if attempt < max_attempts:
                time.sleep(2)
    
    return response_received, response_packet


def process_flash_commands(sock, device_info, config_response: dict, verbose: bool = True) -> Tuple[bool, bool]:
    """Обработка команд для работы с Flash памятью, если бит 0 установлен"""
    
    config_data = parse_config_ext_area(config_response['ext_area'])
    
    if 'error' in config_data:
        if verbose:
            print(f"Не удалось проанализировать конфигурации: {config_data['error']}")
        return False, False
    
    bit0 = config_data.get('bit0', 0)
    
    if bit0 == 1:
        if verbose:
            print("Перемычка SA1 снята, выполняем подготовку Flash...")
        
        unblock_success, unblock_response = send_command_and_wait(
            sock, device_info, CMD_UNBLOCK_FLASH, "CMD_UNBLOCK_FLASH", 
            max_attempts=3, timeout=3.0, verbose=verbose
        )
        
        if unblock_success and unblock_response and unblock_response['get_result'] == OP_OK:
            if verbose:
                print("Flash разблокирована")
            
            erase_success, erase_response = send_command_and_wait(
                sock, device_info, CMD_ERASE_UP_FLASH, "CMD_ERASE_UP_FLASH", 
                max_attempts=3, timeout=3.0, verbose=verbose
            )
            
            if erase_success and erase_response and erase_response['get_result'] == OP_OK:
                if verbose:
                    print("Flash стерта")
                return True, True
            else:
                if verbose:
                    print("Ошибка стирания Flash")
                return True, False
        else:
            if verbose:
                print("Ошибка разблокировки Flash")
            return False, False
    else:
        if verbose:
            print("Перемычка SA1 установлена, Flash защищена от записи")
        return False, False


def send_write_up_flash_command_with_retry(sock, device_info, f_address: int, page_data: bytes, 
                                           max_retries: int = 3, verbose: bool = False) -> Tuple[bool, Optional[dict]]:
    """Отправка команды CMD_WRITE_UP_FLASH с повторными попытками"""
    
    for retry in range(max_retries):
        try:
            ext_area = bytearray(256)
            ext_area[:len(page_data)] = page_data
            
            cmd_data, cmd_info = create_command_packet(
                device_info,  # Передаем весь device_info
                CMD_WRITE_UP_FLASH,
                f_address,
                SIZE_PAGE_FLASH,
                bytes(ext_area)
            )
            
            # Отправляем на публичный IP устройства, порт SU_PORT
            sock.sendto(cmd_data, (device_info['public_ip'], SU_PORT))
            
            sock.settimeout(3.0)
            data, address = sock.recvfrom(65535)
            
            response_packet = parse_response_packet(data)
            
            if response_packet:
                if response_packet['get_cmd'] == CMD_WRITE_UP_FLASH:
                    if response_packet['get_result'] == OP_OK:
                        return True, response_packet
                    else:
                        if retry < max_retries - 1:
                            time.sleep(1)
                            continue
                        else:
                            return False, response_packet
                else:
                    if response_packet['get_cmd'] == CMD_NOP and len(data) == INPUT_PACKET_SIZE:
                        if retry < max_retries - 1:
                            time.sleep(2)
                            continue
                        else:
                            return False, response_packet
            
            if retry < max_retries - 1:
                time.sleep(1)
                
        except socket.timeout:
            if retry < max_retries - 1:
                continue
            else:
                return False, None
        except Exception:
            if retry < max_retries - 1:
                time.sleep(1)
                continue
            else:
                return False, None
    
    return False, None


def program_flash_memory(sock, device_info, firmware_data: bytes) -> Tuple[bool, int]:
    """Программирование Flash памяти содержимым файла"""
    
    print(f"\nНачало программирования Flash:")
    print(f"Размер файла: {len(firmware_data)} байт")
    print(f"Количество страниц: {TOTAL_PAGES}")
    print(f"Начальный адрес: 0x{APPLICATION_BEGIN:08X}")
    
    prog_address = APPLICATION_BEGIN
    offset_address = 0
    successful_pages = 0
    total_pages = TOTAL_PAGES
    
    print_progress_bar(0, total_pages, prefix='Прогресс:', suffix='Начало', length=40, fill='=')
    
    delay_write_block = 0.1
    
    for page_idx in range(total_pages):
        start_offset = offset_address
        end_offset = offset_address + SIZE_PAGE_FLASH
        page_data = firmware_data[start_offset:end_offset]
        
        success, response = send_write_up_flash_command_with_retry(
            sock, device_info, prog_address, page_data, 
            max_retries=3, verbose=False
        )
        
        if success and response and response['get_result'] == OP_OK:
            successful_pages += 1
            
            if (page_idx + 1) % 10 == 0 or (page_idx + 1) == total_pages:
                current_address = APPLICATION_BEGIN + (page_idx * SIZE_PAGE_FLASH)
                suffix = f"Страница {page_idx+1}/{total_pages} | Адр: 0x{current_address:08X}"
                print_progress_bar(page_idx + 1, total_pages, prefix='Прогресс:', suffix=suffix, length=40, fill='=')
            
            prog_address += SIZE_PAGE_FLASH
            offset_address += SIZE_PAGE_FLASH
            
            time.sleep(delay_write_block)
            
        else:
            print(f"\nОшибка записи страницы {page_idx} по адресу 0x{prog_address:08X}")
            return False, successful_pages
    
    print(f"\nПрограммирование завершено!")
    print(f"Успешно записано страниц: {successful_pages}/{total_pages}")
    
    return True, successful_pages


def wait_for_initial_packet_unicast(sock) -> Tuple[Optional[dict], Optional[dict]]:
    """Ожидание начального пакета в режиме unicast (игнорирует broadcast)"""
    print(f"\r[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Ожидание connect-пакета от устройства")
    
    while True:
        try:
            data, address = sock.recvfrom(65535)
            
            print(f"DEBUG: Получен пакет от {address}, размер {len(data)}")
            print(f"DEBUG: Первые 180 байт: {data[:180].hex()}")
            
            # ПРОВЕРКА 1: Быстрая проверка на broadcast по magic_number
            if is_broadcast_packet_quick_check(data):
                # Игнорируем broadcast пакеты (magic_number = 0)
                continue
            
            packet = parse_initial_packet(data)
            
            if packet:
                # Проверка CRC пакета
                data_without_crc = data[:INPUT_PACKET_SIZE-4]
                calculated_crc = calculate_crc32(data_without_crc)
                crc_valid = (packet['crc_struct'] == calculated_crc)
                
                if crc_valid:
                    if packet['magic_number'] == 0:
                        # Дополнительная проверка magic_number после парсинга
                        continue
                    
                    device_local_ip = format_ip(packet['my_ip'])
                    device_local_port = packet['my_port']
                    device_n_frame = packet['n_frame']
                    
                    # Анализ ext_area для получения флагов
                    ext_area = packet['ext_area']
                    flags = 0
                    if len(ext_area) >= 4:
                        flags = struct.unpack('<I', ext_area[0:4])[0]
                    
                    # Извлечение битов из ext_area[0]
                    programming_possible = (flags >> 0) & 1  # Бит 0:
                    go_to_up1 = (flags >> 1) & 1             # Бит 1:
                    dhcp_configured = (flags >> 2) & 1       # Бит 2:
                    global_server_configured = (flags >> 3) & 1  # Бит 3:
                    
                    # ВАЖНО: Используем публичный IP из recvfrom, но порт всегда SU_PORT
                    device_info = {
                        'public_ip': address[0],             # Публичный IP устройства (от recvfrom)
                        'local_ip': device_local_ip,         # Локальный IP устройства (для информации)
                        'local_port': device_local_port,     # Локальный порт устройства (для информации)
                        'n_frame': device_n_frame,
                        'mac': packet['my_mac'],
                        'address': address,
                        'crc_bl': packet['crc_bl'],
                        'programming_possible': programming_possible,
                        'go_to_up1': go_to_up1,
                        'dhcp_configured': dhcp_configured,
                        'global_server_configured': global_server_configured
                    }
                    
                    print(f"\nУСТРОЙСТВО ОБНАРУЖЕНО:")
                    print(f"  Публичный IP: {address[0]}")
                    print(f"  Локальный адрес устройства: {device_local_ip}:{device_local_port}")
                    print(f"  MAC: {format_mac(packet['my_mac'])}")
                    print(f"  CRC бутлоадера: 0x{packet['crc_bl']:08X}")
                    print(f"  Флаги состояния BL:")
                    print(f"    • Программирование возможно: {'ДА' if programming_possible else 'НЕТ'} (бит 0)")
                    print(f"    • Переход на UP1: {'ДА' if go_to_up1 else 'НЕТ'} (бит 1)")
                    print(f"    • IP адрес получен по DHCP: {'ДА' if dhcp_configured else 'НЕТ'} (бит 2)")
                    print(f"    • Настройки для GSU: {'ДА' if global_server_configured else 'НЕТ'} (бит 3)")
                    
                    return packet, device_info
                
        except socket.timeout:
            return None, None


def process_device(sock, initial_packet: dict, device_info: dict, firmware_data: bytes, 
                  firmware_crc: int) -> dict:
    """Обработка одного устройства"""
    
    device_key = f"{device_info['public_ip']}_{format_mac(device_info['mac'])}"
    
    print(f"\n{'='*80}")
    print(f"ОБРАБОТКА УСТРОЙСТВА: {device_info['public_ip']} ({format_mac(device_info['mac'])})")
    print(f"{'='*80}")
    
    # Проверяем CRC устройства и файла  CРАЗУ
    device_crc = initial_packet['crc_uflash']
    
    print(f"Проверка CRC устройства:")
    print(f"  CRC устройства: 0x{device_crc:08X}")
    print(f"  CRC файла:      0x{firmware_crc:08X}")
    
    # Если CRC совпадают, ЗАВЕРШАЕМ обслуживание СРАЗУ
    skip_programming = (device_crc == firmware_crc)
    
    if skip_programming:
        print(f"\nУстройство уже имеет актуальное ПО UP1(CRC: 0x{device_crc:08X})")
        print("Обслуживание устройства ЗАВЕРШЕНО - обновление не требуется")
        
        # Записываем время обработки, чтобы не обрабатывать устройство снова
        processed_devices[device_key] = time.time()
        
        # Возвращаем результаты без выполнения каких-либо команд
        return {
            'device_ip': device_info['public_ip'],
            'device_mac': format_mac(device_info['mac']),
            'device_crc': device_crc,
            'device_crc_bl': device_info.get('crc_bl', 0),  # Добавляем CRC бутлоадера
            'firmware_crc': firmware_crc,
            'skip_programming': True,
            'initial_packet': True,
            'activate_bl': False,
            'get_config': False,
            'unblock_flash': False,
            'erase_flash': False,
            'set_pass_bin': False,
            'write_up_flash': False,
            'write_up_flash_pages': 0,
            'getcrc_flash': False,
            'getcrc_value': 0,
            'jump_to_app': False,
            'errors': []
        }
    
    print(f"Требуется обновление ПО UP1:")
    print(f"  CRC устройства: 0x{device_crc:08X}")
    print(f"  CRC файла:      0x{firmware_crc:08X}")
    
    # Только если CRC разные, выполняем остальные команды
    results = {
        'device_ip': device_info['public_ip'],
        'device_mac': format_mac(device_info['mac']),
        'device_crc': device_crc,
        'device_crc_bl': device_info.get('crc_bl', 0),  # Добавляем CRC бутлоадера
        'firmware_crc': firmware_crc,
        'skip_programming': False,
        'initial_packet': True,
        'activate_bl': False,
        'get_config': False,
        'unblock_flash': False,
        'erase_flash': False,
        'set_pass_bin': False,
        'write_up_flash': False,
        'write_up_flash_pages': 0,
        'getcrc_flash': False,
        'getcrc_value': 0,
        'jump_to_app': False,
        'errors': []
    }
    
    try:
        # ============ ШАГ 2: Отправка команды CMD_ACTIVATE_BL ============
        print("\nПодготовка устройства...")
        
        activate_bl_success, activate_response = send_command_and_wait(
            sock, device_info, CMD_ACTIVATE_BL, "CMD_ACTIVATE_BL", 
            max_attempts=3, timeout=3.0, verbose=True
        )
        results['activate_bl'] = activate_bl_success and activate_response and activate_response['get_result'] == OP_OK
        
        if not results['activate_bl']:
            results['errors'].append("CMD_ACTIVATE_BL не выполнена")
            print(f"Ошибка активации bootloader. Переходим к следующему устройству.")
            processed_devices[device_key] = time.time()
            return results
        
        # ============ ШАГ 3: Отправка команды CMD_GET_CONFIG ============
        get_config_success, config_response = send_command_and_wait(
            sock, device_info, CMD_GET_CONFIG, "CMD_GET_CONFIG", 
            max_attempts=3, timeout=3.0, verbose=True
        )
        results['get_config'] = get_config_success and config_response and config_response['get_result'] == OP_OK
        
        if not results['get_config']:
            results['errors'].append("CMD_GET_CONFIG не выполнена")
            print(f"Ошибка получения конфигурации. Переходим к следующему устройству.")
            processed_devices[device_key] = time.time()
            return results
        
        # ============ ШАГ 4: Отправка команд для работы с Flash памятью ============
        unblock_success, erase_success = process_flash_commands(sock, device_info, config_response, verbose=True)
        results['unblock_flash'] = unblock_success
        results['erase_flash'] = erase_success
        
        # ============ ШАГ 5: Отправка команды CMD_SET_PASS_BIN ============
        PASS_BIN_LOAD = 0x184745bf
        ext_area_bytes = bytearray(256)
        ext_area_bytes[0:4] = struct.pack('<I', PASS_BIN_LOAD)
        
        set_pass_bin_success, set_pass_response = send_command_and_wait(
            sock, 
            device_info, 
            CMD_SET_PASS_BIN, 
            "CMD_SET_PASS_BIN", 
            max_attempts=3, 
            timeout=3.0,
            custom_ext_area=bytes(ext_area_bytes),
            verbose=True
        )
        
        results['set_pass_bin'] = set_pass_bin_success and set_pass_response and set_pass_response['get_result'] == OP_OK
        
        if not results['set_pass_bin']:
            results['errors'].append("CMD_SET_PASS_BIN не выполнена")
            print(f"Ошибка установки пароля. Переходим к следующему устройству.")
            processed_devices[device_key] = time.time()
            return results
        
        # ============ ШАГ 6: Программирование Flash памяти ============
        can_program_flash = True
        config_data = parse_config_ext_area(config_response['ext_area'])
        bit0 = config_data.get('bit0', 0)
        
        if bit0 == 1 and not results['erase_flash']:
            print("\nFlash не была стерта")
            can_program_flash = False
        elif bit0 == 0:
            print("\nПеремычка SA1 установлена, Flash защищена от записи")
            print("Программирование Flash пропускается")
            can_program_flash = False
        
        if can_program_flash:
            write_success, written_pages = program_flash_memory(sock, device_info, firmware_data)
            results['write_up_flash'] = write_success
            results['write_up_flash_pages'] = written_pages
            
            if not write_success:
                results['errors'].append(f"CMD_WRITE_UP_FLASH не выполнена (записано {written_pages}/{TOTAL_PAGES} страниц)")
                print(f"Ошибка программирования Flash. Переходим к следующему устройству.")
                processed_devices[device_key] = time.time()
                return results
            
            print("Программирование Flash памяти завершено успешно!")
            
            # ============ ШАГ 7: Проверка CRC записанной памяти ============
            getcrc_success, getcrc_response = send_command_and_wait(
                sock, device_info, CMD_GETCRC_FLASH, "CMD_GETCRC_FLASH", 
                max_attempts=3, timeout=3.0, verbose=True
            )
            
            if getcrc_success and getcrc_response and getcrc_response['get_result'] == OP_OK:
                results['getcrc_flash'] = True
                
                ext_area_data = getcrc_response['ext_area']
                if len(ext_area_data) >= 4:
                    device_crc_after = struct.unpack('<I', ext_area_data[0:4])[0]
                    results['getcrc_value'] = device_crc_after
                    
                    print(f"CRC сравнение:")
                    print(f"  Файл: 0x{firmware_crc:08X}")
                    print(f"  Устройство: 0x{device_crc_after:08X}")
                    
                    if firmware_crc == device_crc_after:
                        print("CRC совпадают! Программирование выполнено корректно!")
                        
                        # ============ ШАГ 8: Переход к приложению ============
                        print("\nЗапуск приложения...")
                        
                        jump_success, jump_response = send_command_and_wait(
                            sock, device_info, CMD_JUMP_TO_APP, "CMD_JUMP_TO_APP", 
                            max_attempts=1, timeout=3.0, verbose=True
                        )
                        
                        results['jump_to_app'] = jump_success and jump_response and jump_response['get_result'] == OP_OK
                        
                        if results['jump_to_app']:
                            print("Приложение запущено!")
                        
    except Exception as e:
        error_msg = f"Исключение при обработке устройства: {str(e)}"
        results['errors'].append(error_msg)
        print(f"Критическая ошибка: {error_msg}")
    
    # Записываем время обработки в любом случае
    processed_devices[device_key] = time.time()
    
    return results


def print_device_summary(results: dict):
    """Вывод итоговой информации об обработке устройства"""
    
    print(f"\n{'='*80}")
    print(f"ИТОГ ОБРАБОТКИ УСТРОЙСТВА: {results['device_ip']} ({results['device_mac']})")
    print(f"{'='*80}")
    
    # Добавляем вывод CRC бутлоадера, если он есть в результатах
    if 'device_crc_bl' in results:
        print(f"CRC бутлоадера: 0x{results['device_crc_bl']:08X}")
    
    if results['skip_programming']:
        print(f"Статус: Устройство уже имеет актуальное ПО UP1 - ОБСЛУЖИВАНИЕ ЗАВЕРШЕНО")
        print(f"  CRC устройства: 0x{results['device_crc']:08X}")
        print(f"  CRC .bct :      0x{results['firmware_crc']:08X}")
    else:
        print(f"Статус: {'УСПЕШНО ОБНОВЛЕНО' if results['write_up_flash'] and results['getcrc_flash'] and results['jump_to_app'] else 'С ОШИБКАМИ'}")
        print(f"  CRC до:       0x{results['device_crc']:08X}")
        print(f"  CRC после:    0x{results['getcrc_value']:08X}")
        print(f"  CRC .bct :    0x{results['firmware_crc']:08X}")
    
    print(f"\nВыполненные этапы:")
    print(f"  Connect-пакет: OK")
    
    if not results['skip_programming']:
        print(f"  CMD_ACTIVATE_BL:   {'OK' if results['activate_bl'] else 'ОШИБКА'}")
        print(f"  CMD_GET_CONFIG:    {'OK' if results['get_config'] else 'ОШИБКА'}")
        print(f"  CMD_SET_PASS_BIN:  {'OK' if results['set_pass_bin'] else 'ОШИБКА'}")
        print(f"  CMD_WRITE_UP_FLASH: {'OK' if results['write_up_flash'] else 'ОШИБКА'}")
        if results['write_up_flash']:
            print(f"    Записано страниц: {results['write_up_flash_pages']}/{TOTAL_PAGES}")
        print(f"  CMD_GETCRC_FLASH:  {'OK' if results['getcrc_flash'] else 'ОШИБКА'}")
        print(f"  CMD_JUMP_TO_APP:   {'OK' if results['jump_to_app'] else 'ОШИБКА'}")
    
    if results['errors']:
        print(f"\nОшибки:")
        for error in results['errors']:
            print(f"  - {error}")
    
    print(f"{'='*80}")


def batch_update_server(sock, firmware_data: bytes, firmware_crc: int):
    """Сервер массового обновления ПО по локальной сети"""
    print("\n" + "="*80)
    print("СЕРВЕР ОБНОВЛЕНИЯ ПО TALENTUM LSU1A-PY")
    print("="*80)
    print(f"Ожидание подключения устройств...")
    print(f"Файл *.BCT: CRC32 = 0x{firmware_crc:08X}")
    print(f"Размер: {len(firmware_data)} байт")
    print("="*80)
    print("Для остановки сервера нажмите Ctrl+C")
    print("="*80)
    
    processed_count = 0
    successful_count = 0
    skipped_count = 0
    failed_count = 0
    
    while True:
        try:
            initial_packet, device_info = wait_for_initial_packet_unicast(sock)
            
            if not initial_packet or not device_info:
                continue
            
            device_key = f"{device_info['public_ip']}_{format_mac(device_info['mac'])}"
            
            if device_key in processed_devices:
                time_since_last = time.time() - processed_devices[device_key]
                if time_since_last < 60:
                    print(f"Устройство {device_info['public_ip']} уже обрабатывалось {int(time_since_last)} сек. назад. Пропускаем.")
                    continue
            
            processed_count += 1
            print(f"\nОбработка устройства #{processed_count}: {device_info['public_ip']} ({format_mac(device_info['mac'])})")
            
            results = process_device(sock, initial_packet, device_info, firmware_data, firmware_crc)
            
            # Статистика
            if results['skip_programming']:
                skipped_count += 1
            elif results['write_up_flash'] and results['getcrc_flash'] and results['jump_to_app']:
                successful_count += 1
            else:
                failed_count += 1
            
            print_device_summary(results)
            
            print(f"\nТекущая статистика:")
            print(f"  Всего обработано: {processed_count}")
            print(f"  Успешно обновлено: {successful_count}")
            print(f"  Пропущено (CRC совпадает): {skipped_count}")
            print(f"  С ошибками: {failed_count}")
            print(f"{'='*80}")
            
        except KeyboardInterrupt:
            print("\n\nОстановка сервера массового обновления...")
            break
        except Exception as e:
            print(f"Ошибка при обработке пакета: {e}")
            continue


# =================== Основная функция ===================

def main():
    """Сервер массового обновления ПО для локальной сети """
    
    print("Инициализация...")
    
    firmware_info = find_bct_file()
    if not firmware_info:
        print("Не найден файл *.BCT для программирования")
        return
    
    filename, firmware_data, firmware_crc = firmware_info
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # Отключаем прием broadcast пакетов
    # На Windows это работает по умолчанию
    sock.settimeout(600.0)
    
    try:
        server_address = ('', SU_PORT)
        sock.bind(server_address)
        
        print(f"Сервер запущен на порту {SU_PORT}")        
        batch_update_server(sock, firmware_data, firmware_crc)
                
    except KeyboardInterrupt:
        print("\n\nСервер остановлен по запросу пользователя")
    except Exception as e:
        print(f"Ошибка: {e}")
        import traceback
        traceback.print_exc()
    finally:
        sock.close()
        print("\nСокет закрыт")


if __name__ == "__main__":
    main()