Загрузка данных


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
STALCRAFT Auction Bot - Рабочая версия
Версия: 1.0.1
"""

import sys
import os
import json
import time
import threading
import logging
from datetime import datetime
from typing import Optional, Dict, List, Tuple
from dataclasses import dataclass, asdict

# Установка зависимостей
try:
    import tkinter as tk
    from tkinter import ttk, messagebox, filedialog
    import pyautogui
    import pytesseract
    from PIL import ImageGrab, Image, ImageEnhance, ImageFilter
    import cv2
    import numpy as np
except ImportError as e:
    print(f"Ошибка импорта: {e}")
    print("Устанавливаю зависимости...")
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "pytesseract", "opencv-python", "Pillow", "pyautogui", "keyboard"])
    print("Зависимости установлены. Перезапустите программу.")
    sys.exit(1)

# Настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('stalcraft_bot.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# ============= КЛАСС НАСТРОЕК =============
class Config:
    """Класс для хранения всех настроек"""
    
    def __init__(self):
        self.search_text = ""
        self.max_price = 100000
        self.pages_count = 3
        self.use_pages = True
        self.auto_buy = False
        self.test_mode = True
        
        self.coordinates = {
            'input_field': {'x': 0, 'y': 0},
            'refresh_button': {'x': 0, 'y': 0},
            'slider': {'x': 0, 'y': 0},
            'ok_button': {'x': 0, 'y': 0}
        }
        
        self.ocr_areas = {
            'prices': {'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0},
            'buy_button': {'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0},
            'pages': {'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0}
        }
        
        self.ocr_settings = {
            'lang': 'rus',
            'scale': 200,
            'psm': 6,
            'attempts': 3,
            'confidence_threshold': 70,
            'preprocessing': {
                'increase': True,
                'bw': True,
                'contrast': True,
                'binarization': False,
                'denoise': True,
                'sharpness': False
            }
        }
    
    def save(self, filename: str = "config.json"):
        """Сохранение настроек в файл"""
        try:
            data = {
                'search_text': self.search_text,
                'max_price': self.max_price,
                'pages_count': self.pages_count,
                'use_pages': self.use_pages,
                'auto_buy': self.auto_buy,
                'test_mode': self.test_mode,
                'coordinates': self.coordinates,
                'ocr_areas': self.ocr_areas,
                'ocr_settings': self.ocr_settings
            }
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=4, ensure_ascii=False)
            logger.info(f"Настройки сохранены в {filename}")
            return True
        except Exception as e:
            logger.error(f"Ошибка сохранения настроек: {e}")
            return False
    
    def load(self, filename: str = "config.json"):
        """Загрузка настроек из файла"""
        try:
            if os.path.exists(filename):
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    for key, value in data.items():
                        if hasattr(self, key):
                            setattr(self, key, value)
                logger.info(f"Настройки загружены из {filename}")
                return True
        except Exception as e:
            logger.error(f"Ошибка загрузки настроек: {e}")
        return False

# ============= КЛАСС OCR ОБРАБОТЧИКА =============
class OCRProcessor:
    """Обработка OCR с настраиваемыми параметрами"""
    
    def __init__(self, settings: Dict):
        self.settings = settings
        self.last_results = []
        
        # Проверка наличия Tesseract
        try:
            pytesseract.get_tesseract_version()
            logger.info("Tesseract найден")
        except:
            logger.error("Tesseract не найден!")
            messagebox.showerror("Ошибка", 
                "Tesseract OCR не найден!\n\n"
                "Установите Tesseract:\n"
                "1. Скачайте с https://github.com/UB-Mannheim/tesseract/wiki\n"
                "2. Установите в C:\\Program Files\\Tesseract-OCR\\\n"
                "3. Добавьте в PATH или укажите путь в коде")
    
    def preprocess_image(self, image: Image.Image) -> Image.Image:
        """Предобработка изображения"""
        try:
            # Конвертация в numpy array
            img = np.array(image)
            
            # Масштабирование
            if self.settings['scale'] != 100:
                scale = self.settings['scale'] / 100
                width = int(img.shape[1] * scale)
                height = int(img.shape[0] * scale)
                img = cv2.resize(img, (width, height), interpolation=cv2.INTER_CUBIC)
            
            # Чёрно-белое
            if self.settings['preprocessing']['bw'] and len(img.shape) == 3:
                img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            
            # Повышение контрастности
            if self.settings['preprocessing']['contrast']:
                if len(img.shape) == 3:
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
                alpha = 1.5
                beta = 30
                img = cv2.convertScaleAbs(img, alpha=alpha, beta=beta)
            
            # Бинаризация
            if self.settings['preprocessing']['binarization']:
                if len(img.shape) == 3:
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
                _, img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
            
            # Удаление шума
            if self.settings['preprocessing']['denoise']:
                if len(img.shape) == 3:
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
                img = cv2.medianBlur(img, 3)
            
            # Резкость
            if self.settings['preprocessing']['sharpness']:
                if len(img.shape) == 3:
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
                kernel = np.array([[-1,-1,-1], [-1, 9,-1], [-1,-1,-1]])
                img = cv2.filter2D(img, -1, kernel)
            
            return Image.fromarray(img)
        except Exception as e:
            logger.error(f"Ошибка предобработки: {e}")
            return image
    
    def extract_prices(self, image: Image.Image) -> List[Dict]:
        """Извлечение цен из изображения"""
        results = []
        
        for attempt in range(self.settings['attempts']):
            try:
                # Предобработка
                processed = self.preprocess_image(image)
                
                # Настройка Tesseract для цен
                custom_config = f'--oem 3 --psm {self.settings["psm"]} -c tessedit_char_whitelist=0123456789 '
                
                # Распознавание
                data = pytesseract.image_to_data(
                    processed,
                    lang=self.settings['lang'],
                    config=custom_config,
                    output_type=pytesseract.Output.DICT
                )
                
                # Парсинг результатов
                prices = self._parse_prices(data)
                if prices:
                    results.append(prices)
                    
            except Exception as e:
                logger.error(f"Ошибка OCR (попытка {attempt+1}): {e}")
                continue
        
        # Валидация результатов
        return self._validate_results(results)
    
    def _parse_prices(self, data: Dict) -> List[Dict]:
        """Парсинг цен из данных Tesseract"""
        prices = []
        n_boxes = len(data['text'])
        
        for i in range(n_boxes):
            try:
                text = data['text'][i].strip()
                if not text:
                    continue
                    
                conf = int(data['conf'][i])
                
                # Проверка уверенности
                if conf < self.settings['confidence_threshold']:
                    continue
                
                # Очистка текста
                clean_text = ''.join(filter(str.isdigit, text))
                if clean_text and len(clean_text) >= 4:
                    price = int(clean_text)
                    if 1000 <= price <= 10000000:
                        prices.append({
                            'price': price,
                            'confidence': conf,
                            'bbox': (
                                data['left'][i],
                                data['top'][i],
                                data['width'][i],
                                data['height'][i]
                            ),
                            'text': text
                        })
            except (ValueError, IndexError) as e:
                continue
        
        return prices
    
    def _validate_results(self, results: List[List[Dict]]) -> List[Dict]:
        """Валидация результатов OCR"""
        if not results:
            return []
        
        # Если только одна попытка
        if len(results) == 1:
            return results[0]
        
        # Проверка стабильности
        stable_prices = []
        for i in range(len(results[0])):
            price = results[0][i]['price']
            is_stable = all(
                r[i]['price'] == price if i < len(r) else False
                for r in results[1:]
            )
            if is_stable:
                stable_prices.append(results[0][i])
        
        return stable_prices
    
    def extract_text(self, image: Image.Image) -> Dict:
        """Извлечение текста (для кнопок)"""
        try:
            processed = self.preprocess_image(image)
            custom_config = f'--oem 3 --psm 6'
            
            data = pytesseract.image_to_data(
                processed,
                lang=self.settings['lang'],
                config=custom_config,
                output_type=pytesseract.Output.DICT
            )
            
            # Поиск текста
            texts = {}
            for i, text in enumerate(data['text']):
                if text.strip():
                    texts[text.strip().lower()] = {
                        'bbox': (data['left'][i], data['top'][i], 
                                data['width'][i], data['height'][i]),
                        'conf': int(data['conf'][i])
                    }
            return texts
            
        except Exception as e:
            logger.error(f"Ошибка извлечения текста: {e}")
            return {}

# ============= КЛАСС БОТА =============
class AuctionBot:
    """Основной движок бота"""
    
    def __init__(self, config: Config):
        self.config = config
        self.ocr = OCRProcessor(config.ocr_settings)
        self.is_running = False
        self.current_page = 1
        self.purchases_count = 0
        self.total_spent = 0
        self.last_status = "Готов к работе"
        
        # Настройка pyautogui
        pyautogui.FAILSAFE = True
        pyautogui.PAUSE = 0.1
    
    def start(self):
        """Запуск бота"""
        if self.is_running:
            return
        
        # Проверка настроек
        if not self._validate_config():
            self.last_status = "Ошибка: проверьте настройки"
            return
        
        self.is_running = True
        self.current_page = 1
        
        # Запуск в отдельном потоке
        self.bot_thread = threading.Thread(target=self._run)
        self.bot_thread.daemon = True
        self.bot_thread.start()
        
        logger.info("Бот запущен")
        self.last_status = "Бот запущен"
    
    def stop(self):
        """Остановка бота"""
        self.is_running = False
        logger.info("Бот остановлен")
        self.last_status = "Бот остановлен"
    
    def _run(self):
        """Основной цикл бота"""
        while self.is_running:
            try:
                logger.info(f"Цикл #{self.purchases_count + 1}")
                self.last_status = "Ввод товара..."
                
                # 1. Ввод товара
                if not self._input_search_text():
                    self.last_status = "Ошибка ввода товара"
                    continue
                
                # 2. Обновление списка
                self.last_status = "Обновление списка..."
                if not self._refresh_auction():
                    self.last_status = "Ошибка обновления"
                    continue
                
                # 3. Поиск лотов
                self.last_status = "Поиск лотов..."
                if self._search_lots():
                    continue
                
                time.sleep(1)
                
            except Exception as e:
                logger.error(f"Ошибка: {e}")
                self.last_status = f"Ошибка: {str(e)[:30]}"
                time.sleep(3)
    
    def _validate_config(self) -> bool:
        """Проверка настроек"""
        if not self.config.search_text:
            logger.warning("Не указан товар")
            return False
        
        if self.config.max_price <= 0:
            logger.warning("Некорректная цена")
            return False
        
        # Проверка координат
        for key, coords in self.config.coordinates.items():
            if coords['x'] == 0 and coords['y'] == 0:
                logger.warning(f"Не настроены координаты: {key}")
                return False
        
        # Проверка OCR областей
        for key, area in self.config.ocr_areas.items():
            if area['x1'] == area['x2'] or area['y1'] == area['y2']:
                logger.warning(f"Не настроена OCR область: {key}")
                return False
        
        return True
    
    def _input_search_text(self) -> bool:
        """Ввод текста поиска"""
        try:
            coords = self.config.coordinates['input_field']
            pyautogui.click(coords['x'], coords['y'])
            time.sleep(0.3)
            
            # Очистка
            pyautogui.hotkey('ctrl', 'a')
            time.sleep(0.1)
            pyautogui.press('delete')
            time.sleep(0.1)
            
            # Ввод
            pyautogui.write(self.config.search_text)
            time.sleep(0.2)
            
            logger.info(f"Введён текст: {self.config.search_text}")
            return True
            
        except Exception as e:
            logger.error(f"Ошибка ввода: {e}")
            return False
    
    def _refresh_auction(self) -> bool:
        """Обновление списка"""
        try:
            coords = self.config.coordinates['refresh_button']
            pyautogui.click(coords['x'], coords['y'])
            time.sleep(2)
            
            logger.info("Список обновлён")
            return True
            
        except Exception as e:
            logger.error(f"Ошибка обновления: {e}")
            return False
    
    def _search_lots(self) -> bool:
        """Поиск лотов с прокруткой"""
        # Проверка видимой области
        if self._check_and_buy():
            return True
        
        # Прокрутка
        for step in range(80, 401, 80):
            if not self.is_running:
                return False
            
            self._scroll_down(step)
            
            if self._check_and_buy():
                return True
        
        # Переключение страницы
        if self.config.use_pages and self.current_page < self.config.pages_count:
            self.current_page += 1
            if self._switch_page(self.current_page):
                return self._search_lots()
        
        return False
    
    def _check_and_buy(self) -> bool:
        """Проверка лотов"""
        try:
            area = self.config.ocr_areas['prices']
            screenshot = ImageGrab.grab(bbox=(
                area['x1'], area['y1'],
                area['x2'], area['y2']
            ))
            
            prices = self.ocr.extract_prices(screenshot)
            
            if not prices:
                return False
            
            # Поиск минимальной цены
            valid_prices = [p for p in prices if p['price'] <= self.config.max_price]
            
            if not valid_prices:
                return False
            
            best = min(valid_prices, key=lambda x: x['price'])
            
            logger.info(f"Найден лот: {best['price']:,} (уверенность: {best['confidence']}%)")
            self.last_status = f"Найден лот: {best['price']:,}"
            
            # Тестовый режим
            if self.config.test_mode:
                self.last_status += " (ТЕСТ)"
                return False
            
            # Покупка
            if self.config.auto_buy:
                return self._buy_item(best)
            
            return False
            
        except Exception as e:
            logger.error(f"Ошибка проверки: {e}")
            return False
    
    def _buy_item(self, price_data: Dict) -> bool:
        """Покупка лота"""
        try:
            # Клик по лоту
            bbox = price_data['bbox']
            area = self.config.ocr_areas['prices']
            
            click_x = area['x1'] + bbox[0] + bbox[2] // 2
            click_y = area['y1'] + bbox[1] + bbox[3] // 2
            
            pyautogui.click(click_x, click_y)
            time.sleep(0.5)
            
            # Поиск кнопки
            if not self._find_buy_button():
                return False
            
            # Подтверждение
            coords = self.config.coordinates['ok_button']
            pyautogui.click(coords['x'], coords['y'])
            time.sleep(0.5)
            
            self.purchases_count += 1
            self.total_spent += price_data['price']
            
            logger.info(f"Куплено за {price_data['price']:,}")
            self.last_status = f"Куплено за {price_data['price']:,}"
            
            return True
            
        except Exception as e:
            logger.error(f"Ошибка покупки: {e}")
            return False
    
    def _find_buy_button(self) -> bool:
        """Поиск кнопки выкупа"""
        try:
            area = self.config.ocr_areas['buy_button']
            screenshot = ImageGrab.grab(bbox=(
                area['x1'], area['y1'],
                area['x2'], area['y2']
            ))
            
            texts = self.ocr.extract_text(screenshot)
            
            # Поиск ключевых слов
            keywords = ['выкупить', 'купить', 'выкуп', 'лот', 'приобрести']
            
            for text, data in texts.items():
                text_lower = text.lower()
                for keyword in keywords:
                    if keyword in text_lower:
                        bbox = data['bbox']
                        click_x = area['x1'] + bbox[0] + bbox[2] // 2
                        click_y = area['y1'] + bbox[1] + bbox[3] // 2
                        
                        pyautogui.click(click_x, click_y)
                        logger.info(f"Кнопка найдена: {text}")
                        return True
            
            return False
            
        except Exception as e:
            logger.error(f"Ошибка поиска кнопки: {e}")
            return False
    
    def _scroll_down(self, steps: int):
        """Прокрутка"""
        try:
            slider = self.config.coordinates['slider']
            pyautogui.moveTo(slider['x'], slider['y'])
            pyautogui.mouseDown()
            pyautogui.moveTo(slider['x'], slider['y'] + steps, duration=0.2)
            pyautogui.mouseUp()
            time.sleep(0.3)
            
        except Exception as e:
            logger.error(f"Ошибка прокрутки: {e}")
    
    def _switch_page(self, page_number: int) -> bool:
        """Переключение страницы"""
        try:
            area = self.config.ocr_areas['pages']
            screenshot = ImageGrab.grab(bbox=(
                area['x1'], area['y1'],
                area['x2'], area['y2']
            ))
            
            processed = self.ocr.preprocess_image(screenshot)
            custom_config = '--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789'
            
            data = pytesseract.image_to_data(
                processed,
                lang='rus',
                config=custom_config,
                output_type=pytesseract.Output.DICT
            )
            
            for i, text in enumerate(data['text']):
                if text.strip() == str(page_number):
                    bbox = (data['left'][i], data['top'][i],
                           data['width'][i], data['height'][i])
                    
                    click_x = area['x1'] + bbox[0] + bbox[2] // 2
                    click_y = area['y1'] + bbox[1] + bbox[3] // 2
                    
                    pyautogui.click(click_x, click_y)
                    time.sleep(1)
                    
                    logger.info(f"Страница {page_number}")
                    return True
            
            return False
            
        except Exception as e:
            logger.error(f"Ошибка переключения: {e}")
            return False
    
    def get_stats(self) -> Dict:
        """Получение статистики"""
        return {
            'purchases': self.purchases_count,
            'spent': self.total_spent,
            'status': self.last_status,
            'running': self.is_running
        }

# ============= КЛАСС ГРАФИЧЕСКОГО ИНТЕРФЕЙСА =============
class BotGUI:
    """Главное окно приложения"""
    
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("STALCRAFT Auction Bot")
        self.root.geometry("700x800")
        self.root.resizable(False, False)
        
        # Конфигурация
        self.config = Config()
        self.config.load()
        
        # Бот
        self.bot = AuctionBot(self.config)
        
        # Переменные для координат
        self.coord_vars = {}
        self.area_vars = {}
        
        # Создание интерфейса
        self._setup_ui()
        
        # Обработка закрытия
        self.root.protocol("WM_DELETE_WINDOW", self._on_close)
        
        # Обновление статуса
        self._update_status()
    
    def _setup_ui(self):
        """Настройка интерфейса"""
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky="nsew")
        
        # Заголовок
        title = ttk.Label(main_frame, text="STALCRAFT AUCTION BOT", 
                         font=('Arial', 16, 'bold'))
        title.grid(row=0, column=0, columnspan=3, pady=(0, 10))
        
        # Основные настройки
        basic_frame = ttk.LabelFrame(main_frame, text="Основные настройки", padding="10")
        basic_frame.grid(row=1, column=0, columnspan=3, sticky="ew", pady=5)
        
        # Товар
        ttk.Label(basic_frame, text="Товар:").grid(row=0, column=0, sticky="w")
        self.search_var = tk.StringVar(value=self.config.search_text)
        entry = ttk.Entry(basic_frame, textvariable=self.search_var, width=30)
        entry.grid(row=0, column=1, padx=5, sticky="w")
        
        # Цена
        ttk.Label(basic_frame, text="Макс. цена:").grid(row=1, column=0, sticky="w", pady=5)
        self.price_var = tk.StringVar(value=str(self.config.max_price))
        entry_price = ttk.Entry(basic_frame, textvariable=self.price_var, width=15)
        entry_price.grid(row=1, column=1, padx=5, sticky="w")
        
        # Страницы
        ttk.Label(basic_frame, text="Страниц:").grid(row=2, column=0, sticky="w")
        self.pages_var = tk.StringVar(value=str(self.config.pages_count))
        entry_pages = ttk.Entry(basic_frame, textvariable=self.pages_var, width=8)
        entry_pages.grid(row=2, column=1, padx=5, sticky="w")
        
        self.use_pages_var = tk.BooleanVar(value=self.config.use_pages)
        ttk.Checkbutton(basic_frame, text="Использовать страницы", 
                       variable=self.use_pages_var).grid(row=2, column=2, padx=5)
        
        # Координаты
        coord_frame = ttk.LabelFrame(main_frame, text="Координаты", padding="10")
        coord_frame.grid(row=2, column=0, columnspan=3, sticky="ew", pady=5)
        
        coord_items = [
            ("Поле ввода", "input_field"),
            ("Обновить", "refresh_button"),
            ("Ползунок", "slider"),
            ("ОК", "ok_button")
        ]
        
        for i, (label, key) in enumerate(coord_items):
            ttk.Label(coord_frame, text=f"{label}:").grid(row=i, column=0, sticky="w", padx=5)
            
            coords = self.config.coordinates[key]
            self.coord_vars[key] = tk.StringVar(
                value=f"X={coords['x']} Y={coords['y']}"
            )
            ttk.Label(coord_frame, textvariable=self.coord_vars[key]).grid(
                row=i, column=1, padx=5, sticky="w"
            )
            
            ttk.Button(coord_frame, text="Настроить", 
                      command=lambda k=key: self._set_coordinate(k)).grid(
                row=i, column=2, padx=5
            )
        
        # OCR области
        ocr_frame = ttk.LabelFrame(main_frame, text="OCR области", padding="10")
        ocr_frame.grid(row=3, column=0, columnspan=3, sticky="ew", pady=5)
        
        ocr_items = [
            ("Цены", "prices"),
            ("Кнопка выкупа", "buy_button"),
            ("Страницы", "pages")
        ]
        
        for i, (label, key) in enumerate(ocr_items):
            ttk.Label(ocr_frame, text=f"{label}:").grid(row=i, column=0, sticky="w", padx=5)
            
            area = self.config.ocr_areas[key]
            self.area_vars[key] = tk.StringVar(
                value=f"({area['x1']},{area['y1']}) → ({area['x2']},{area['y2']})"
            )
            ttk.Label(ocr_frame, textvariable=self.area_vars[key]).grid(
                row=i, column=1, padx=5, sticky="w"
            )
            
            ttk.Button(ocr_frame, text="Настроить", 
                      command=lambda k=key: self._set_ocr_area(k)).grid(
                row=i, column=2, padx=5
            )
        
        # Кнопки управления
        control_frame = ttk.Frame(main_frame)
        control_frame.grid(row=4, column=0, columnspan=3, pady=10)
        
        # Режим
        ttk.Label(control_frame, text="Режим:").grid(row=0, column=0, padx=5)
        self.mode_var = tk.StringVar(value="ТЕСТ" if self.config.test_mode else "ВКЛ")
        mode_combo = ttk.Combobox(control_frame, textvariable=self.mode_var,
                                 values=['ТЕСТ', 'ВКЛ'], width=8, state='readonly')
        mode_combo.grid(row=0, column=1, padx=5)
        
        ttk.Button(control_frame, text="Проверить OCR", 
                  command=self._test_ocr).grid(row=0, column=2, padx=5)
        
        self.start_btn = ttk.Button(control_frame, text="▶ ЗАПУСК", 
                                   command=self._start_bot)
        self.start_btn.grid(row=0, column=3, padx=5)
        
        self.stop_btn = ttk.Button(control_frame, text="■ СТОП", 
                                  command=self._stop_bot, state='disabled')
        self.stop_btn.grid(row=0, column=4, padx=5)
        
        # Статус
        status_frame = ttk.LabelFrame(main_frame, text="Статус", padding="5")
        status_frame.grid(row=5, column=0, columnspan=3, sticky="ew", pady=5)
        
        self.status_var = tk.StringVar(value="Готов к работе")
        ttk.Label(status_frame, textvariable=self.status_var, font=('Arial', 10)).pack()
        
        self.stats_var = tk.StringVar(value="Покупок: 0 | Потрачено: 0")
        ttk.Label(status_frame, textvariable=self.stats_var, font=('Arial', 9)).pack()
        
        # Сохранение
        ttk.Button(main_frame, text="Сохранить настройки", 
                  command=self._save_config).grid(row=6, column=0, columnspan=3, pady=10)
    
    def _set_coordinate(self, key: str):
        """Установка координаты через клик"""
        self.root.iconify()
        time.sleep(0.3)
        
        # Получаем позицию мыши
        x, y = pyautogui.position()
        
        # Обновляем настройки
        self.config.coordinates[key]['x'] = x
        self.config.coordinates[key]['y'] = y
        
        # Обновляем отображение
        self.coord_vars[key].set(f"X={x} Y={y}")
        
        self.root.deiconify()
        logger.info(f"Координата {key}: X={x} Y={y}")
    
    def _set_ocr_area(self, key: str):
        """Установка OCR области через два клика"""
        self.root.iconify()
        time.sleep(0.3)
        
        try:
            # Первая точка
            messagebox.showinfo("Установка области", 
                f"Наведите мышь на ЛЕВЫЙ ВЕРХНИЙ угол области '{key}' и нажмите ОК")
            x1, y1 = pyautogui.position()
            
            # Вторая точка
            messagebox.showinfo("Установка области", 
                f"Наведите мышь на ПРАВЫЙ НИЖНИЙ угол области '{key}' и нажмите ОК")
            x2, y2 = pyautogui.position()
            
            # Сохраняем
            self.config.ocr_areas[key] = {
                'x1': min(x1, x2),
                'y1': min(y1, y2),
                'x2': max(x1, x2),
                'y2': max(y1, y2)
            }
            
            # Обновляем отображение
            area = self.config.ocr_areas[key]
            self.area_vars[key].set(f"({area['x1']},{area['y1']}) → ({area['x2']},{area['y2']})")
            
            logger.info(f"OCR область {key}: {self.area_vars[key].get()}")
            
        except Exception as e:
            logger.error(f"Ошибка установки области: {e}")
        
        self.root.deiconify()
    
    def _test_ocr(self):
        """Тест OCR"""
        try:
            # Проверяем настройки областей
            area = self.config.ocr_areas['prices']
            if area['x1'] == area['x2'] or area['y1'] == area['y2']:
                messagebox.showwarning("Ошибка", "Сначала настройте область цен!")
                return
            
            # Делаем скриншот
            screenshot = ImageGrab.grab(bbox=(
                area['x1'], area['y1'],
                area['x2'], area['y2']
            ))
            
            # Распознаём
            prices = self.bot.ocr.extract_prices(screenshot)
            
            # Показываем результат
            result_text = "РЕЗУЛЬТАТЫ OCR ТЕСТА\n"
            result_text += "=" * 40 + "\n"
            result_text += f"Область: Цены\n"
            result_text += f"Язык: {self.config.ocr_settings['lang']}\n"
            result_text += f"Масштаб: {self.config.ocr_settings['scale']}%\n"
            result_text += f"PSM: {self.config.ocr_settings['psm']}\n"
            result_text += f"Попыток: {self.config.ocr_settings['attempts']}\n"
            result_text += f"Порог: {self.config.ocr_settings['confidence_threshold']}%\n"
            result_text += "=" * 40 + "\n\n"
            
            if prices:
                result_text += f"Найдено цен: {len(prices)}\n\n"
                for p in prices:
                    result_text += f"{p['price']:,}  — {p['confidence']}%\n"
                
                valid = [p for p in prices if p['price'] <= self.config.max_price]
                if valid:
                    best = min(valid, key=lambda x: x['price'])
                    result_text += f"\nПодходящих: {len(valid)}\n"
                    result_text += f"Минимальная: {best['price']:,}"
                else:
                    result_text += f"\nПодходящих: 0"
            else:
                result_text += "Цены не найдены!\n"
                result_text += "Проверьте настройки OCR и область распознавания."
            
            messagebox.showinfo("Тест OCR", result_text)
            
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка теста OCR: {e}")
    
    def _start_bot(self):
        """Запуск бота"""
        # Обновляем настройки
        self.config.search_text = self.search_var.get()
        try:
            self.config.max_price = int(self.price_var.get())
            self.config.pages_count = int(self.pages_var.get())
        except ValueError:
            messagebox.showerror("Ошибка", "Некорректное значение цены или страниц!")
            return
        
        self.config.use_pages = self.use_pages_var.get()
        self.config.test_mode = self.mode_var.get() == "ТЕСТ"
        self.config.auto_buy = self.mode_var.get() == "ВКЛ"
        
        # Запуск
        self.bot.start()
        
        # Обновляем кнопки
        self.start_btn.config(state='disabled')
        self.stop_btn.config(state='normal')
    
    def _stop_bot(self):
        """Остановка бота"""
        self.bot.stop()
        
        # Обновляем кнопки
        self.start_btn.config(state='normal')
        self.stop_btn.config(state='disabled')
    
    def _save_config(self):
        """Сохранение настроек"""
        # Обновляем настройки из полей
        self.config.search_text = self.search_var.get()
        try:
            self.config.max_price = int(self.price_var.get())
            self.config.pages_count = int(self.pages_var.get())
        except:
            pass
        
        self.config.use_pages = self.use_pages_var.get()
        self.config.test_mode = self.mode_var.get() == "ТЕСТ"
        self.config.auto_buy = self.mode_var.get() == "ВКЛ"
        
        if self.config.save():
            messagebox.showinfo("Успех", "Настройки сохранены!")
        else:
            messagebox.showerror("Ошибка", "Не удалось сохранить настройки!")
    
    def _update_status(self):
        """Обновление статуса"""
        try:
            stats = self.bot.get_stats()
            self.status_var.set(stats['status'])
            self.stats_var.set(f"Покупок: {stats['purchases']} | Потрачено: {stats['spent']:,}")
        except:
            pass
        
        # Обновление каждую секунду
        self.root.after(1000, self._update_status)
    
    def _on_close(self):
        """Закрытие приложения"""
        self.bot.stop()
        self.config.save()
        self.root.destroy()

# ============= ЗАПУСК =============
if __name__ == "__main__":
    try:
        app = BotGUI()
        app.root.mainloop()
    except Exception as e:
        logger.error(f"Ошибка запуска: {e}")
        messagebox.showerror("Ошибка", f"Ошибка запуска приложения: {e}")
        sys.exit(1)