для метода enum_windows_callback нужно внести правки. раньше мы искали окно найти для блокнота и окно найти было отдельным открытым н фоне программа. новая программа для поиска EzCad2, в ней мы ищем внутри окно "текст". окна все находятся внутри основного окна программы в отличии от блокнота, где окно "Найти" было открыто параллельно

import json
import ctypes
import ctypes.wintypes
import time
import traceback
import pythoncom
import win32gui
import win32process
import psutil
import comtypes
import comtypes.client
import queue  # Для передачи данных из колбэка в основной поток
import redis
import threading
import tkinter as tk

from logger_setup import setup_logger
from constants import *
from hooks import HookManager
from redis_conn import redis_sync_conn
from tkinter import messagebox

# UIAutomation Client import после инициализации модуля
try:
    from comtypes.gen.UIAutomationClient import CUIAutomation

    uia = comtypes.client.CreateObject(CUIAutomation)
except ImportError:
    # Если он не сгенерен, можно сгенерировать вручную, или оставить как есть
    pass

# Переменные из win32api для SendMessage
user32 = ctypes.windll.user32

logger = setup_logger(__name__)

# Установка "статических" значений Redis
def init_redis_keys():
    redis_sync_conn.set('st_1d::', json.dumps(list(range(560, 980))))
    redis_sync_conn.set('st_1e::', json.dumps(list(range(999, 2500))))
    redis_sync_conn.set('st_1f::', json.dumps(list(range(2600, 3100))))

# ---- UIAutomation и поиск окна/элементов ----
def load_uia_module():
    try:
        comtypes.client.GetModule("UIAutomationCore.dll")
    except Exception as e:
        logger.error(f"Ошибка загрузки UIAutomationCore.dll: {e}")
        raise e

def find_target_process_and_window(target_process_name, target_window_substr):
    """
    Возвращает (find_window_hwnd, target_edit_hwnd, target_pid, target_thread_id) или (None, None, None, None)
    """
    windows = []

    def enum_windows_callback(hwnd, extra):
        if win32gui.IsWindowVisible(hwnd):
            title = win32gui.GetWindowText(hwnd)
            if target_window_substr in title:
                try:
                    thread_id, proc_id = win32process.GetWindowThreadProcessId(hwnd)
                    p = psutil.Process(proc_id)
                    if p.name().lower() == target_process_name.lower():
                        class_name = win32gui.GetClassName(hwnd)
                        logger.debug(f"Имя класса окна: {class_name}. Имя: {p.name()}")

                        windows.append((hwnd, proc_id, thread_id))
                        return
                except (psutil.NoSuchProcess, psutil.AccessDenied, win32process.error):
                    pass
        return True

    win32gui.EnumWindows(enum_windows_callback, None)
    if not windows:
        return None, None, None, None

    find_window_hwnd, target_pid, target_thread_id = windows[0]

    # Используем UI Automation для получения HWND поля Edit
    try:
        elem_dialog = uia.ElementFromHandle(find_window_hwnd)
        if not elem_dialog:
            logger.error("Не удалось получить UIA-элемент окна.")
            return None, None, None, None

        condition = uia.CreatePropertyCondition(UIA_ControlTypePropertyId, UIA_EditControlTypeId)
        elem_edit = elem_dialog.FindFirst(UIA_STANDARD_TREESCOPE_DESCENDANTS, condition)
        if not elem_edit:
            logger.error("Не найден элемент Edit в окне.")
            return None, None, None, None

        hwnd_edit = elem_edit.CurrentNativeWindowHandle

        if not hwnd_edit:
            logger.error("Не удалось получить HWND для Edit-элемента.")
            return None, None, None, None

        logger.info(f"Найден элемент Edit с HWND: {hwnd_edit}")
        return find_window_hwnd, hwnd_edit, target_pid, target_thread_id
    except Exception as e:
        logger.error(f"Ошибка UI Automation: {e}")
        return None, None, None, None

def get_text_from_hwnd(hwnd):
    if hwnd is None or hwnd == 0:
        return ""

    length = user32.SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0)
    if length == 0:
        return ""

    buf = ctypes.create_unicode_buffer(length + 1)
    user32.SendMessageW(hwnd, WM_GETTEXT, length + 1, ctypes.byref(buf))
    return buf.value

