import os
import re
import time
import threading
import random
import requests
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime

from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
    QLabel, QTextEdit, QLineEdit, QCheckBox, QProgressBar, QTableWidget,
    QTableWidgetItem, QHeaderView, QFileDialog, QMessageBox, QDialog,
    QListWidget, QListWidgetItem, QSplitter, QMenu, QAction, QStyleFactory,
    QDesktopWidget, QFrame, QComboBox, QGroupBox
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QUrl, QObject 
from PyQt5.QtGui import QDesktopServices, QFont, QCursor

import sys
import uiautomator2 as u2  # Для автоматизации UI на Android

# --- Constants for File Paths (adjust if necessary) ---
TOKENS_FILE_PATH = r"токены для автопостинга.txt"
FAILED_TOKENS_FILE_PATH = r"Токены неуспешных публикаций.txt"
POSTS_DIR = r"посты"
UPLOADED_POSTS_DIR = os.path.join(POSTS_DIR, "загруженные")
AUTOPOST_STATE_FILE_PATH = r"autopost_state.txt"
POST_LIMIT_FILE_PATH = r"post_limit_state.txt"
PHOTO_COUNT_FILE_PATH = r"photo_count_state.txt"
LAST_POST_DATE_FILE_PATH = r"last_post_date.txt"
STATISTICS_FILE_PATH = r"statistics.csv"
### ИЗМЕНЕНИЕ: Новый файл для сохранения лимита смены IP
IP_CHANGE_LIMIT_FILE_PATH = r"ip_change_limit_state.txt"
CHANGE_IP_START_STATE_FILE_PATH = r"change_ip_start_state.txt"
CHANGE_IP_END_STATE_FILE_PATH = r"change_ip_end_state.txt"
LAST_MODEL_FILE_PATH = r"last_model.txt"

# --- Dark Theme Stylesheet ---
DARK_STYLESHEET = """
    QMainWindow, QDialog {
        background-color: #2b2b2b;
    }
    QWidget {
        background-color: #2b2b2b;
        color: #dcdcdc;
        font-family: Arial;
        font-size: 11pt;
    }
    QLabel {
        color: #dcdcdc;
    }
    QLineEdit, QTextEdit, QListWidget, QTableWidget, QSpinBox {
        background-color: #3c3f41;
        color: #bbbbbb;
        border: 1px solid #555555;
        selection-background-color: #54585a;
        selection-color: #ffffff;
    }
    QPushButton {
        background-color: #4a4d4f;
        color: #dcdcdc;
        border: 1px solid #555555;
        padding: 8px 12px;
        min-height: 1.5em;
    }
    QPushButton:hover {
        background-color: #5a5d5f;
    }
    QPushButton:pressed {
        background-color: #6a6d6f;
    }
    QPushButton:disabled {
        background-color: #3c3f41;
        color: #777777;
    }
    QCheckBox {
        color: #dcdcdc;
    }
    QCheckBox::indicator {
        width: 13px;
        height: 13px;
        background-color: #3c3f41;
        border: 1px solid #555;
    }
    QCheckBox::indicator:checked {
        background-color: #6897bb;
    }
    QProgressBar {
        border: 1px solid #555555;
        text-align: center;
        color: #dcdcdc;
    }
    QProgressBar::chunk {
        background-color: #6897bb;
    }
    QHeaderView::section {
        background-color: #3c3f41;
        color: #dcdcdc;
        padding: 4px;
        border: 1px solid #555555;
        font-weight: bold;
    }
    QTableWidget {
        gridline-color: #555555;
    }
    QSplitter::handle {
        background-color: #4a4d4f;
    }
    QSplitter::handle:horizontal {
        width: 5px;
    }
    QSplitter::handle:vertical {
        height: 5px;
    }
    QMenu {
        background-color: #3c3f41;
        color: #dcdcdc;
        border: 1px solid #555;
    }
    QMenu::item:selected {
        background-color: #54585a;
        color: #ffffff;
    }
    QTextEdit#successfulText {
        background-color: #274e13;
    }
    QTextEdit#failedText {
        background-color: #592323;
    }
    QLabel#successfulPostsLabel {
        color: #8fbc8f;
        font-weight: bold;
    }
    QLabel#failedPostsLabel, QLabel#tokenErrorsSpecificLabel, QLabel#bannedTokensSpecificLabel, QLabel#otherErrorsSpecificLabel {
        color: #f08080;
        font-weight: bold;
    }
    QLabel#lastPostDateLabel {
        color: #dda0dd;
        font-weight: bold;
    }
    QLabel#photosCountLabel {
        color: #87ceeb;
        font-weight: bold;
    }
    QFrame[frameShape="4"] {
        color: #555555;
        background-color: #555555;
    }
    QFrame[frameShape="5"] {
        color: #555555;
        background-color: #555555;
    }
"""
FONT_STYLE = QFont("Arial", 11)
HEADER_FONT = QFont("Arial", 12, QFont.Bold)
user_info_dict = {}
stop_flag = threading.Event()

# --- Thread-safe counter for progress tracking ---
class ThreadSafeCounter:
    def __init__(self):
        self._value = 0
        self._lock = threading.Lock()
    
    def increment(self):
        with self._lock:
            self._value += 1
            return self._value
    
    def get_value(self):
        with self._lock:
            return self._value
    
    def reset(self):
        with self._lock:
            self._value = 0

# --- Кастомный класс QTextEdit с кликабельными ссылками ---
class ClickableQTextEdit(QTextEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.anchor = None
        self.parent_widget = parent

    def mousePressEvent(self, event):
        """Обработка нажатия мыши для определения ссылки"""
        self.anchor = self.anchorAt(event.pos())
        if self.anchor:
            QApplication.setOverrideCursor(Qt.PointingHandCursor)
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """Обработка отпускания мыши для открытия ссылки"""
        if self.anchor:
            try:
                # Открываем ссылку в браузере
                QDesktopServices.openUrl(QUrl(self.anchor))
                if self.parent_widget and hasattr(self.parent_widget, 'log_message_slot'):
                    self.parent_widget.log_message_slot(f"Открыта ссылка: {self.anchor}\n")
            except Exception as e:
                if self.parent_widget and hasattr(self.parent_widget, 'log_message_slot'):
                    self.parent_widget.log_message_slot(f"Ошибка открытия ссылки {self.anchor}: {e}\n")
            finally:
                QApplication.setOverrideCursor(Qt.ArrowCursor)
                self.anchor = None
        super().mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        """Обработка движения мыши для изменения курсора"""
        anchor = self.anchorAt(event.pos())
        if anchor:
            QApplication.setOverrideCursor(Qt.PointingHandCursor)
        else:
            QApplication.setOverrideCursor(Qt.ArrowCursor)
        super().mouseMoveEvent(event)

# --- Helper Functions for UI separators ---
def create_horizontal_separator():
    """Создает горизонтальный разделитель"""
    separator = QFrame()
    separator.setFrameShape(QFrame.HLine)
    separator.setFrameShadow(QFrame.Sunken)
    return separator

def create_vertical_separator():
    """Создает вертикальный разделитель"""
    separator = QFrame()
    separator.setFrameShape(QFrame.VLine)
    separator.setFrameShadow(QFrame.Sunken)
    return separator

# --- Worker Signals ---
class WorkerSignals(QWidget):
    log_message = pyqtSignal(str)
    update_progress = pyqtSignal(int, int, int)
    successful_post = pyqtSignal(str, str)
    failed_post = pyqtSignal(str, str)
    enable_controls = pyqtSignal(bool)
    posting_finished = pyqtSignal()
    update_schedule_table = pyqtSignal(list)
    set_progress_max = pyqtSignal(int)
    increment_progress_bar = pyqtSignal()

# --- Helper Functions (Networking, VK API) ---
def natural_key(filename):
    basename = os.path.basename(filename)
    return [text for text in re.split(r'(\d+)', basename)] 

def get_current_wifi_ssid():
    try:
        process = subprocess.Popen(['netsh', 'wlan', 'show', 'interfaces'],
                                 stdout=subprocess.PIPE, text=True,
                                 encoding='utf-8', errors='ignore',
                                 creationflags=subprocess.CREATE_NO_WINDOW)
        stdout, stderr = process.communicate(timeout=10)
        if process.returncode == 0:
            for line in stdout.splitlines():
                if "SSID" in line and "BSSID" not in line:
                    match = re.search(r":\s*(.+)", line)
                    if match:
                        ssid = match.group(1).strip()
                        if ssid: return ssid
            return "Нет подключения"
        return "Ошибка опроса"
    except Exception: return "Ошибка Wi-Fi"

def get_external_ip():
    try:
        response = requests.get('https://api.ipify.org', timeout=10)
        response.raise_for_status()
        return response.text.strip()
    except requests.RequestException: return "Недоступно"

def get_location_by_ip(ip):
    try:
        response = requests.get(f'https://ipinfo.io/{ip}/json', timeout=10)
        response.raise_for_status()
        data = response.json()
        city, region, country = data.get('city', 'Н/Д'), data.get('region', 'Н/Д'), data.get('country', 'Н/Д')
        return f"{city}, {region}, {country}"
    except requests.RequestException: return "Недоступно"

def get_user_info(token):
    url = 'https://api.vk.com/method/users.get'
    params = {'access_token': token, 'v': '5.131'}
    response = requests.get(url, params=params, timeout=10)
    data = response.json()
    if 'response' in data:
        user_info = data['response'][0]
        user_info_dict[token] = f"{user_info['first_name']} {user_info['last_name']}"
        return user_info['id'], user_info_dict[token]
    elif 'error' in data and data['error'].get('error_code') == 5:
        raise Exception(f"Ошибка авторизации: {data['error']['error_msg']}")
    else:
        raise Exception("Ошибка получения информации о пользователе: " + str(data.get('error', {})))

def upload_photo(token, photo_path):
    url = 'https://api.vk.com/method/photos.getWallUploadServer'
    params = {'access_token': token, 'v': '5.131'}
    response = requests.post(url, params=params, timeout=10)
    response.raise_for_status()
    upload_info = response.json()
    if 'response' not in upload_info:
        raise Exception("Ошибка получения сервера для загрузки фото: " + str(upload_info.get('error', {})))
    upload_url = upload_info['response']['upload_url']
    with open(photo_path, 'rb') as photo_file:
        files = {'photo': photo_file}
        upload_response = requests.post(upload_url, files=files, timeout=10)
        upload_response.raise_for_status()
        upload_data = upload_response.json()
    if 'photo' not in upload_data:
        raise Exception("Ошибка загрузки фото: " + str(upload_data))
    return upload_data

def save_photo(token, upload_data, user_id):
    url = 'https://api.vk.com/method/photos.saveWallPhoto'
    params = {
        'access_token': token, 'user_id': user_id, 'server': upload_data['server'],
        'photo': upload_data['photo'], 'hash': upload_data['hash'], 'v': '5.131'
    }
    response = requests.post(url, params=params, timeout=10)
    response.raise_for_status()
    data = response.json()
    if 'response' not in data:
        raise Exception("Ошибка сохранения фото: " + str(data.get('error', {})))
    return data

def upload_video(token, video_path):
    url = 'https://api.vk.com/method/video.save'
    params = {'access_token': token, 'v': '5.131', 'name': os.path.basename(video_path)}
    response = requests.post(url, params=params, timeout=10)
    response.raise_for_status()
    video_info = response.json()
    if 'response' not in video_info:
        raise Exception("Ошибка получения сервера для загрузки видео: " + str(video_info.get('error', {})))
    upload_url = video_info['response']['upload_url']
    with open(video_path, 'rb') as video_file:
        files = {'video_file': video_file}
        upload_response = requests.post(upload_url, files=files, timeout=10)
        upload_response.raise_for_status()
    if 'video_id' not in video_info['response']:
        raise Exception("Ошибка загрузки или сохранения видео: " + str(video_info.get('error', video_info)))
    return video_info['response']

def post_to_vk(token, user_id, message, attachments):
    url = 'https://api.vk.com/method/wall.post'
    params = {
        'access_token': token, 'owner_id': user_id, 'message': message,
        'attachments': attachments, 'v': '5.131'
    }
    response = requests.post(url, params=params, timeout=10)
    response.raise_for_status() 
    data = response.json()
    if 'error' in data: 
        raise Exception("Ошибка публикации поста: " + data['error']['error_msg'])
    return data

# --- Posting Logic Thread ---
class PostingWorker(QThread):
    def __init__(self, tokens_list, posting_schedule_list, autopost_flag, post_limit_val, photo_count_val, signals_emitter, ip_change_limit, main_app):
        super().__init__()
        self.tokens_list = tokens_list
        self.posting_schedule_list = posting_schedule_list
        self.autopost_flag = autopost_flag
        self.post_limit_val = post_limit_val
        self.photo_count_val = photo_count_val
        self.thread_count_val = 10  # Фиксированное количество потоков
        self.signals = signals_emitter
        self.successful_counter = ThreadSafeCounter()
        self.failed_counter = ThreadSafeCounter()
        self.final_failed_tokens_for_ui_copy = set()
        self.final_successful_tokens_for_ui_copy = set()
        self._failed_tokens_lock = threading.Lock()
        self._successful_tokens_lock = threading.Lock()
        self.posts_since_last_ip_change = 0
        self.ip_change_limit = ip_change_limit
        self.main_app = main_app  # Ссылка на главное окно для вызова run_flight_mode_scenario
        self._ip_change_lock = threading.Lock()

    def post_to_account(self, token, user_id_param, message, photos, videos):
        if stop_flag.is_set():
            return ("Публикация остановлена пользователем.", False, None, [])
        max_attempts = 3
        attempt = 0
        user_name = ""
        actual_user_id = user_id_param
        files_to_move = []
        while attempt < max_attempts:
            if stop_flag.is_set():
                return ("Публикация остановлена.", False, None, [])
            attempt += 1
            try:
                if actual_user_id is None or attempt == 1:
                    actual_user_id, user_name = get_user_info(token)
                elif not user_name:
                     _, user_name = get_user_info(token)
                thread_id = threading.current_thread().ident
                self.signals.log_message.emit(f"[Поток {thread_id}] Публикация на аккаунте: {user_name if user_name else 'ID ' + str(actual_user_id)}... Попытка {attempt}\n")
                attachments = []
                
                # Обработка фотографий
                for photo_idx, photo in enumerate(photos):
                    if stop_flag.is_set(): return ("Публикация остановлена.", False, None, [])
                    self.signals.log_message.emit(f"[Поток {thread_id}] Загрузка фото {photo_idx+1}/{len(photos)}: {os.path.basename(photo)} на {user_name}...\n")
                    upload_data = upload_photo(token, photo)
                    saved_photo = save_photo(token, upload_data, actual_user_id)
                    if 'response' in saved_photo and saved_photo['response']:
                        attachments.append(f"photo{saved_photo['response'][0]['owner_id']}_{saved_photo['response'][0]['id']}")
                        files_to_move.append(photo)  # Добавляем в список для перемещения
                    else:
                        raise Exception(f"Не удалось сохранить фото {os.path.basename(photo)}. Ответ: {saved_photo}")
                
                # Обработка видео
                for video_idx, video in enumerate(videos):
                    if stop_flag.is_set(): return ("Публикация остановлена.", False, None, [])
                    self.signals.log_message.emit(f"[Поток {thread_id}] Загрузка видео {video_idx+1}/{len(videos)}: {os.path.basename(video)} на {user_name}...\n")
                    video_upload_response = upload_video(token, video)
                    video_owner_id = video_upload_response.get('owner_id', actual_user_id)
                    attachments.append(f"video{video_owner_id}_{video_upload_response['video_id']}")
                    files_to_move.append(video)  # Добавляем в список для перемещения
                
                result = post_to_vk(token, actual_user_id, message, ','.join(attachments))
                post_id = result['response']['post_id']
                link = f"https://vk.com/wall{actual_user_id}_{post_id}"
                return (f"[Поток {thread_id}] Пост успешно опубликован на аккаунте: {user_name}", link, actual_user_id, files_to_move)
            except (requests.ConnectionError, requests.Timeout) as net_e:
                self.signals.log_message.emit(f"[Поток {thread_id}] Сетевая ошибка: {net_e}. Повтор через 5-10 сек...\n")
                time.sleep(random.uniform(5, 10))
            except Exception as e:
                error_text = str(e) 
                user_name_display = user_name if user_name else user_info_dict.get(token, token)
                if attempt >= max_attempts:
                    error_message = f"[Поток {thread_id}] Ошибка на аккаунте {user_name_display}: {error_text} (после {max_attempts} попыток)\n"
                    return (error_message, False, token, [])
                else:
                    self.signals.log_message.emit(f"[Поток {thread_id}] Попытка {attempt} не удалась на {user_name_display}. Ошибка: {error_text}\n")
                    if "too many requests per second" in error_text.lower() or \
                       (isinstance(e, requests.exceptions.HTTPError) and e.response.status_code == 429) or \
                       ('error_code' in error_text and '6' in error_text):
                        self.signals.log_message.emit(f"[Поток {thread_id}] Обнаружена ошибка 'too many requests'. Увеличиваю задержку...\n")
                        time.sleep(random.uniform(5,10))
                    else:
                        time.sleep(random.uniform(1,3))
        return (f"Не удалось опубликовать на {user_name_display} после {max_attempts} попыток.", False, token, [])

    def move_uploaded_files(self, files):
        """Перемещает файлы в папку загруженных после успешной публикации"""
        if not files:
            return
            
        os.makedirs(UPLOADED_POSTS_DIR, exist_ok=True)
        for file_path in files:
            try:
                if os.path.exists(file_path):
                    destination = os.path.join(UPLOADED_POSTS_DIR, os.path.basename(file_path))
                    if os.path.exists(destination):
                        name, ext = os.path.splitext(os.path.basename(file_path))
                        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                        destination = os.path.join(UPLOADED_POSTS_DIR, f"{name}_{timestamp}{ext}")
                    
                    shutil.move(file_path, destination)
                    thread_id = threading.current_thread().ident
                    self.signals.log_message.emit(f"[Поток {thread_id}] Файл перемещён: {os.path.basename(file_path)} -> {os.path.basename(destination)}\n")
                else:
                    self.signals.log_message.emit(f"Файл для перемещения не найден: {os.path.basename(file_path)}\n")
            except Exception as e:
                self.signals.log_message.emit(f"Ошибка при перемещении файла {os.path.basename(file_path)}: {e}\n")

    def create_unique_posts_for_tokens(self, all_photos, photo_count_val, tokens_count):
        """Создает уникальные наборы фотографий для каждого токена"""
        unique_posts = []
        photo_index = 0
        
        for token_index in range(tokens_count):
            if photo_index >= len(all_photos):
                photo_index = 0
            
            photos_for_this_token = []
            for _ in range(photo_count_val):
                if photo_index < len(all_photos):
                    photos_for_this_token.append(all_photos[photo_index])
                    photo_index += 1
                else:
                    break
            
            if photos_for_this_token:
                unique_posts.append({
                    'text': '', 
                    'photos': photos_for_this_token, 
                    'videos': []
                })
        
        return unique_posts

    def trigger_ip_change(self):
        if self.ip_change_limit <= 0 or self.main_app.device_combobox.currentText() == "Нет подключённых устройств":
            return

        self.signals.log_message.emit(f"Достигнут лимит постов ({self.ip_change_limit}). Запуск смены IP...\n")
        try:
            self.main_app.run_flight_mode_scenario()

            max_wait = 30
            start_time = time.time()
            while time.time() - start_time < max_wait:
                ip = get_external_ip()
                if ip != "Недоступно":
                    self.signals.log_message.emit(f"Подключение к сети восстановлено. Новый IP: {ip}\n")
                    self.posts_since_last_ip_change = 0
                    return
                time.sleep(2)

            self.signals.log_message.emit("Ошибка: Не удалось подключиться к сети после смены IP (таймаут). Продолжаем без смены.\n")
        except Exception as e:
            self.signals.log_message.emit(f"Ошибка смены IP: {e}. Продолжаем без смены.\n")

    def process_posting_task(self, task_data):
        """Обработка одной задачи публикации (для многопоточности)"""
        token, entry, post_number = task_data
        
        if stop_flag.is_set():
            return None
            
        message = entry['text']
        photos_to_post = entry['photos']
        videos_to_post = entry['videos']
        
        try:
            result = self.post_to_account(token, None, message, photos_to_post, videos_to_post)
            
            if len(result) >= 4:
                status_message, outcome_is_link_or_false, problematic_item, files_to_move = result
                
                if outcome_is_link_or_false == False:
                    self.failed_counter.increment()
                    self.signals.failed_post.emit(status_message, problematic_item)
                    if problematic_item:
                        with self._failed_tokens_lock:
                            self.final_failed_tokens_for_ui_copy.add(problematic_item)
                else:
                    success_count = self.successful_counter.increment()
                    with self._successful_tokens_lock:
                        self.final_successful_tokens_for_ui_copy.add(token)
                    self.signals.successful_post.emit(status_message, outcome_is_link_or_false)
                    
                    if files_to_move:
                        self.move_uploaded_files(files_to_move)
                    
                    with self._ip_change_lock:
                        self.posts_since_last_ip_change += 1
            
            self.signals.increment_progress_bar.emit()
            
            if not stop_flag.is_set():
                time.sleep(random.uniform(1, 3))
                
        except Exception as exc:
            self.failed_counter.increment()
            display_name = user_info_dict.get(token, token)
            error_msg = f"Критическая ошибка для {display_name}: {exc}\n"
            self.signals.failed_post.emit(error_msg, token)
            with self._failed_tokens_lock:
                self.final_failed_tokens_for_ui_copy.add(token)
            
            self.signals.increment_progress_bar.emit()

    def run(self):
        stop_flag.clear()
        self.signals.enable_controls.emit(False)
        self.successful_counter.reset()
        self.failed_counter.reset()
        self.posts_since_last_ip_change = 0
        
        failed_tokens_this_run = set()

        self.signals.log_message.emit(f"Запуск публикации с использованием {self.thread_count_val} потоков...\n")

        if self.autopost_flag:
            if not os.path.exists(POSTS_DIR):
                self.signals.log_message.emit(f"Папка {POSTS_DIR} не найдена для автопостинга.\n")
                self.signals.posting_finished.emit()
                return
            
            all_photos = sorted([
                os.path.join(POSTS_DIR, f) for f in os.listdir(POSTS_DIR)
                if f.lower().endswith(('.jpg', '.jpeg', '.png'))
            ], key=natural_key)
            
            if not all_photos:
                self.signals.log_message.emit("В папке 'посты' нет фотографий для автопостинга.\n")
                self.signals.posting_finished.emit()
                return
            
            total_posts_needed = len(self.tokens_list) * self.post_limit_val
            unique_posts = self.create_unique_posts_for_tokens(all_photos, self.photo_count_val, total_posts_needed)
            
            self.signals.log_message.emit(f"Автопостинг: сформировано {len(unique_posts)} уникальных постов для {len(self.tokens_list)} аккаунтов.\n")
            
            if self.signals.update_schedule_table:
                self.signals.update_schedule_table.emit(list(unique_posts))
        else:
            unique_posts = self.posting_schedule_list

        if not self.tokens_list:
            self.signals.log_message.emit("Нет доступных токенов для публикации.\n")
            self.signals.posting_finished.emit()
            return

        if not unique_posts:
            self.signals.log_message.emit("Нет наборов публикаций для выполнения.\n")
            self.signals.posting_finished.emit()
            return

        if self.autopost_flag:
            total_posts_to_make = min(len(unique_posts), len(self.tokens_list) * self.post_limit_val)
        else:
            total_posts_to_make = len(unique_posts) * len(self.tokens_list)
            
        self.signals.set_progress_max.emit(total_posts_to_make)
        self.signals.update_progress.emit(0, 0, total_posts_to_make)

        for entry in unique_posts:
            entry['photos'] = sorted(entry.get('photos', []), key=natural_key)
            entry['videos'] = sorted(entry.get('videos', []), key=natural_key)
        self.signals.log_message.emit("Файлы в наборах публикаций отсортированы.\n")

        # Подготовка задач
        if self.autopost_flag:
            tasks = []
            post_index = 0
            for token_index, token in enumerate(self.tokens_list):
                for post_number in range(self.post_limit_val):
                    if post_index >= len(unique_posts):
                        break
                    entry = unique_posts[post_index]
                    tasks.append((token, entry, post_index + 1))
                    post_index += 1
            self.signals.log_message.emit(f"Создано {len(tasks)} задач для параллельной обработки в автопостинге.\n")
        else:
            # Ручной режим: задачи генерируются по-другому
            tasks = []
            for entry_idx, entry in enumerate(unique_posts):
                for token in self.tokens_list:
                    tasks.append((token, entry, entry_idx + 1))
            self.signals.log_message.emit(f"Создано {len(tasks)} задач для параллельной обработки в ручном режиме.\n")

        # Обработка задач в батчах
        batch_size = self.ip_change_limit if self.ip_change_limit > 0 else len(tasks)
        for batch_start in range(0, len(tasks), batch_size):
            if stop_flag.is_set():
                break
            batch_end = min(batch_start + batch_size, len(tasks))
            batch_tasks = tasks[batch_start:batch_end]
            
            self.signals.log_message.emit(f"Обработка батча задач {batch_start+1}-{batch_end} из {len(tasks)}...\n")
            
            with ThreadPoolExecutor(max_workers=self.thread_count_val) as executor:  # Для отладки: max_workers=1
                futures = [executor.submit(self.process_posting_task, task) for task in batch_tasks]
                
                for future in as_completed(futures):
                    if stop_flag.is_set():
                        for f in futures:
                            if not f.done():
                                f.cancel()
                        break
                    
                    try:
                        future.result()
                    except Exception as e:
                        self.signals.log_message.emit(f"Ошибка в потоке: {e}\n")
                    
                    success_count = self.successful_counter.get_value()
                    failed_count = self.failed_counter.get_value()
                    self.signals.update_progress.emit(success_count, failed_count, total_posts_to_make)
            
            # Проверка на смену IP после батча
            with self._ip_change_lock:
                if self.posts_since_last_ip_change >= self.ip_change_limit and self.ip_change_limit > 0:
                    self.signals.log_message.emit(f"Завершение батча. Смена IP...\n")
                    executor.shutdown(wait=False)
                    self.trigger_ip_change()
                    self.posts_since_last_ip_change = 0

        if failed_tokens_this_run:
            self.write_failed_tokens(failed_tokens_this_run)

        final_message = "Публикация завершена.\n" if not stop_flag.is_set() else "Процесс был остановлен пользователем.\n"
        self.signals.log_message.emit(final_message)
        self.signals.update_progress.emit(self.successful_counter.get_value(), self.failed_counter.get_value(), total_posts_to_make)
        self.signals.posting_finished.emit()

    def write_failed_tokens(self, failed_tokens_set_param):
        separator = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " ------------------------\n"
        try:
            with open(FAILED_TOKENS_FILE_PATH, 'a', encoding='utf-8') as file:
                file.write(separator)
                for token in failed_tokens_set_param:
                    file.write(token + '\n')
            self.signals.log_message.emit(f"Токены неуспешных публикаций записаны в {FAILED_TOKENS_FILE_PATH}\n")
        except Exception as e:
            self.signals.log_message.emit(f"Ошибка записи токенов неуспешных публикаций: {e}\n")
            
    def get_final_failed_tokens(self):
        return self.final_failed_tokens_for_ui_copy

    def get_final_successful_tokens(self):
        return self.final_successful_tokens_for_ui_copy

# --- Add Posting Set Dialog ---
class AddPostingSetDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Добавить набор публикации")
        self.setGeometry(300, 300, 700, 550)
        self.setModal(True)
        self.posting_schedule_files = []
        self.initUI()
        self.center()

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def initUI(self):
        layout = QVBoxLayout(self)
        title_label = QLabel("Новый набор публикации")
        title_label.setFont(HEADER_FONT)
        layout.addWidget(title_label)

        layout.addWidget(create_horizontal_separator())

        files_label_text = QLabel("Файлы (Фото и Видео): Перетащите файлы сюда или используйте кнопки.")
        layout.addWidget(files_label_text)
        files_layout = QHBoxLayout()
        self.files_listbox = QListWidget()
        self.files_listbox.setSelectionMode(QListWidget.ExtendedSelection)
        self.files_listbox.setAcceptDrops(True)
        self.files_listbox.dragEnterEvent = self.dragEnterEventFiles
        self.files_listbox.dragMoveEvent = self.dragMoveEventFiles
        self.files_listbox.dropEvent = self.dropEventFiles
        files_layout.addWidget(self.files_listbox, 1)
        
        files_layout.addWidget(create_vertical_separator())
        
        files_buttons_layout = QVBoxLayout()
        add_files_button = QPushButton("+"); add_files_button.setFixedWidth(40)
        add_files_button.clicked.connect(self.add_files_dialog)
        files_buttons_layout.addWidget(add_files_button)
        remove_files_button = QPushButton("-"); remove_files_button.setFixedWidth(40)
        remove_files_button.clicked.connect(self.remove_selected_files_dialog)
        files_buttons_layout.addWidget(remove_files_button)
        files_buttons_layout.addStretch()
        files_layout.addLayout(files_buttons_layout)
        layout.addLayout(files_layout)

        layout.addWidget(create_horizontal_separator())

        text_label = QLabel("Текст сообщения:")
        layout.addWidget(text_label)
        message_layout = QHBoxLayout()
        self.message_entry = QTextEdit()
        self.message_entry.setFixedHeight(100)
        message_layout.addWidget(self.message_entry, 1)
        
        message_layout.addWidget(create_vertical_separator())
        
        paste_button_child = QPushButton("Вставить")
        paste_button_child.clicked.connect(lambda: self.message_entry.paste())
        message_btn_vlayout = QVBoxLayout(); message_btn_vlayout.addWidget(paste_button_child); message_btn_vlayout.addStretch()
        message_layout.addLayout(message_btn_vlayout)
        layout.addLayout(message_layout)

        layout.addWidget(create_horizontal_separator())

        buttons_frame_layout = QHBoxLayout(); buttons_frame_layout.addStretch()
        save_and_new_button = QPushButton("Сохранить и Новый")
        save_and_new_button.clicked.connect(lambda: self.save_entry_dialog(close_after=False))
        buttons_frame_layout.addWidget(save_and_new_button)
        save_button = QPushButton("Сохранить и Закрыть")
        save_button.clicked.connect(lambda: self.save_entry_dialog(close_after=True))
        buttons_frame_layout.addWidget(save_button)
        cancel_button = QPushButton("Отмена")
        cancel_button.clicked.connect(self.reject)
        buttons_frame_layout.addWidget(cancel_button)
        buttons_frame_layout.addStretch()
        layout.addLayout(buttons_frame_layout)
        self.setLayout(layout)

    def dragEnterEventFiles(self, event):
        if event.mimeData().hasUrls(): event.acceptProposedAction()
        else: event.ignore()

    def dragMoveEventFiles(self, event):
        if event.mimeData().hasUrls(): event.acceptProposedAction()
        else: event.ignore()

    def dropEventFiles(self, event):
        files = [url.toLocalFile() for url in event.mimeData().urls()]
        valid_files_to_add = [
            f for f in files
            if os.path.exists(f) and f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.mp4', '.avi', '.mov', '.mkv'))
        ]
        newly_added_basenames = []
        for file_path in valid_files_to_add:
            if file_path not in self.posting_schedule_files:
                self.posting_schedule_files.append(file_path)
                self.files_listbox.addItem(QListWidgetItem(os.path.basename(file_path)))
                newly_added_basenames.append(os.path.basename(file_path))
        if newly_added_basenames and self.parent():
             self.parent().log_message_slot(f"Добавлено файлов через DnD: {', '.join(newly_added_basenames)}\n")

    def add_files_dialog(self):
        selected_files, _ = QFileDialog.getOpenFileNames(
            self, "Выберите фото и/или видео", "",
            "Медиа файлы (*.jpg *.jpeg *.png *.gif *.mp4 *.avi *.mov *.mkv);;Все файлы (*.*)"
        )
        newly_added_basenames = []
        for file_path in selected_files:
            if file_path not in self.posting_schedule_files:
                self.posting_schedule_files.append(file_path)
                self.files_listbox.addItem(QListWidgetItem(os.path.basename(file_path)))
                newly_added_basenames.append(os.path.basename(file_path))
        if newly_added_basenames and self.parent():
             self.parent().log_message_slot(f"Добавлено файлов через диалог: {', '.join(newly_added_basenames)}\n")

    def remove_selected_files_dialog(self):
        selected_items = self.files_listbox.selectedItems()
        if not selected_items: return
        removed_basenames = []
        for item in reversed(selected_items):
            row = self.files_listbox.row(item)
            base_name = os.path.basename(self.posting_schedule_files[row])
            removed_basenames.append(base_name)
            del self.posting_schedule_files[row]
            self.files_listbox.takeItem(row)
        if removed_basenames and self.parent():
            self.parent().log_message_slot(f"Удалены выбранные файлы: {', '.join(reversed(removed_basenames))}\n")

    def save_entry_dialog(self, close_after=False):
        text = self.message_entry.toPlainText().strip()
        if not self.posting_schedule_files:
            QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один файл.")
            return False
        if not text:
            reply = QMessageBox.question(self, "Подтверждение", "Сообщение пустое. Продолжить?",
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.No: return False
        photos_paths = [f for f in self.posting_schedule_files if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))]
        videos_paths = [f for f in self.posting_schedule_files if f.lower().endswith(('.mp4', '.avi', '.mov', '.mkv'))]
        if self.parent():
            self.parent().add_posting_set_data({'text': text, 'photos': photos_paths, 'videos': videos_paths})
            self.parent().log_message_slot("Новый набор публикации сохранен.\n")
        self.message_entry.clear()
        self.files_listbox.clear()
        self.posting_schedule_files.clear()
        if close_after: self.accept()
        return True