# --- Вспомогательные функции для UI ---
def get_hwnd_of_button(parent_hwnd, button_text):
    target_text = button_text.replace("&", "").strip()
    found_hwnds = []

    def enum_child(hwnd, lParam):
        class_name = win32gui.GetClassName(hwnd)
        text = win32gui.GetWindowText(hwnd)
        if class_name.lower() == "button" and text.replace("&", "").strip() == target_text:
            found_hwnds.append(hwnd)
            return False
        return True

    win32gui.EnumChildWindows(parent_hwnd, enum_child, None)
    return found_hwnds[0] if found_hwnds else None

def enable_window_and_refresh(hwnd_button, enable=True):
    win32gui.EnableWindow(hwnd_button, enable)
    parent_hwnd = win32gui.GetParent(hwnd_button)
    if parent_hwnd:
        user32.InvalidateRect(parent_hwnd, None, True)
        user32.UpdateWindow(parent_hwnd)
    else:
        user32.InvalidateRect(hwnd_button, None, True)
        user32.UpdateWindow(hwnd_button)

def is_window_valid(hwnd):
    """Проверить, что окно существует и видимо."""
    if not hwnd or hwnd == 0:
        return False
    return win32gui.IsWindow(hwnd) and win32gui.IsWindowVisible(hwnd)

def is_process_alive(pid):
    try:
        p = psutil.Process(pid)
        return p.is_running()
    except Exception:
        return False


##################################
# Здесь основные изменения: разделяем GUI и логику хуков

command_queue = queue.Queue()  # из GUI в хук
event_queue = queue.Queue()    # из хука в GUI