# --- Statistics Dialog ---
class StatisticsDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.setWindowTitle("Статистика")
        self.setGeometry(100, 100, 1200, 500)  # <--- увеличили стартовый размер окна!
        self.setMinimumWidth(1000)             # <--- минимальная ширина окна
        self.setModal(False)
        self.initUI()
        self.center()
        self.update_timer = QTimer(self)
        self.update_timer.timeout.connect(self.load_statistics)
        self.update_timer.start(1000)
        self.load_statistics()

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def initUI(self):
        layout = QVBoxLayout(self)
        self.stats_table = QTableWidget()
        self.stats_table.setColumnCount(6)
        self.stats_table.setHorizontalHeaderLabels([
            "Дата и время", "Успешные публикации", "Неуспешные публикации",
            "Количество токенов", "Фото в 'посты'", "Статус"
        ])
        header = self.stats_table.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.Interactive)
        header.setStretchLastSection(True)
        min_widths = [210, 130, 140, 150, 150, 130]
        for i, w in enumerate(min_widths):
            self.stats_table.setColumnWidth(i, w)
            self.stats_table.horizontalHeader().setMinimumSectionSize(w)
        layout.addWidget(self.stats_table)
        self.setLayout(layout)

    def load_statistics(self):
        self.stats_table.setRowCount(0)
        if self.parent.posting_thread and self.parent.posting_thread.isRunning():
            current_time = datetime.now().strftime("%d.%m.%Y %H:%M:%S") + " (в процессе)"
            successful = int(self.parent.successful_posts_label.text().split(": ")[1])
            failed = int(self.parent.failed_posts_label.text().split(": ")[1])
            tokens_count = len(self.parent.tokens)
            photos_count = self.parent.count_photos_in_posts_folder()
            status = "В процессе"
            self.stats_table.insertRow(0)
            values = [current_time, str(successful), str(failed), str(tokens_count), str(photos_count), status]
            for col, val in enumerate(values):
                item = QTableWidgetItem(val)
                item.setToolTip(val)
                self.stats_table.setItem(0, col, item)
        if os.path.exists("statistics.csv"):
            with open("statistics.csv", "r", encoding="utf-8") as f:
                lines = f.readlines()[::-1][:10]
                for i, line in enumerate(lines):
                    parts = line.strip().split(',')
                    if len(parts) == 6:
                        row = i + (1 if self.parent.posting_thread and self.parent.posting_thread.isRunning() else 0)
                        self.stats_table.insertRow(row)
                        for col, val in enumerate(parts):
                            item = QTableWidgetItem(val)
                            item.setToolTip(val)
                            self.stats_table.setItem(row, col, item)

    def closeEvent(self, event):
        self.update_timer.stop()
        super().closeEvent(event)