def hook_worker_main(stop_event):
    """
    Запускается в отдельном потоке, внутри HooksManager.
    Коммуникация с GUI — через очереди.
    """
    pythoncom.CoInitialize()
    hooks = None

    # target_process_name = "notepad.exe"
    target_process_name = "EzCad2.exe"
    # target_window_substr = "Найти"
    target_window_substr = "Текст"

    find_window_hwnd, target_edit_hwnd, target_pid, target_thread_id = (None, None, None, None)
    button_hwnd = None

    duplicate_confirmed = False
    duplicate_warning_sent = False

    def validate_text(text):
        nonlocal duplicate_confirmed, duplicate_warning_sent

        is_valid = len(text.strip()) > 0 and not any(c.isdigit() for c in text)

        if hooks is None:
            # Возможно, хуки ещё не установлены, просто игнорируем
            return

        try:
            if is_valid or duplicate_confirmed:
                if not win32gui.IsWindowEnabled(button_hwnd):
                    enable_window_and_refresh(button_hwnd, True)

                hooks.block_enter = False
                duplicate_warning_sent = False  # сброс при исправлении
                logger.info(f"Текст валиден: '{text}'. Кнопка и Enter разблокированы.")
            else:
                if win32gui.IsWindowEnabled(button_hwnd):
                    enable_window_and_refresh(button_hwnd, False)

                hooks.block_enter = True
                if not duplicate_warning_sent:
                    event_queue.put({'warning': 'duplicate'})
                    duplicate_warning_sent = True  # отправляем предупреждение один раз
                logger.info(f"Текст НЕ валиден: '{text}'. Кнопка и Enter заблокированы.")

        except Exception as e:
            logger.error(f"Ошибка в validate_text: {e}")

    try:
        while not stop_event.is_set():
            # Проверяем валидность окна/поля кнопки
            if not is_window_valid(find_window_hwnd) or not is_window_valid(target_edit_hwnd) or not button_hwnd or not is_window_valid(button_hwnd):
                if hooks:
                    logger.info("Окно или кнопка закрыты/недоступны. Удаляем хуки.")
                    hooks.remove_win_event_hook()
                    hooks.remove_keyboard_hook()
                    hooks = None

                logger.debug("Поиск окна 'Найти' и поле Edit...")
                find_window_hwnd, target_edit_hwnd, target_pid, target_thread_id = find_target_process_and_window(target_process_name, target_window_substr)

                if not all([find_window_hwnd, target_edit_hwnd, target_pid]):
                    duplicate_confirmed = False
                    duplicate_warning_sent = False
                    button_hwnd = None
                    time.sleep(1)
                    continue

                button_hwnd = get_hwnd_of_button(find_window_hwnd, "Найти далее")
                if not button_hwnd:
                    logger.warning("Кнопка 'Найти далее' не найдена, пробуем позже...")
                    find_window_hwnd = None
                    target_edit_hwnd = None
                    target_pid = None
                    time.sleep(1)
                    continue

                hooks = HookManager(lambda: target_edit_hwnd, lambda: find_window_hwnd)

                if not hooks.install_keyboard_hook():
                    logger.error("Не удалось установить клавиатурный хук. Пытаемся позже...")
                    hooks = None
                    time.sleep(1)
                    continue

                if not hooks.install_win_event_hook(target_pid):
                    logger.error("Не удалось установить WinEventHook. Пытаемся позже...")
                    hooks.remove_keyboard_hook()
                    hooks = None
                    time.sleep(1)
                    continue

                logger.info("Хуки установлены. Отслеживаем изменения.")

                event_queue.put({
                    'target_edit_hwnd': target_edit_hwnd,
                    'find_window_hwnd': find_window_hwnd,
                     'target_pid': target_pid
                     })

                try:
                    current_text = get_text_from_hwnd(target_edit_hwnd)
                    logger.debug(f"Начальная валидация текста: '{current_text}'")
                    validate_text(current_text)
                    event_queue.put(current_text)
                except Exception as e:
                    logger.error(f"Ошибка при получении текста для начальной валидации: {e}")

            # Обработка сообщений хуков
            if hooks:
                pythoncom.PumpWaitingMessages()

                # Обработка текстовых событий из hooks.text_queue
                try:
                    prev_text = ""

                    while True:
                        new_text = hooks.text_queue.get_nowait()
                        logger.debug(f"Получен текст из очереди: '{new_text}'")

                        if new_text != prev_text:
                            logger.debug("Текст изменился, сбрасываем подтверждение дубля")
                            duplicate_confirmed = False
                            prev_text = new_text  # обновляем

                        validate_text(new_text)
                        # Отправим текст в GUI (например, для обновления статуса)
                        event_queue.put(new_text)

                except queue.Empty:
                    pass

            # Обработка команд из GUI
            try:
                while True:
                    cmd = command_queue.get_nowait()
                    if cmd == "STOP":
                        logger.info("Получена команда STOP из GUI, выходим из хука.")
                        stop_event.set()
                        break
                    elif cmd == "CONFIRM_DUPLICATE":
                        logger.info("Получена команда подтверждения дубликата из GUI.")
                        duplicate_confirmed = True
                        duplicate_warning_sent = False

                        # Разблокируем кнопку и Enter
                        if hooks and button_hwnd:
                            if not win32gui.IsWindowEnabled(button_hwnd):
                                enable_window_and_refresh(button_hwnd, True)
                            hooks.block_enter = False
            except queue.Empty:
                pass

            time.sleep(0.05)

    except Exception as e:
        logger.critical(f"Ошибка в hook_worker_main: {e}")
        traceback.print_exc()
    finally:
        if hooks:
            hooks.remove_win_event_hook()
            hooks.remove_keyboard_hook()
        pythoncom.CoUninitialize()
        logger.info("hook_worker_main завершился")


class AppGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Монитор окна 'Найти'")
        self.root.geometry("350x150")

        self.status_label = tk.Label(self.root, text="Статус хуков: Выключены")
        self.status_label.pack(pady=10)

        self.text_label = tk.Label(self.root, text="Последний текст: —")
        self.text_label.pack(pady=10)

        self.start_button = tk.Button(self.root, text="Запустить хуки", command=self.start_hooks)
        self.start_button.pack(pady=5)

        self.stop_button = tk.Button(self.root, text="Остановить хуки", command=self.stop_hooks, state='disabled')
        self.stop_button.pack(pady=5)

        self.stop_event = None
        self.hook_thread = None

        self.target_edit_hwnd = None
        self.find_window_hwnd = None
        self.target_pid = None
        self.showing_duplicate_warning = False

        # Периодическая проверка очереди событий от хуков
        self.check_events()

        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def start_hooks(self):
        if self.hook_thread and self.hook_thread.is_alive():
            messagebox.showinfo("Информация", "Хуки уже запущены.")
            return

        def clear_queue(q):
            try:
                while True:
                    q.get_nowait()
            except queue.Empty:
                pass

        clear_queue(command_queue)

        self.stop_event = threading.Event()
        self.hook_thread = threading.Thread(target=hook_worker_main, args=(self.stop_event,), daemon=True)
        self.hook_thread.start()

        self.status_label.config(text="Статус хуков: Запущены")
        self.start_button.config(state='disabled')
        self.stop_button.config(state='normal')
        logger.info("Хуки запущены из GUI")

    def stop_hooks(self):
        if not self.hook_thread or not self.hook_thread.is_alive():
            messagebox.showinfo("Информация", "Хуки не запущены.")
            return

        if messagebox.askyesno("Подтверждение", "Остановить хуки?"):
            command_queue.put("STOP")
            self.stop_event.set()

            self.status_label.config(text="Статус хуков: Остановка...")
            self.start_button.config(state='normal')
            self.stop_button.config(state='disabled')
            logger.info("Хуки остановлены из GUI")

    def check_events(self):
        try:
            while True:
                evt = event_queue.get_nowait()

                if isinstance(evt, dict):
                    if 'target_edit_hwnd' in evt:
                        self.target_edit_hwnd = evt['target_edit_hwnd']
                    if 'find_window_hwnd' in evt:
                        self.find_window_hwnd = evt['find_window_hwnd']
                    if 'target_pid' in evt:
                        self.target_pid = evt['target_pid']
                    elif evt.get("warning") == "duplicate":
                        # Показать предупреждение с кнопкой
                        if not self.showing_duplicate_warning:
                            self.showing_duplicate_warning = True
                            self.show_duplicate_warning()
                    else:
                        self.text_label.config(text=f"Последний текст: {evt}")
                else:
                    self.text_label.config(text=f"Последний текст: {evt}")
        except queue.Empty:
            pass

        if self.hook_thread and not self.hook_thread.is_alive():
            self.status_label.config(text="Статус хуков: Остановлен")
            self.start_button.config(state='normal')
            self.stop_button.config(state='disabled')

        self.root.after(100, self.check_events)

    def show_duplicate_warning(self):
        # Показываем модальное предупреждение с кнопкой "Подтвердить ввод дубля"
        if not self.target_edit_hwnd:
            res = messagebox.askyesno("Обнаружен дубль", "Обнаружен дубль.\nПодтвердить ввод дубля?")
        else:
            win32gui.EnableWindow(self.target_edit_hwnd, False)
            self.root.update()
            res = messagebox.askyesno("Обнаружен дубль", "Обнаружен дубль.\nПодтвердить ввод дубля?")
            win32gui.EnableWindow(self.target_edit_hwnd, True)
            self.root.update()

        check_process = (self.target_edit_hwnd and is_window_valid(self.target_edit_hwnd) and
                self.find_window_hwnd and is_window_valid(self.find_window_hwnd) and
                self.target_pid and is_process_alive(self.target_pid))

        if res and check_process:
            # Отправляем команду для снимания блокировки
            command_queue.put("CONFIRM_DUPLICATE")

        self.showing_duplicate_warning = False

    def on_closing(self):
        if messagebox.askokcancel("Выход", "Закрыть программу?"):
            if self.hook_thread and self.hook_thread.is_alive():
                command_queue.put("STOP")
                self.stop_event.set()
                self.hook_thread.join(timeout=5)
            self.root.destroy()

def main():
    pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED)
    init_redis_keys()
    load_uia_module()

    root = tk.Tk()

    app = AppGUI(root)
    app.start_hooks()

    root.mainloop()

    # CoUninitialize после закрытия GUI (хуки уже остановлены в on_closing)
    pythoncom.CoUninitialize()
    logger.info("Программа завершена")

if __name__ == "__main__":
    try:
        main()
    finally:
        print("Вызов CoUninitialize из __main__ finally.")
        pythoncom.CoUninitialize()