# --- IP Change Signals ---
class IPSignals(QObject):
    device_scan_completed = pyqtSignal(list)  # Сигнал для обновления списка устройств

ip_signals = IPSignals()

# --- Main Application Window ---
class VKAutoPosterApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Автопостинг ВКонтакте")
        self.setGeometry(100, 100, 1250, 850)
        self.tokens = []
        self.posting_schedule = []
        self.auto_scroll_log = True
        self.worker_signals = WorkerSignals()
        self.posting_thread = None
        self.final_failed_tokens_for_ui_copy = set()
        self.final_successful_tokens_for_ui_copy = set()
        self.token_error_specific_count = 0
        self.banned_token_specific_count = 0
        self.other_error_specific_count = 0
        self.auth_error_tokens = set()
        self.banned_error_tokens = set()
        self.other_error_tokens = set()

        self.elapsed_time_label = QLabel("Время: 00:00:00")
        self.runtime_qtimer = QTimer(self)
        self.runtime_qtimer.timeout.connect(self.update_runtime_display)
        self.posting_start_time = None
        self.elapsed_seconds = 0

        # Для смены IP
        self.devices_by_model = {}  # Словарь для хранения устройств: модель -> serial
        self.ip_change_limit = 0

        self.initUI()
        self.center()
        self.load_settings()
        self.update_add_schedule_button_state()
        self.update_buttons_on_token_load()
        self.update_retry_button_state()

        self.info_update_timer = QTimer(self)
        self.info_update_timer.timeout.connect(self.update_info_display_slot)
        self.info_update_timer.start(5000)
        self.update_info_display_slot()

        # Таймер для обновления счётчика фотографий
        self.photos_count_timer = QTimer(self)
        self.photos_count_timer.timeout.connect(self.update_photos_count_label)
        self.photos_count_timer.start(5000)  # Обновляем каждые 5 секунд
        self.update_photos_count_label()  # Первоначальное обновление
        self.load_last_post_date()

        self.worker_signals.log_message.connect(self.log_message_slot)
        self.worker_signals.update_progress.connect(self.update_progress_labels_slot)
        self.worker_signals.successful_post.connect(self.successful_post_slot)
        self.worker_signals.failed_post.connect(self.failed_post_slot)
        self.worker_signals.enable_controls.connect(self.toggle_posting_buttons_slot)
        self.worker_signals.posting_finished.connect(self.on_posting_finished)
        self.worker_signals.update_schedule_table.connect(self.receive_schedule_update_from_worker)
        self.worker_signals.set_progress_max.connect(self.progress_bar.setMaximum)
        self.worker_signals.increment_progress_bar.connect(self.increment_progress_bar_slot)

        # Подключение сигналов для IP
        ip_signals.device_scan_completed.connect(self.update_device_list)

        # Автоматическое сканирование устройств при запуске
        self.scan_devices_async()

    def center(self):
        """Центрирование окна на экране"""
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def count_photos_in_posts_folder(self):
        """Подсчитывает количество фотографий в папке постов"""
        if not os.path.exists(POSTS_DIR):
            return 0
        
        try:
            files = os.listdir(POSTS_DIR)
            photo_files = [f for f in files if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))]
            return len(photo_files)
        except Exception as e:
            self.log_message_slot(f"Ошибка подсчёта фотографий в папке постов: {e}\n")
            return 0

    def update_photos_count_label(self):
        """Обновляет метку с количеством оставшихся фотографий"""
        photos_count = self.count_photos_in_posts_folder()
        self.photos_count_label.setText(f"Фото в папке 'посты': {photos_count}")

    def load_last_post_date(self):
        """Загрузка даты последней публикации"""
        if os.path.exists(LAST_POST_DATE_FILE_PATH):
            try:
                with open(LAST_POST_DATE_FILE_PATH, 'r', encoding='utf-8') as f:
                    last_date = f.read().strip()
                    if last_date:
                        self.last_post_date_label.setText(f"Последняя публикация: {last_date}")
                    else:
                        self.last_post_date_label.setText("Последняя публикация: Нет данных")
            except Exception as e:
                self.log_message_slot(f"Ошибка загрузки даты последней публикации: {e}\n")
                self.last_post_date_label.setText("Последняя публикация: Ошибка загрузки")
        else:
            self.last_post_date_label.setText("Последняя публикация: Нет данных")

    def save_last_post_date(self):
        """Сохранение даты последней публикации"""
        current_datetime = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
        try:
            with open(LAST_POST_DATE_FILE_PATH, 'w', encoding='utf-8') as f:
                f.write(current_datetime)
            self.last_post_date_label.setText(f"Последняя публикация: {current_datetime}")
        except Exception as e:
            self.log_message_slot(f"Ошибка сохранения даты последней публикации: {e}\n")

    def initUI(self):
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # Секция прогресс-бара
        progress_outer_layout = QVBoxLayout()
        self.progress_bar = QProgressBar(); self.progress_bar.setValue(0); self.progress_bar.setTextVisible(False)
        progress_outer_layout.addWidget(self.progress_bar)
        
        labels_info_layout = QHBoxLayout() 
        self.progress_label = QLabel("Осталось постов: 0") 
        labels_info_layout.addWidget(self.progress_label)
        labels_info_layout.addStretch()
        self.elapsed_time_label.setAlignment(Qt.AlignRight) 
        labels_info_layout.addWidget(self.elapsed_time_label)
        progress_outer_layout.addLayout(labels_info_layout)
        
        main_layout.addLayout(progress_outer_layout)

        main_layout.addWidget(create_horizontal_separator())

        # **ИЗМЕНЁННАЯ СЕКЦИЯ: Информационные метки в две строки**
        info_frame_main_layout = QVBoxLayout()
        
        # Первая строка: основная статистика
        info_frame_top_layout = QHBoxLayout()
        self.successful_posts_label = QLabel("Успешные публикации: 0"); self.successful_posts_label.setObjectName("successfulPostsLabel")
        self.successful_posts_label.setToolTip("Нажмите, чтобы скопировать успешные токены")
        self.successful_posts_label.mousePressEvent = lambda e: self.copy_tokens_to_clipboard_handler(True)
        info_frame_top_layout.addWidget(self.successful_posts_label)
        
        info_frame_top_layout.addWidget(create_vertical_separator())
        
        self.failed_posts_label = QLabel("Неуспешные публикации: 0"); self.failed_posts_label.setObjectName("failedPostsLabel")
        self.failed_posts_label.setToolTip("Нажмите, чтобы скопировать все неуспешные токены")
        self.failed_posts_label.mousePressEvent = lambda e: self.copy_tokens_to_clipboard_handler(False)
        info_frame_top_layout.addWidget(self.failed_posts_label)
        
        info_frame_top_layout.addWidget(create_vertical_separator())
        
        self.last_post_date_label = QLabel("Последняя публикация: Нет данных")
        self.last_post_date_label.setObjectName("lastPostDateLabel")
        self.last_post_date_label.setToolTip("Дата и время последней успешной публикации")
        info_frame_top_layout.addWidget(self.last_post_date_label)
        
        info_frame_top_layout.addWidget(create_vertical_separator())
        
        # Счётчик фотографий в папке постов
        self.photos_count_label = QLabel("Фото в папке 'посты': 0")
        self.photos_count_label.setObjectName("photosCountLabel")
        self.photos_count_label.setToolTip("Количество фотографий в папке 'посты', доступных для автопостинга")
        info_frame_top_layout.addWidget(self.photos_count_label)
        
        info_frame_top_layout.addStretch()
        info_frame_main_layout.addLayout(info_frame_top_layout)
        
        # Вторая строка: настройки и сетевая информация
        info_frame_bottom_layout = QHBoxLayout()
        
        info_frame_bottom_layout.addWidget(create_vertical_separator())
        
        self.wifi_status_label = QLabel("Wi-Fi: Сканирование...")
        info_frame_bottom_layout.addWidget(self.wifi_status_label)
        
        info_frame_bottom_layout.addWidget(create_vertical_separator())
        
        self.ip_label = QLabel("IP: Сканирование...")
        info_frame_bottom_layout.addWidget(self.ip_label)
        
        info_frame_bottom_layout.addWidget(create_vertical_separator())
        
        self.location_label = QLabel("Локация: Сканирование...")
        info_frame_bottom_layout.addWidget(self.location_label)
        
        info_frame_bottom_layout.addStretch()
        info_frame_main_layout.addLayout(info_frame_bottom_layout)
        
        main_layout.addLayout(info_frame_main_layout)

        main_layout.addWidget(create_horizontal_separator())

        # Секция настроек автопостинга
        autopost_settings_layout = QHBoxLayout()
        self.autopost_check = QCheckBox("Автопубликация")
        self.autopost_check.stateChanged.connect(self.save_autopost_state_setting)
        self.autopost_check.stateChanged.connect(self.update_add_schedule_button_state)
        autopost_settings_layout.addWidget(self.autopost_check)
        
        autopost_settings_layout.addWidget(create_vertical_separator())
        
        autopost_settings_layout.addWidget(QLabel("Лимит постов на аккаунт:"))
        self.post_limit_entry = QLineEdit("1"); self.post_limit_entry.setFixedWidth(50)
        self.post_limit_entry.textChanged.connect(self.save_post_limit_setting)
        autopost_settings_layout.addWidget(self.post_limit_entry)
        
        autopost_settings_layout.addWidget(create_vertical_separator())
        
        autopost_settings_layout.addWidget(QLabel("Фотографий на пост:"))
        self.photo_count_entry = QLineEdit("1"); self.photo_count_entry.setFixedWidth(50)
        self.photo_count_entry.textChanged.connect(self.save_photo_count_setting)
        autopost_settings_layout.addWidget(self.photo_count_entry)
        
        ### ИЗМЕНЕНИЕ: Новое поле для лимита смены IP
        autopost_settings_layout.addWidget(create_vertical_separator())
        autopost_settings_layout.addWidget(QLabel("Сменить IP после (постов):"))
        self.ip_change_limit_entry = QLineEdit("0"); self.ip_change_limit_entry.setFixedWidth(50)
        self.ip_change_limit_entry.setToolTip("0 - отключено. Смена IP после указанного числа успешных постов.")
        self.ip_change_limit_entry.textChanged.connect(self.save_ip_change_limit_setting)
        autopost_settings_layout.addWidget(self.ip_change_limit_entry)
        
        autopost_settings_layout.addStretch()
        main_layout.addLayout(autopost_settings_layout)

        main_layout.addWidget(create_horizontal_separator())

        # Секция управления аккаунтами
        account_mgmt_layout = QHBoxLayout()
        self.account_button = QPushButton("Указать аккаунты")
        self.account_button.clicked.connect(self.load_tokens_ui_update)
        account_mgmt_layout.addWidget(self.account_button)
        
        account_mgmt_layout.addWidget(create_vertical_separator())
        
        self.tokens_label = QLabel("Токенов: 0")
        account_mgmt_layout.addWidget(self.tokens_label)
        account_mgmt_layout.addStretch()
        main_layout.addLayout(account_mgmt_layout)

        main_layout.addWidget(create_horizontal_separator())

        # Секция кнопок управления
        controls_layout = QHBoxLayout(); controls_layout.addStretch()
        self.post_button = QPushButton("Выложить пост")
        self.post_button.clicked.connect(self.submit_post_handler)
        controls_layout.addWidget(self.post_button)
        
        controls_layout.addWidget(create_vertical_separator())
        
        self.stop_button = QPushButton("Остановить"); self.stop_button.setEnabled(False)
        self.stop_button.clicked.connect(self.stop_posting_handler)
        controls_layout.addWidget(self.stop_button)
        
        controls_layout.addWidget(create_vertical_separator())
        
        self.retry_other_errors_button = QPushButton("Повторить для 'Прочих ошибок'")
        self.retry_other_errors_button.clicked.connect(self.retry_other_errors_handler)
        self.retry_other_errors_button.setEnabled(False)
        controls_layout.addWidget(self.retry_other_errors_button)

        controls_layout.addWidget(create_vertical_separator())

        self.add_schedule_button = QPushButton("Добавить набор публикаций")
        self.add_schedule_button.clicked.connect(self.open_add_posting_set_dialog)
        controls_layout.addWidget(self.add_schedule_button)
        
        controls_layout.addWidget(create_vertical_separator())
        
        self.remove_schedule_button = QPushButton("Удалить выбранные")
        self.remove_schedule_button.clicked.connect(self.remove_selected_posting_sets)
        controls_layout.addWidget(self.remove_schedule_button)

        controls_layout.addWidget(create_vertical_separator())

        self.stats_button = QPushButton("Статистика")
        self.stats_button.clicked.connect(self.show_statistics_dialog)
        controls_layout.addWidget(self.stats_button)

        controls_layout.addStretch()
        main_layout.addLayout(controls_layout)

        # Новая секция для смены IP
        ip_group = QGroupBox("Смена IP")
        ip_layout = QVBoxLayout(ip_group)

        # Комбо-бокс для выбора модели устройства
        self.device_combobox = QComboBox()
        self.device_combobox.addItem("Нет подключённых устройств")
        self.device_combobox.currentTextChanged.connect(self.on_model_selected)
        ip_layout.addWidget(QLabel("Выберите устройство:"))
        ip_layout.addWidget(self.device_combobox)

        # Кнопка для сканирования устройств
        self.scan_devices_button = QPushButton("Сканировать устройства")
        self.scan_devices_button.clicked.connect(self.scan_devices_async)
        ip_layout.addWidget(self.scan_devices_button)

        # Новые галочки для смены IP в начале и в конце (по горизонтали)
        ip_change_checks_layout = QHBoxLayout()
        self.change_ip_start_check = QCheckBox("Сменить IP в начале процесса")
        self.change_ip_start_check.setToolTip("Автоматически сменить IP перед началом публикации")
        self.change_ip_start_check.stateChanged.connect(self.save_change_ip_start_state)
        ip_change_checks_layout.addWidget(self.change_ip_start_check)

        self.change_ip_end_check = QCheckBox("Сменить IP в конце процесса")
        self.change_ip_end_check.setToolTip("Автоматически сменить IP после завершения публикации")
        self.change_ip_end_check.stateChanged.connect(self.save_change_ip_end_state)
        ip_change_checks_layout.addWidget(self.change_ip_end_check)
        ip_change_checks_layout.addStretch()
        ip_layout.addLayout(ip_change_checks_layout)

        main_layout.addWidget(ip_group)

        main_layout.addWidget(create_horizontal_separator())

        # Основные панели (таблица и лог)
        splitter_schedule_log = QSplitter(Qt.Horizontal)
        schedule_table_container = QWidget(); schedule_table_layout = QVBoxLayout(schedule_table_container)
        schedule_table_layout.addWidget(QLabel("План публикаций:"))
        self.posting_schedule_table = QTableWidget(); self.posting_schedule_table.setColumnCount(4)
        self.posting_schedule_table.setHorizontalHeaderLabels(["№", "Фото", "Видео", "Текст"])
        self.posting_schedule_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
        self.posting_schedule_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.posting_schedule_table.setSelectionBehavior(QTableWidget.SelectRows)
        self.posting_schedule_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.posting_schedule_table.customContextMenuRequested.connect(self.show_table_context_menu)
        schedule_table_layout.addWidget(self.posting_schedule_table)
        splitter_schedule_log.addWidget(schedule_table_container)
        log_output_container = QWidget(); log_output_layout = QVBoxLayout(log_output_container)
        log_output_layout.addWidget(QLabel("Лог процесса:"))
        self.output_text = QTextEdit(); self.output_text.setReadOnly(True)
        self.output_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.output_text.customContextMenuRequested.connect(lambda pos: self.show_text_edit_context_menu(pos, self.output_text))
        log_output_layout.addWidget(self.output_text)
        splitter_schedule_log.addWidget(log_output_container)
        splitter_schedule_log.setSizes([400, 600])
        main_layout.addWidget(splitter_schedule_log, 1)

        # Панели результатов
        splitter_results = QSplitter(Qt.Horizontal)
        successful_container = QWidget(); successful_layout = QVBoxLayout(successful_container)
        successful_layout.addWidget(QLabel("Успешные публикации:"))
        
        self.successful_text = ClickableQTextEdit(self)
        self.successful_text.setReadOnly(True)
        self.successful_text.setObjectName("successfulText")
        self.successful_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.successful_text.customContextMenuRequested.connect(lambda pos: self.show_text_edit_context_menu(pos, self.successful_text))
        successful_layout.addWidget(self.successful_text)
        splitter_results.addWidget(successful_container)
        
        failed_container = QWidget(); failed_layout = QVBoxLayout(failed_container)
        failed_layout.addWidget(QLabel("Неуспешные публикации:"))

        # Специфичные счетчики ошибок с разделителями
        specific_failed_counts_layout = QHBoxLayout()
        self.token_errors_specific_label = QLabel("Ошибка авторизации (токен): 0")
        self.token_errors_specific_label.setObjectName("tokenErrorsSpecificLabel") 
        self.token_errors_specific_label.setToolTip("Нажмите, чтобы скопировать токены с ошибкой авторизации")
        self.token_errors_specific_label.mousePressEvent = lambda event, cat="auth": self.copy_categorized_tokens_handler(cat)
        self.token_errors_specific_label.setCursor(QCursor(Qt.PointingHandCursor))
        specific_failed_counts_layout.addWidget(self.token_errors_specific_label)

        specific_failed_counts_layout.addWidget(create_vertical_separator())

        self.banned_tokens_specific_label = QLabel("Аккаунт заблокирован: 0")
        self.banned_tokens_specific_label.setObjectName("bannedTokensSpecificLabel") 
        self.banned_tokens_specific_label.setToolTip("Нажмите, чтобы скопировать заблокированные токены")
        self.banned_tokens_specific_label.mousePressEvent = lambda event, cat="banned": self.copy_categorized_tokens_handler(cat)
        self.banned_tokens_specific_label.setCursor(QCursor(Qt.PointingHandCursor))
        specific_failed_counts_layout.addWidget(self.banned_tokens_specific_label)
        
        specific_failed_counts_layout.addWidget(create_vertical_separator())
        
        self.other_errors_specific_label = QLabel("Прочие ошибки: 0") 
        self.other_errors_specific_label.setObjectName("otherErrorsSpecificLabel")
        self.other_errors_specific_label.setToolTip("Нажмите, чтобы скопировать токены с прочими ошибками")
        self.other_errors_specific_label.mousePressEvent = lambda event, cat="other": self.copy_categorized_tokens_handler(cat)
        self.other_errors_specific_label.setCursor(QCursor(Qt.PointingHandCursor))
        specific_failed_counts_layout.addWidget(self.other_errors_specific_label)
        
        specific_failed_counts_layout.addStretch() 
        failed_layout.addLayout(specific_failed_counts_layout)

        failed_layout.addWidget(create_horizontal_separator())

        self.failed_text = QTextEdit(); self.failed_text.setReadOnly(True); self.failed_text.setObjectName("failedText")
        self.failed_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.failed_text.customContextMenuRequested.connect(lambda pos: self.show_text_edit_context_menu(pos, self.failed_text))
        failed_layout.addWidget(self.failed_text)
        splitter_results.addWidget(failed_container)
        splitter_results.setSizes([500,500])
        main_layout.addWidget(splitter_results, 1)
        self.setFont(FONT_STYLE)

    def update_runtime_display(self):
        self.elapsed_seconds += 1
        hours = self.elapsed_seconds // 3600
        minutes = (self.elapsed_seconds % 3600) // 60
        seconds = self.elapsed_seconds % 60
        self.elapsed_time_label.setText(f"Время: {hours:02}:{minutes:02}:{seconds:02}")

    def copy_tokens_to_clipboard_handler(self, successful=True):
        tokens_set = self.final_successful_tokens_for_ui_copy if successful else self.final_failed_tokens_for_ui_copy
        type_name = "успешных" if successful else "всех неуспешных"
        if not tokens_set:
            self.log_message_slot(f"Нет {type_name} токенов для копирования в буфер.\n")
            return
        try:
            tokens_str = "\n".join(sorted(list(tokens_set)))
            QApplication.clipboard().setText(tokens_str)
            self.log_message_slot(f"{type_name.capitalize()} токены ({len(tokens_set)} шт.) скопированы в буфер обмена.\n")
        except Exception as e:
            self.log_message_slot(f"Ошибка копирования токенов ({type_name}) в буфер: {e}\n")
            
    def copy_categorized_tokens_handler(self, category_key):
        tokens_to_copy = set()
        category_name_log = ""
        if category_key == "auth":
            tokens_to_copy = self.auth_error_tokens
            category_name_log = "с ошибкой авторизации (токен)"
        elif category_key == "banned":
            tokens_to_copy = self.banned_error_tokens
            category_name_log = "заблокированных аккаунтов"
        elif category_key == "other":
            tokens_to_copy = self.other_error_tokens
            category_name_log = "с прочими ошибками"
        else:
            self.log_message_slot(f"Неизвестная категория токенов для копирования: {category_key}\n")
            return

        if not tokens_to_copy:
            self.log_message_slot(f"Нет токенов ({category_name_log}) для копирования.\n")
            return
        try:
            tokens_str = "\n".join(sorted(list(tokens_to_copy)))
            QApplication.clipboard().setText(tokens_str)
            self.log_message_slot(f"Токены ({len(tokens_to_copy)} шт.) {category_name_log} скопированы в буфер обмена.\n")
        except Exception as e:
            self.log_message_slot(f"Ошибка копирования токенов ({category_name_log}) в буфер: {e}\n")

    def receive_schedule_update_from_worker(self, new_schedule):
        self.posting_schedule = new_schedule
        self.update_posting_schedule_table_display()

    def increment_progress_bar_slot(self):
        self.progress_bar.setValue(self.progress_bar.value() + 1)

    def show_text_edit_context_menu(self, position, text_edit_widget):
        menu = QMenu(); copy_action = menu.addAction("Копировать"); select_all_action = menu.addAction("Выбрать все")
        menu.addSeparator()
        if text_edit_widget == self.output_text:
            auto_scroll_action = QAction("Включить автопрокрутку", self, checkable=True)
            auto_scroll_action.setChecked(self.auto_scroll_log)
            auto_scroll_action.triggered.connect(self.toggle_auto_scroll_log)
            menu.addAction(auto_scroll_action)
        action = menu.exec_(text_edit_widget.mapToGlobal(position))
        if action == copy_action: text_edit_widget.copy()
        elif action == select_all_action: text_edit_widget.selectAll()

    def toggle_auto_scroll_log(self):
        self.auto_scroll_log = not self.auto_scroll_log
        self.log_message_slot(f"Автопрокрутка лога {'включена' if self.auto_scroll_log else 'отключена'}.\n")

    def show_table_context_menu(self, position):
        menu = QMenu(); select_all_action = menu.addAction("Выбрать все"); remove_action = menu.addAction("Удалить выбранные")
        if not self.posting_schedule_table.selectedItems(): remove_action.setEnabled(False)
        action = menu.exec_(self.posting_schedule_table.viewport().mapToGlobal(position))
        if action == select_all_action: self.posting_schedule_table.selectAll()
        elif action == remove_action: self.remove_selected_posting_sets()

    def update_info_display_slot(self):
        threading.Thread(target=self._fetch_network_info, daemon=True).start()

    def _fetch_network_info(self):
        ssid = get_current_wifi_ssid(); ip = get_external_ip()
        location = "Недоступно"; 
        if ip != "Недоступно": location = get_location_by_ip(ip)
        self.wifi_status_label.setText(f"Wi-Fi: {ssid}"); self.ip_label.setText(f"IP: {ip}"); self.location_label.setText(f"Локация: {location}")

    def load_settings(self):
        autopost_enabled = False
        if os.path.exists(AUTOPOST_STATE_FILE_PATH):
            try:
                with open(AUTOPOST_STATE_FILE_PATH, 'r', encoding='utf-8') as f: autopost_enabled = f.read().strip().lower() == 'true'
            except Exception as e: self.log_message_slot(f"Ошибка загрузки состояния автопостинга: {e}\n")
        self.autopost_check.setChecked(autopost_enabled)
        
        if os.path.exists(POST_LIMIT_FILE_PATH):
            try:
                with open(POST_LIMIT_FILE_PATH, 'r', encoding='utf-8') as f: self.post_limit_entry.setText(f.read().strip() or "1")
            except Exception as e: 
                self.log_message_slot(f"Ошибка загрузки лимита постов: {e}\n")
        else: 
            self.post_limit_entry.setText("1")
        
        if os.path.exists(PHOTO_COUNT_FILE_PATH):
            try:
                with open(PHOTO_COUNT_FILE_PATH, 'r', encoding='utf-8') as f: 
                    self.photo_count_entry.setText(f.read().strip() or "1")
            except Exception as e: 
                self.log_message_slot(f"Ошибка загрузки кол-ва фото: {e}\n")
        else: 
            self.photo_count_entry.setText("1")
        
        if os.path.exists(IP_CHANGE_LIMIT_FILE_PATH):
            try:
                with open(IP_CHANGE_LIMIT_FILE_PATH, 'r', encoding='utf-8') as f: 
                    limit = int(f.read().strip() or "0")
                    self.ip_change_limit_entry.setText(str(limit))
                    self.ip_change_limit = limit
            except Exception as e: 
                self.log_message_slot(f"Ошибка загрузки лимита смены IP: {e}\n")
                self.ip_change_limit_entry.setText("0")
                self.ip_change_limit = 0
        else: 
            self.ip_change_limit_entry.setText("0")
            self.ip_change_limit = 0

        change_ip_start_enabled = False
        if os.path.exists(CHANGE_IP_START_STATE_FILE_PATH):
            try:
                with open(CHANGE_IP_START_STATE_FILE_PATH, 'r', encoding='utf-8') as f: 
                    change_ip_start_enabled = f.read().strip().lower() == 'true'
            except Exception as e: 
                self.log_message_slot(f"Ошибка загрузки состояния смены IP в начале: {e}\n")
        self.change_ip_start_check.setChecked(change_ip_start_enabled)
        
        change_ip_end_enabled = False
        if os.path.exists(CHANGE_IP_END_STATE_FILE_PATH):
            try:
                with open(CHANGE_IP_END_STATE_FILE_PATH, 'r', encoding='utf-8') as f: 
                    change_ip_end_enabled = f.read().strip().lower() == 'true'
            except Exception as e: 
                self.log_message_slot(f"Ошибка загрузки состояния смены IP в конце: {e}\n")
        self.change_ip_end_check.setChecked(change_ip_end_enabled)
        
        self.load_tokens_ui_update()

    def save_setting_to_file(self, file_path, value_to_save, error_msg_prefix):
        try:
            with open(file_path, 'w', encoding='utf-8') as f: 
                f.write(str(value_to_save))
        except Exception as e: 
            self.log_message_slot(f"{error_msg_prefix}: {e}\n")

    def save_autopost_state_setting(self): 
        self.save_setting_to_file(AUTOPOST_STATE_FILE_PATH, self.autopost_check.isChecked(), "Ошибка сохранения состояния автопостинга")
    
    def save_post_limit_setting(self): 
        self.save_setting_to_file(POST_LIMIT_FILE_PATH, self.post_limit_entry.text(), "Ошибка сохранения лимита постов")
    
    def save_photo_count_setting(self): 
        self.save_setting_to_file(PHOTO_COUNT_FILE_PATH, self.photo_count_entry.text(), "Ошибка сохранения кол-ва фото")

    def save_ip_change_limit_setting(self):
        try:
            self.ip_change_limit = int(self.ip_change_limit_entry.text() or "0")
            self.save_setting_to_file(IP_CHANGE_LIMIT_FILE_PATH, self.ip_change_limit, "Ошибка сохранения лимита смены IP")
        except ValueError:
            self.log_message_slot("Ошибка: Введите число для лимита смены IP.\n")
            self.ip_change_limit_entry.setText("0")
            self.ip_change_limit = 0

    def save_change_ip_start_state(self):
        self.save_setting_to_file(CHANGE_IP_START_STATE_FILE_PATH, self.change_ip_start_check.isChecked(), "Ошибка сохранения состояния смены IP в начале")

    def save_change_ip_end_state(self):
        self.save_setting_to_file(CHANGE_IP_END_STATE_FILE_PATH, self.change_ip_end_check.isChecked(), "Ошибка сохранения состояния смены IP в конце")

    def load_tokens_ui_update(self):
        if os.path.exists(TOKENS_FILE_PATH):
            try:
                with open(TOKENS_FILE_PATH, 'r', encoding='utf-8') as file: 
                    self.tokens = [token.strip() for token in file.readlines() if token.strip()]
                self.tokens_label.setText(f"Токенов загружено: {len(self.tokens)}")
                if self.tokens: 
                    self.log_message_slot(f"Загружено {len(self.tokens)} токенов.\n")
                else: 
                    self.log_message_slot("Файл токенов пуст.\n")
            except Exception as e:
                self.log_message_slot(f"Ошибка чтения файла токенов: {e}\n")
                QMessageBox.critical(self, "Ошибка токенов", f"Не удалось прочитать файл токенов: {e}")
                self.tokens = []
                self.tokens_label.setText("Токенов: 0 (ошибка)")
        else:
            self.log_message_slot(f"Файл с токенами не найден: {TOKENS_FILE_PATH}\n")
            self.tokens_label.setText("Токенов: 0 (файл не найден)")
        self.update_buttons_on_token_load()

    def update_buttons_on_token_load(self):
        has_tokens = bool(self.tokens)
        self.post_button.setEnabled(has_tokens)
        self.remove_schedule_button.setEnabled(has_tokens and bool(self.posting_schedule))
        self.autopost_check.setEnabled(has_tokens)
        if not has_tokens: 
            self.autopost_check.setChecked(False)
        self.update_add_schedule_button_state()
        self.update_retry_button_state()

    def update_add_schedule_button_state(self):
        is_autopost = self.autopost_check.isChecked()
        self.add_schedule_button.setEnabled(not is_autopost and bool(self.tokens))
        self.post_limit_entry.setEnabled(is_autopost and bool(self.tokens))
        self.photo_count_entry.setEnabled(is_autopost and bool(self.tokens))
        has_device = self.device_combobox.currentText() != "Нет подключённых устройств"
        self.ip_change_limit_entry.setEnabled(has_device and bool(self.tokens))
        if is_autopost and self.posting_schedule:
            self.log_message_slot("Режим автопубликации включен. Ручной план публикаций очищен.\n")
            self.posting_schedule.clear()
            self.update_posting_schedule_table_display()
        self.update_retry_button_state()

    def update_retry_button_state(self):
        can_retry = bool(self.other_error_tokens) and not (self.posting_thread and self.posting_thread.isRunning())
        self.retry_other_errors_button.setEnabled(can_retry)

    def open_add_posting_set_dialog(self):
        dialog = AddPostingSetDialog(self)
        dialog.exec_()

    def add_posting_set_data(self, data):
        self.posting_schedule.append(data)
        self.update_posting_schedule_table_display()
        self.update_buttons_on_token_load()

    def remove_selected_posting_sets(self):
        selected_rows = sorted(list(set(item.row() for item in self.posting_schedule_table.selectedItems())), reverse=True)
        if not selected_rows:
            QMessageBox.warning(self, "Предупреждение", "Выберите запись для удаления.")
            return
        removed_count = 0
        for row_index in selected_rows:
            if 0 <= row_index < len(self.posting_schedule):
                del self.posting_schedule[row_index]
                self.log_message_slot(f"Удалена публикация (бывший №{row_index + 1} в списке до удаления).\n")
                removed_count += 1
        if removed_count > 0: 
            self.update_posting_schedule_table_display()
        self.update_buttons_on_token_load()

    def update_posting_schedule_table_display(self):
        self.posting_schedule_table.setRowCount(0)
        for idx, entry in enumerate(self.posting_schedule):
            self.posting_schedule_table.insertRow(idx)
            photos_count = len(entry.get('photos', []))
            videos_count = len(entry.get('videos', []))
            text_preview = (entry['text'][:30].replace(os.linesep, ' ') + '...') if len(entry['text']) > 30 else entry['text'].replace(os.linesep, ' ')
            
            item0 = QTableWidgetItem(str(idx + 1))
            item0.setToolTip(str(idx + 1))  # Полное содержимое в тултипе
            self.posting_schedule_table.setItem(idx, 0, item0)
            
            photos_str = f"{photos_count} фото"
            photos_tooltip = ", ".join([os.path.basename(p) for p in entry.get('photos', [])]) if entry.get('photos') else "Нет фото"
            item1 = QTableWidgetItem(photos_str)
            item1.setToolTip(photos_tooltip)
            self.posting_schedule_table.setItem(idx, 1, item1)
            
            videos_str = f"{videos_count} видео"
            videos_tooltip = ", ".join([os.path.basename(v) for v in entry.get('videos', [])]) if entry.get('videos') else "Нет видео"
            item2 = QTableWidgetItem(videos_str)
            item2.setToolTip(videos_tooltip)
            self.posting_schedule_table.setItem(idx, 2, item2)
            
            item3 = QTableWidgetItem(text_preview)
            item3.setToolTip(entry['text'])  # Полный текст в тултипе
            self.posting_schedule_table.setItem(idx, 3, item3)
        
        self.posting_schedule_table.resizeColumnsToContents()
        self.posting_schedule_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)

    def show_statistics_dialog(self):
        dialog = StatisticsDialog(self)
        dialog.show()

    def submit_post_handler(self):
        if self.posting_thread and self.posting_thread.isRunning():
            self.log_message_slot("Процесс публикации уже запущен.\n")
            return
        self.clear_previous_results()
        self.final_failed_tokens_for_ui_copy.clear()
        self.final_successful_tokens_for_ui_copy.clear()
        
        self.posting_start_time = time.monotonic() 
        self.elapsed_seconds = 0
        self.elapsed_time_label.setText("Время: 00:00:00")
        self.runtime_qtimer.start(1000)
        
        post_limit = 0
        photo_count = 0
        is_autopost = self.autopost_check.isChecked()
        
        if is_autopost:
            try:
                post_limit = int(self.post_limit_entry.text())
                photo_count = int(self.photo_count_entry.text())
                if post_limit <= 0 or photo_count <= 0:
                    QMessageBox.critical(self, "Ошибка", "Лимит постов и количество фотографий должны быть больше 0.")
                    self.runtime_qtimer.stop() 
                    return
            except ValueError:
                QMessageBox.critical(self, "Ошибка", "Введите корректные числовые значения для лимита постов и количества фотографий.")
                self.runtime_qtimer.stop() 
                return
        
        # Проверка галочки для смены IP в начале
        if self.change_ip_start_check.isChecked():
            self.log_message_slot("Смена IP перед началом процесса...\n")
            self.run_flight_mode_scenario()
        
        self.posting_thread = PostingWorker(
            list(self.tokens),
            list(self.posting_schedule), 
            is_autopost, 
            post_limit, 
            photo_count, 
            self.worker_signals,
            self.ip_change_limit,  # Лимит IP
            self  # Ссылка на главное окно
        )
        self.posting_thread.start()
        self.progress_bar.setValue(0)

    def retry_other_errors_handler(self):
        if self.posting_thread and self.posting_thread.isRunning():
            self.log_message_slot("Процесс публикации уже запущен.\n")
            return

        if not self.other_error_tokens:
            self.log_message_slot("Нет токенов в категории 'Прочие ошибки' для повтора.\n")
            return

        tokens_for_retry = list(self.other_error_tokens)

        self.log_message_slot(f"Запуск повторной публикации для {len(tokens_for_retry)} токенов из 'Прочих ошибок'...\n")
        
        self.clear_previous_results() 
        self.final_failed_tokens_for_ui_copy.clear()
        self.final_successful_tokens_for_ui_copy.clear()
            
        is_autopost = self.autopost_check.isChecked()
        posting_schedule_for_retry = []
        post_limit = 0
        photo_count = 0
        
        if is_autopost:
            try:
                post_limit = int(self.post_limit_entry.text())
                photo_count = int(self.photo_count_entry.text())
                if post_limit <= 0 or photo_count <= 0:
                    QMessageBox.critical(self, "Ошибка", "Лимит постов и количество фотографий для автопостинга должны быть больше 0.")
                    self.update_retry_button_state()
                    return
            except ValueError:
                QMessageBox.critical(self, "Ошибка", "Введите корректные числовые значения для лимита постов и количества фотографий для автопостинга.")
                self.update_retry_button_state()
                return
        else: 
            if not self.posting_schedule:
                self.log_message_slot("Нет наборов публикаций в ручном режиме для повтора.\n")
                QMessageBox.information(self, "Повтор", "План публикаций пуст. Добавьте наборы для публикации.")
                self.update_retry_button_state()
                return
            posting_schedule_for_retry = list(self.posting_schedule)

        self.posting_start_time = time.monotonic()
        self.elapsed_seconds = 0
        self.elapsed_time_label.setText("Время: 00:00:00")
        self.runtime_qtimer.start(1000)
        
        # Проверка галочки для смены IP в начале для повтора
        if self.change_ip_start_check.isChecked():
            self.log_message_slot("Смена IP перед началом процесса...\n")
            self.run_flight_mode_scenario()
        
        self.posting_thread = PostingWorker(
            tokens_for_retry,
            posting_schedule_for_retry, 
            is_autopost,
            post_limit, 
            photo_count, 
            self.worker_signals,
            self.ip_change_limit,  # Лимит IP
            self  # Ссылка на главное окно
        )
        self.posting_thread.start()
        self.progress_bar.setValue(0)

    def stop_posting_handler(self):
        if self.posting_thread and self.posting_thread.isRunning():
            stop_flag.set()
            self.log_message_slot("Остановка публикации...\n")
            self.stop_button.setEnabled(False)

    def clear_previous_results(self):
        self.output_text.clear()
        self.successful_text.clear()
        self.failed_text.clear()
        self.successful_posts_label.setText("Успешные публикации: 0")
        self.failed_posts_label.setText("Неуспешные публикации: 0")
        
        self.token_error_specific_count = 0
        self.banned_token_specific_count = 0
        self.other_error_specific_count = 0 
        self.token_errors_specific_label.setText("Ошибка авторизации (токен): 0")
        self.banned_tokens_specific_label.setText("Аккаунт заблокирован: 0")
        self.other_errors_specific_label.setText("Прочие ошибки: 0")

        self.auth_error_tokens.clear()
        self.banned_error_tokens.clear()
        self.other_error_tokens.clear()
        
        self.elapsed_time_label.setText("Время: 00:00:00") 
        self.elapsed_seconds = 0
        self.update_retry_button_state()

    def log_message_slot(self, message):
        self.output_text.append(message.strip())
        if self.auto_scroll_log: 
            self.output_text.verticalScrollBar().setValue(self.output_text.verticalScrollBar().maximum())

    def update_progress_labels_slot(self, success_count, failed_count, total_expected_posts):
        self.successful_posts_label.setText(f"Успешные публикации: {success_count}")
        self.failed_posts_label.setText(f"Неуспешные публикации: {failed_count}") 
        remaining = max(0, total_expected_posts - (success_count + failed_count))
        self.progress_label.setText(f"Осталось попыток постов (из общего плана): {remaining}")

    def successful_post_slot(self, message, link):
        self.successful_text.append(f"{message} <a href='{link}' style='color: #6897bb;'>{link}</a><br>")
        self.save_last_post_date()
        self.update_photos_count_label()

    def failed_post_slot(self, message, token): 
        self.failed_text.append(message.strip())

        is_token_error = "User authorization failed: invalid access_token (4)" in message
        is_banned_error = ("account has been blocked" in message.lower() or
                           "user is blocked" in message.lower() or
                           "user was banned" in message.lower())
        
        if token:
            if is_token_error:
                self.token_error_specific_count += 1
                self.token_errors_specific_label.setText(f"Ошибка авторизации (токен): {self.token_error_specific_count}")
                self.auth_error_tokens.add(token)
            elif is_banned_error: 
                self.banned_token_specific_count += 1
                self.banned_tokens_specific_label.setText(f"Аккаунт заблокирован: {self.banned_token_specific_count}")
                self.banned_error_tokens.add(token)
            else: 
                self.other_error_specific_count += 1
                self.other_errors_specific_label.setText(f"Прочие ошибки: {self.other_error_specific_count}")
                self.other_error_tokens.add(token)
        
        self.update_retry_button_state()

    def toggle_posting_buttons_slot(self, enable_main_actions):
        self.post_button.setEnabled(enable_main_actions)
        self.stop_button.setEnabled(not enable_main_actions)
        
        self.add_schedule_button.setEnabled(enable_main_actions and not self.autopost_check.isChecked())
        self.remove_schedule_button.setEnabled(enable_main_actions and bool(self.posting_schedule))
        self.account_button.setEnabled(enable_main_actions)
        self.autopost_check.setEnabled(enable_main_actions)
        
        if enable_main_actions and self.autopost_check.isChecked():
             self.post_limit_entry.setEnabled(True)
             self.photo_count_entry.setEnabled(True)
        elif not enable_main_actions and self.autopost_check.isChecked():
            self.post_limit_entry.setEnabled(False)
            self.photo_count_entry.setEnabled(False)

        self.update_retry_button_state()

    def on_posting_finished(self):
        self.runtime_qtimer.stop() 
        if self.posting_thread:
            self.final_failed_tokens_for_ui_copy = self.posting_thread.get_final_failed_tokens()
            self.final_successful_tokens_for_ui_copy = self.posting_thread.get_final_successful_tokens()
        
        self.toggle_posting_buttons_slot(True) 
        
        self.posting_thread = None
        status = "Остановлено" if stop_flag.is_set() else "Готово"
        
        stop_flag.clear()
        if self.progress_bar.value() < self.progress_bar.maximum():
             self.progress_bar.setValue(self.progress_bar.maximum())
        
        self.update_photos_count_label()

        current_time = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
        successful = int(self.successful_posts_label.text().split(": ")[1])
        failed = int(self.failed_posts_label.text().split(": ")[1])
        tokens_count = len(self.tokens)
        photos_count = self.count_photos_in_posts_folder()
        
        try:
            with open(STATISTICS_FILE_PATH, 'a', encoding='utf-8') as f:
                f.write(f"{current_time},{successful},{failed},{tokens_count},{photos_count},{status}\n")
        except Exception as e:
            self.log_message_slot(f"Ошибка сохранения статистики: {e}\n")

        # Проверка галочки для смены IP в конце
        if self.change_ip_end_check.isChecked():
            self.log_message_slot("Смена IP после завершения процесса...\n")
            self.run_flight_mode_scenario()

    def closeEvent(self, event):
        if self.posting_thread and self.posting_thread.isRunning():
            reply = QMessageBox.question(self, "Подтверждение",
                                         "Процесс публикации еще активен. Вы уверены, что хотите выйти?",
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                stop_flag.set()
                if self.posting_thread: 
                    self.posting_thread.wait(5000)
                self.runtime_qtimer.stop() 
                self.photos_count_timer.stop()
                self.info_update_timer.stop()
                event.accept()
            else: 
                event.ignore()
        else: 
            self.runtime_qtimer.stop()
            self.photos_count_timer.stop() 
            self.info_update_timer.stop()
            event.accept()

    # Методы для смены IP
    def scan_devices_async(self):
        """Асинхронное сканирование подключённых устройств через ADB"""
        def worker():
            self.devices_by_model.clear()
            try:
                result = subprocess.run(["adb", "devices", "-l"], capture_output=True, text=True, check=False)
                output = result.stdout.strip().splitlines()
                for line in output[1:]:  # Пропускаем первую строку
                    line = line.strip()
                    if not line or "offline" in line or "unauthorized" in line or "unknown" in line:
                        continue
                    parts = line.split()
                    serial = parts[0]
                    model = None
                    for p in parts:
                        if p.startswith("model:"):
                            model = p.split(":", 1)[1]
                            break
                    if not model:
                        model = "UnknownModel"
                    self.devices_by_model[model] = serial
                print(f"Обнаруженные устройства: {self.devices_by_model}")
            except Exception as e:
                print(f"Ошибка при сканировании устройств: {e}")
            model_list = list(self.devices_by_model.keys())
            ip_signals.device_scan_completed.emit(model_list)

        thread = threading.Thread(target=worker, daemon=True)
        thread.start()

    def update_device_list(self, model_list):
        """Обновление комбо-бокса списком устройств"""
        # Сохраняем текущий выбор перед обновлением
        current_model = self.device_combobox.currentText()
        
        self.device_combobox.blockSignals(True)  # Блокируем сигналы, чтобы не вызывать сохранение во время обновления
        self.device_combobox.clear()
        if model_list:
            self.device_combobox.addItems(model_list)
        else:
            self.device_combobox.addItem("Нет подключённых устройств")
        
        # Восстанавливаем предыдущий выбор
        index = self.device_combobox.findText(current_model)
        if index != -1:
            self.device_combobox.setCurrentIndex(index)
        else:
            saved_model = self.load_last_model()
            index = self.device_combobox.findText(saved_model)
            if index != -1:
                self.device_combobox.setCurrentIndex(index)
        
        self.device_combobox.blockSignals(False)  # Разблокируем сигналы
        
        # Сохраняем текущее значение после обновления
        current_after_update = self.device_combobox.currentText()
        if current_after_update != "Нет подключённых устройств":
            self.save_last_model(current_after_update)

        self.update_add_schedule_button_state()

    def on_model_selected(self, model):
        """Обработчик выбора модели устройства"""
        print(f"Выбрана модель: {model}")
        self.save_last_model(model)
        self.update_add_schedule_button_state()  # Обновляем UI

    def save_last_model(self, model):
        """Сохраняет выбранную модель в файл"""
        try:
            with open(LAST_MODEL_FILE_PATH, 'w', encoding='utf-8') as f:
                f.write(model)
        except Exception as e:
            self.log_message_slot(f"Ошибка сохранения выбранной модели: {e}\n")

    def load_last_model(self):
        """Загружает выбранную модель из файла"""
        if os.path.exists(LAST_MODEL_FILE_PATH):
            try:
                with open(LAST_MODEL_FILE_PATH, 'r', encoding='utf-8') as f:
                    return f.read().strip()
            except Exception as e:
                self.log_message_slot(f"Ошибка загрузки выбранной модели: {e}\n")
                return ""
        return ""

    def run_flight_mode_scenario(self):
        """Сценарий смены IP: включение/выключение авиарежима и tethering на выбранном устройстве"""
        chosen_model = self.device_combobox.currentText()
        if chosen_model == "Нет подключённых устройств" or chosen_model not in self.devices_by_model:
            self.log_message_slot("Ошибка: Не выбрано устройство или оно недоступно.\n")
            return

        serial = self.devices_by_model[chosen_model]
        try:
            self.log_message_slot(f"Запуск сценария смены IP на устройстве '{chosen_model}' (serial: {serial}).\n")
            d = u2.connect(serial)  # Подключение к устройству через uiautomator2
            self.log_message_slot(f"Информация об устройстве: {d.device_info}\n")

            # Шаг 1: Открыть настройки авиарежима
            subprocess.run(["adb", "-s", serial, "shell", "am", "start", "-a", "android.settings.AIRPLANE_MODE_SETTINGS"])
            time.sleep(0.3)

            # Шаг 2: Переключить авиарежим (вкл/выкл)
            switch_element = d(resourceId="android:id/switch_widget")
            if switch_element.exists(timeout=3):
                switch_element.click()  # Включить/выключить
                time.sleep(0.3)
                switch_element.click()  # Вернуть в исходное (для цикла)
                time.sleep(0.5)
            else:
                self.log_message_slot("Ошибка: Переключатель авиарежима не найден.\n")

            # Шаг 3: Активировать tethering (мобильный hotspot для смены IP)
            d.app_start("com.android.settings", ".TetherSettings")
            time.sleep(0.3)
            if d(resourceId="com.android.settings:id/recycler_view").child(index=0).exists(timeout=2):
                d(resourceId="com.android.settings:id/recycler_view").child(index=0).click()
                time.sleep(0.3)
            else:
                self.log_message_slot("Ошибка: Элемент tethering не найден.\n")

            # Шаг 4: Закрыть настройки (нажатие 'back' несколько раз)
            for _ in range(3):
                d.press("back")
                time.sleep(0.2)

            self.log_message_slot("Сценарий смены IP завершён. Ожидайте подключения к новой сети.\n")
            time.sleep(1)  # Задержка для стабилизации соединения

        except Exception as e:
            self.log_message_slot(f"Ошибка при выполнении сценария: {e}\n")

# Запуск приложения
if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    QStyleFactory.create('Fusion')
    app.setStyleSheet(DARK_STYLESHEET)
    mainWin = VKAutoPosterApp()
    mainWin.show()
    sys.exit(app.exec_())
