"""
===============================================================================
Desktop Automation
===============================================================================

Framework unificado para automação de aplicações desktop no Windows,
integrando:

• Win32 API
• UI Automation (UIA / COM)
• pywinauto (backend "uia" e "win32")
• pyautogui (input global de teclado e mouse)
• psutil (gerenciamento de processos)

-------------------------------------------------------------------------------
Visão Geral
-------------------------------------------------------------------------------

Este framework fornece tanto utilitários de baixo nível quanto
abstrações orientadas a objeto para automação completa de aplicações
desktop Windows.
-------------------------------------------------------------------------------
Principais Recursos
-------------------------------------------------------------------------------

✔ Manipulação de janelas (handle, estilo, foco, maximização)
✔ Busca de janelas por estilo, ID ou critérios combinados
✔ Conexão automática a processos com retry e timeout
✔ Definição e gerenciamento de janela ativa
✔ Busca estruturada de elementos (title, auto_id, control_type)
✔ Escrita direta e escrita humanizada em campos
✔ Interação com dropdowns e listboxes
✔ Automação de teclado e mouse (local e global)
✔ Monitoramento automático de popups
✔ Cálculo geométrico proporcional de áreas de janela
✔ Inspeção de identificadores de controle
✔ Suporte a arquitetura RPA desktop

-------------------------------------------------------------------------------
Dependências
-------------------------------------------------------------------------------

Instale os pacotes necessários com:

    pip install pyautogui pywinauto comtypes pywin32 psutil

===============================================================================
"""

from __future__ import annotations

import ctypes
import os
import time
from collections.abc import Iterable
from dataclasses import dataclass
from threading import Thread
from typing import Any, Literal, TypedDict, cast

import psutil
import pyautogui
import win32api
import win32con
import win32gui
from comtypes.client import CreateObject, GetModule
from pywinauto.application import Application
from pywinauto.findwindows import ElementNotFoundError

from logger_config import logger

"""
===============================================================================
Classes de alto nível para automação de janelas usando pywinauto
===============================================================================
"""


@dataclass
class WindowElement:
    """Classe para armazenar informações de elementos da janela."""

    title: str | None = None
    auto_id: str | None = None
    control_type: str | None = None


class WindowAutomation:
    """Classe para automação de janelas do Windows usando pywinauto."""

    def __init__(
        self, process_name: str, exe_path: str, max_retries: int = 3, timeout: int = 15
    ):
        """
        Inicializa a automação para um processo específico.

        Args:
            process_name: Nome do processo a ser automatizado
            exe_path: Caminho completo para o executável
            max_retries: Número máximo de tentativas de conexão
            timeout: Tempo máximo de espera em segundos para cada tentativa
        """
        self.process_name = process_name
        self.exe_path = exe_path
        self.max_retries = max_retries
        self.timeout = timeout
        self.app = None
        self.current_window = None

    def connect_to_process(self) -> bool:
        """
        Tenta iniciar o processo e conectar a ele.

        Returns:
            bool: True se conectou com sucesso, False caso contrário
        """
        try:
            os.startfile(filepath=self.exe_path)
            logger.info(f"Iniciando aplicativo: {self.exe_path}")
            time.sleep(2)  # Aguarda a inicialização
        except Exception as e:
            logger.error(f"Erro ao iniciar o aplicativo: {e}")

        for attempt in range(self.max_retries):
            self._log_attempt(attempt)

            if self._try_connect_with_timeout():
                return True

            if not self._should_retry(attempt):
                break

            self._wait_before_retry()

        self._log_connection_failure()
        return False

    def _try_connect_with_timeout(self) -> bool:
        """
        Tenta conectar ao processo com timeout.

        Returns:
            bool: True se conectou com sucesso, False caso contrário
        """
        start_time = time.time()

        while self._is_within_timeout(start_time):
            if self._try_connect_to_available_process():
                return True
            time.sleep(0.5)

        return False

    def _try_connect_to_available_process(self) -> bool:
        """
        Tenta conectar a um processo disponível.

        Returns:
            bool: True se conectou com sucesso, False caso contrário
        """
        for proc in psutil.process_iter(["pid", "name"]):
            try:
                if self._is_target_process(proc):
                    return self._connect_to_pid(proc.info["pid"])
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                continue
        return False

    def _is_target_process(self, proc) -> bool:
        """Verifica se o processo é o alvo desejado."""
        return proc.info["name"].lower() == self.process_name.lower()

    def _connect_to_pid(self, pid: int) -> bool:
        """
        Tenta conectar a um PID específico.

        Args:
            pid: ID do processo

        Returns:
            bool: True se conectou com sucesso, False caso contrário
        """
        try:
            self.app = Application(backend="uia").connect(process=pid)
            logger.info(f"Conectado ao processo com PID: {pid}")
            return True
        except Exception as e:
            logger.error(f"Erro ao conectar ao processo: {e}")
            return False

    def set_active_window(
        self,
        title: str,
        class_name: str | None = None,
        control_type: str | None = None,
        auto_id: str | None = None,
        process: int | None = None,
        visible_only: bool = True,
        enabled_only: bool = True,
        debug: bool = False,
        max_retries: int = 3,
        retry_interval: int = 3,
    ) -> bool:
        """
        Define a janela ativa para interação, tentando até 3 vezes com intervalo entre as tentativas.

        Args:
            title (str): O título da janela a ser definida como ativa.
            class_name (str, optional): Nome da classe da janela.
            control_type (str, optional): Tipo de controle da janela.
            auto_id (str, optional): ID automático da janela.
            process (int, optional): ID do processo.
            visible_only (bool): Se True, considera apenas janelas visíveis.
            enabled_only (bool): Se True, considera apenas janelas habilitadas.
            debug (bool): Se True, imprime os identificadores de controle da janela.
            max_retries (int): Número máximo de tentativas para encontrar a janela.
            retry_interval (int): Intervalo entre as tentativas em segundos.

        Returns:
            bool: True se a janela foi definida como ativa com sucesso, False caso contrário.
        """
        if not self.app:
            logger.info("Nenhuma aplicação conectada.")
            return False

        criteria = {"title_re": title}
        optional_criteria = {
            "class_name": class_name,
            "control_type": control_type,
            "auto_id": auto_id,
            "process": process,
        }
        criteria.update({k: v for k, v in optional_criteria.items() if v is not None})
        for attempt in range(max_retries):
            try:
                windows = self.app.windows()
                window_filters = []
                if visible_only:
                    window_filters.append(lambda w: w.is_visible())
                if enabled_only:
                    window_filters.append(lambda w: w.is_enabled())
                if class_name:
                    window_filters.append(lambda w: w.class_name() == class_name)
                matching_windows = [
                    w
                    for w in windows
                    if title in w.window_text() and all(f(w) for f in window_filters)
                ]
                if matching_windows:
                    self.current_window = matching_windows[0]
                else:
                    self.current_window = self.app.window(**criteria)

                if hasattr(self.current_window, "wrapper_object"):
                    self.current_window.wrapper_object()

                self.current_window.set_focus()
                if debug:
                    self.current_window.logger.info_control_identifiers()
                logger.info(f"Janela '{title}' definida como ativa.")
                return True
            except Exception as e:
                logger.info(
                    f"Erro ao tentar definir a janela ativa ({attempt+1}/{max_retries}): {e}"
                )
                if attempt < max_retries - 1:
                    logger.info(
                        f"Aguardando {retry_interval} segundos antes da próxima tentativa..."
                    )
                    time.sleep(retry_interval)
                else:
                    logger.error(
                        f"Falha ao definir a janela '{title}' após {max_retries} tentativas."
                    )
        return False

    def find_element(self, element_info: WindowElement):
        """
        Encontra um elemento na janela atual.

        Args:
            element_info (WindowElement): Objeto com as informações do elemento.

        Returns:
            Elemento encontrado ou levanta um erro caso não seja possível encontrar.
        """
        if not self.current_window:
            raise ValueError("Nenhuma janela ativa definida")

        # Filtra somente os atributos que não são None
        kwargs = {
            "title": element_info.title,
            "auto_id": element_info.auto_id,
            "control_type": element_info.control_type,
        }

        kwargs = {key: value for key, value in kwargs.items() if value is not None}

        try:
            return self.current_window.child_window(**kwargs)
        except ElementNotFoundError as e:
            logger.error(f"Elemento não encontrado: {kwargs}")
            raise e

    def write_to_element(self, element, value: str) -> bool:
        """
        Escreve um valor em um elemento.
        """
        try:
            element.set_text(value)
            return True
        except Exception as e:
            logger.error(f"Erro ao escrever no elemento: {e}")
            return False

    def humanized_writing(self, element, value: str):
        element.set_focus()

        texto = value
        for char in texto:
            if char == " ":
                element.type_keys("{SPACE}")
            else:
                element.type_keys(char)

    def identifiers(self, title):
        """
        Define a janela ativa para interação.
        """
        if not self.app:
            return False

        try:
            self.current_window = self.app.window(title=title)
            self.current_window.wrapper_object()
            self.current_window.set_focus()
            self.current_window.print_control_identifiers()
        except Exception as e:
            raise Exception(f"Erro ao definir janela ativa: {e}") from e

    def _is_within_timeout(self, start_time: float) -> bool:
        """Verifica se ainda está dentro do tempo limite."""
        return time.time() - start_time <= self.timeout

    def _should_retry(self, attempt: int) -> bool:
        """Verifica se deve tentar novamente."""
        return attempt < self.max_retries - 1

    def _wait_before_retry(self):
        """Aguarda antes de tentar novamente."""
        logger.info("Aguardando 2 segundos antes da próxima tentativa...")
        time.sleep(2)

    def _log_attempt(self, attempt: int):
        """Registra tentativa de conexão."""
        logger.info(f"Tentativa {attempt + 1} de {self.max_retries}")

    def _log_connection_failure(self):
        """Registra falha na conexão."""
        logger.error(f"Processo não encontrado após {self.max_retries} tentativas")


class _EnumContext(TypedDict):
    target_id: int
    results: list[int]


@dataclass(slots=True)
class ControlInfo:
    handle: int
    auto_id: int
    text: str
    class_name: str
    rect: tuple[int, int, int, int]
    style: int
    ex_style: int
    visible: bool
    enabled: bool
    parent: int | None
    depth: int


"""
===============================================================================
Funções de baixo nível para manipulação de janelas e controles via Win32 API
===============================================================================
"""


def find_window(
    window_name: str,
    max_attempts: int = 3,
    wait_time: float = 2.0,
    raise_on_fail: bool = True,
    contains: bool = False,
) -> int | None:
    """
    Localiza uma janela pelo seu título.

    Args:
        window_name (str): Título da janela a ser localizada.
        max_attempts (int): Número máximo de tentativas.
        wait_time (float): Intervalo em segundos entre tentativas.
        raise_on_fail (bool): Se True, lança exceção ao não encontrar.
        partial_match (bool): Se True, busca por título parcial.

    Returns:
        Optional[int]: Handle da janela encontrada ou None.

    Raises:
        WindowNotFoundError: Caso não encontre e raise_on_fail=True.
    """

    def _find_partial(title: str) -> int | None:
        found_handle = None

        def callback(hwnd, _):
            nonlocal found_handle
            if win32gui.IsWindowVisible(hwnd):
                window_text = win32gui.GetWindowText(hwnd)
                if title.lower() in window_text.lower():
                    found_handle = hwnd

        win32gui.EnumWindows(callback, None)
        return found_handle

    for attempt in range(1, max_attempts + 1):
        if contains:
            handle = _find_partial(window_name)
        else:
            handle = win32gui.FindWindow(None, window_name)

        if handle:
            logger.info(
                f"Janela encontrada na tentativa {attempt}: Handle = {hex(handle)}"
            )
            return handle

        logger.info(
            f"Tentativa {attempt}/{max_attempts} falhou. " f"Aguardando {wait_time}s..."
        )
        time.sleep(wait_time)

    message = (
        f"Janela '{window_name}' não encontrada após " f"{max_attempts} tentativas."
    )

    if raise_on_fail:
        logger.error(message)
        raise RuntimeError(message)

    logger.warning(message)
    return None


def enum_child_windows_callback(hwnd: int, context: _EnumContext) -> bool:
    """
    Callback para enumeração de janelas filhas.

    Args:
        hwnd (int): Handle da janela filha atual.
        context (_EnumContext): Contexto contendo:
            - target_id (int): ID alvo
            - results (List[int]): lista acumuladora de handles

    Returns:
        bool: True para continuar enumeração.
    """
    try:
        control_id: int = win32gui.GetDlgCtrlID(hwnd)

        if control_id == context["target_id"]:
            logger.info(
                f"Handle encontrado para ID {context['target_id']}: {hex(hwnd)}"
            )
            context["results"].append(hwnd)

    except Exception as e:
        logger.error(f"Erro ao processar handle {hwnd}: {e}")

    return True


def get_handle_by_id(hwnd: int, target_id: int, instance: int = 0) -> int | None:
    """
    Obtém o handle de um controle especificado pelo ID dentro de uma janela
    e permite selecionar uma instância específica.

    Args:
        hwnd (int): Handle da janela pai.
        target_id (int): ID do controle desejado.
        instance (int): Índice da instância desejada (>= 0).

    Returns:
        Optional[int]: Handle encontrado ou None.
    """
    if instance < 0:
        raise ValueError("instance deve ser >= 0")

    results: list[int] = []

    context: _EnumContext = {"target_id": target_id, "results": results}

    win32gui.EnumChildWindows(hwnd, enum_child_windows_callback, context)

    if len(results) > instance:
        return results[instance]

    return None


def wait_for_control(
    hwnd: int,
    target_id: int,
    timeout: float = 15.0,
    poll_interval: float = 0.5,
) -> int:
    """
    Aguarda até que um controle com o `target_id` seja encontrado
    e esteja pronto para interação.

    Um controle é considerado "pronto" quando:
        - Está visível
        - Está habilitado
        - Possui dimensões válidas (> 0)

    Args:
        hwnd (int): Handle da janela pai.
        target_id (int): ID do controle desejado.
        timeout (float): Tempo máximo de espera (segundos).
        poll_interval (float): Intervalo entre verificações (segundos).

    Returns:
        int: Handle do controle pronto.

    Raises:
        TimeoutError: Se o controle não estiver pronto no tempo limite.
        ValueError: Se parâmetros inválidos forem fornecidos.
    """

    if timeout <= 0:
        raise ValueError("timeout deve ser > 0")

    if poll_interval <= 0:
        raise ValueError("poll_interval deve ser > 0")

    start_time: float = time.monotonic()
    deadline: float = start_time + timeout
    attempts: int = 0

    while time.monotonic() < deadline:
        attempts += 1

        handle: int | None = get_handle_by_id(hwnd, target_id)

        if handle is not None:
            try:
                if win32gui.IsWindowVisible(handle) and win32gui.IsWindowEnabled(
                    handle
                ):
                    left, top, right, bottom = win32gui.GetWindowRect(handle)
                    width: int = right - left
                    height: int = bottom - top

                    if width > 0 and height > 0:
                        logger.info(
                            f"Controle pronto após {attempts} tentativas "
                            f"({time.monotonic() - start_time:.2f}s)."
                        )
                        return handle

            except Exception as e:
                logger.debug(f"Falha ao validar controle {handle}: {e}")

        time.sleep(poll_interval)

    elapsed: float = time.monotonic() - start_time

    raise TimeoutError(
        f"Controle ID {target_id} não encontrado ou não pronto "
        f"após {attempts} tentativas em {elapsed:.2f}s "
        f"(timeout configurado: {timeout}s)."
    )


def is_valid_window(handle: int | None) -> bool:
    """
    Verifica se o handle fornecido corresponde a uma janela válida.

    Args:
        handle (Optional[int]): Handle da janela a ser validada.

    Returns:
        bool: True se o handle representa uma janela válida;
              False caso contrário.
    """
    if not isinstance(handle, int):
        return False

    if handle == 0:
        return False

    try:
        return bool(win32gui.IsWindow(handle))
    except Exception:
        return False


def get_handle_from_hex(hex_handle: str | int) -> int:
    """
    Converte uma representação hexadecimal ou decimal de handle
    para inteiro.

    Aceita:
        - int
        - str em formato hexadecimal (com ou sem '0x')
        - str decimal

    Args:
        hex_handle (Union[str, int]): Representação do handle.

    Returns:
        int: Handle convertido para inteiro.

    Raises:
        ValueError: Se o valor não puder ser convertido.
        TypeError: Se o tipo for inválido.
    """
    if isinstance(hex_handle, int):
        if hex_handle < 0:
            raise ValueError("Handle não pode ser negativo.")
        return hex_handle

    if isinstance(hex_handle, str):
        value = hex_handle.strip()

        if not value:
            raise ValueError("Handle vazio não é válido.")

        try:
            # Detecta automaticamente base 16 se tiver 0x
            if value.lower().startswith("0x"):
                result = int(value, 16)
            else:
                # Tenta decimal primeiro, se falhar tenta hexadecimal
                try:
                    result = int(value, 10)
                except ValueError:
                    result = int(value, 16)

            if result < 0:
                raise ValueError("Handle não pode ser negativo.")

            return result

        except ValueError as e:
            raise ValueError(f"Handle inválido: '{hex_handle}'. Erro: {e}") from e

    raise TypeError(
        f"Tipo inválido para handle: {type(hex_handle).__name__}. "
        "Esperado str ou int."
    )


def get_edit_text(hwnd_edit: int) -> str:
    """
    Recupera o texto de um controle EDIT pelo seu handle.

    Args:
        hwnd_edit (int): Handle do controle de edição.

    Returns:
        str: Texto contido no controle.

    Raises:
        ValueError: Se o handle for inválido.
        RuntimeError: Se ocorrer falha ao recuperar o texto.
    """
    if not isinstance(hwnd_edit, int) or hwnd_edit <= 0:
        raise ValueError("Handle inválido fornecido.")

    try:
        if not win32gui.IsWindow(hwnd_edit):
            raise ValueError("Handle não corresponde a uma janela válida.")

        # Obtém tamanho do texto
        length: int = int(
            win32gui.SendMessage(
                hwnd_edit,
                win32con.WM_GETTEXTLENGTH,
                0,
                0,
            )
        )

        if length <= 0:
            return ""

        # Cria buffer Unicode
        buffer = ctypes.create_unicode_buffer(length + 1)

        win32gui.SendMessage(
            hwnd_edit,
            win32con.WM_GETTEXT,
            length + 1,
            buffer,
        )

        return buffer.value

    except Exception as e:
        raise RuntimeError(f"Falha ao obter texto do controle {hwnd_edit}: {e}") from e


def click_button_by_handle(
    hwnd: int,
    offset_x: int = 0,
    offset_y: int = 0,
) -> None:
    """
    Simula um clique do mouse no centro do controle especificado pelo handle.

    Args:
        hwnd (int): Handle do controle.
        offset_x (int): Deslocamento horizontal do centro.
        offset_y (int): Deslocamento vertical do centro.

    Raises:
        ValueError: Se o handle for inválido.
        RuntimeError: Se o controle não estiver apto para interação.
    """
    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError(f"Handle {hwnd} inválido fornecido.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError(f"Handle {hwnd} não corresponde a uma janela válida.")

    if not win32gui.IsWindowVisible(hwnd):
        raise RuntimeError(f"Controle {hwnd} não está visível.")

    if not win32gui.IsWindowEnabled(hwnd):
        raise RuntimeError(f"Controle {hwnd} não está habilitado.")

    left, top, right, bottom = win32gui.GetWindowRect(hwnd)

    width: int = right - left
    height: int = bottom - top

    if width <= 0 or height <= 0:
        raise RuntimeError("Controle possui dimensões inválidas.")

    center_x: int = (left + width // 2) + offset_x
    center_y: int = (top + height // 2) + offset_y

    try:
        win32api.SetCursorPos((center_x, center_y))
        time.sleep(0.05)

        win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
        time.sleep(0.02)
        win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)

    except Exception as e:
        raise RuntimeError(f"Falha ao clicar no controle {hwnd}: {e}") from e


def click_button(
    hex_handle: str | int,
    offset_x: int = 0,
    offset_y: int = 0,
) -> None:
    """
    Converte um handle (hexadecimal ou inteiro) para int e
    simula um clique no controle correspondente.

    Args:
        hex_handle (Union[str, int]): Handle em formato hexadecimal ou inteiro.
        offset_x (int): Deslocamento horizontal do clique.
        offset_y (int): Deslocamento vertical do clique.

    Raises:
        ValueError: Se o handle for inválido.
        RuntimeError: Se falhar ao clicar no controle.
        TypeError: Se o tipo do handle for inválido.
    """
    handle: int = get_handle_from_hex(hex_handle)

    # A validação final é responsabilidade da função de baixo nível
    click_button_by_handle(handle, offset_x, offset_y)


def right_click(hex_handle: str | int) -> None:
    """
    Simula clique com botão direito via mensagem WM_RBUTTONDOWN/UP
    no centro do controle especificado.

    Args:
        hex_handle (Union[str, int]): Handle em formato hexadecimal ou inteiro.

    Raises:
        ValueError: Se o handle for inválido.
        RuntimeError: Se o controle não estiver apto.
        TypeError: Se o tipo for inválido.
    """
    handle: int = get_handle_from_hex(hex_handle)

    if not is_valid_window(handle):
        raise ValueError(f"Handle {handle} é inválido ou janela não existe.")

    if not win32gui.IsWindowEnabled(handle):
        raise RuntimeError(f"Controle {handle} não está habilitado.")

    if not win32gui.IsWindowVisible(handle):
        raise RuntimeError(f"Controle {handle} não está visível.")

    # Coordenadas relativas ao cliente
    left, top, right, bottom = win32gui.GetClientRect(handle)

    width: int = right - left
    height: int = bottom - top

    if width <= 0 or height <= 0:
        raise RuntimeError(f"Controle {handle} possui dimensões inválidas.")

    x: int = width // 2
    y: int = height // 2

    lparam: int = (y << 16) | x

    win32gui.SendMessage(
        handle,
        win32con.WM_RBUTTONDOWN,
        win32con.MK_RBUTTON,
        lparam,
    )

    time.sleep(0.02)

    win32gui.SendMessage(
        handle,
        win32con.WM_RBUTTONUP,
        0,
        lparam,
    )


def write_text(hex_handle: str | int, text: str) -> None:
    if not isinstance(text, str):
        text = str(text)

    handle: int = get_handle_from_hex(hex_handle)

    if not is_valid_window(handle):
        raise ValueError(f"Handle {hex_handle} inválido ou janela não existe.")

    # Cria buffer Unicode compatível com LPCWSTR
    buffer = ctypes.create_unicode_buffer(text)

    result = cast(
        int,
        win32gui.SendMessage(
            handle,
            win32con.WM_SETTEXT,
            0,
            buffer,
        ),
    )

    if result == 0:
        raise RuntimeError(f"Falha ao definir texto no controle {handle}.")


def set_text(hwnd: int, text: str) -> None:
    """
    Define texto em um controle EDIT e notifica corretamente o parent.

    Args:
        hwnd (int): Handle do controle.
        text (str): Texto a ser definido.

    Raises:
        ValueError: Se handle inválido.
        RuntimeError: Se falhar ao definir texto.
    """
    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError("Handle inválido.")

    if not isinstance(text, str):
        raise TypeError("text deve ser str.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError("Handle não corresponde a uma janela válida.")

    # Define texto via buffer Unicode
    buffer = ctypes.create_unicode_buffer(text)

    result = cast(
        int,
        win32gui.SendMessage(
            hwnd,
            win32con.WM_SETTEXT,
            0,
            buffer,
        ),
    )

    if result == 0:
        raise RuntimeError("Falha ao definir texto.")

    # Notificar parent corretamente
    parent = win32gui.GetParent(hwnd)

    if parent:
        control_id = win32gui.GetDlgCtrlID(hwnd)

        # HIWORD = notification code
        # LOWORD = control ID
        wparam = (win32con.EN_CHANGE << 16) | control_id

        win32gui.SendMessage(
            parent,
            win32con.WM_COMMAND,
            wparam,
            hwnd,
        )


def get_control_properties(
    parent_handle: int,
    *,
    recursive: bool = False,
    max_depth: int = 10,
    visible_only: bool = False,
    enabled_only: bool = False,
    class_filter: str | None = None,
    text_contains: str | None = None,
    fast_mode: bool = False,
) -> list[ControlInfo]:
    """
    Inspeciona controles filhos de uma janela.

    Args:
        parent_handle (int): Handle da janela pai.
        recursive (bool): Se True, percorre toda a árvore.
        max_depth (int): Profundidade máxima de recursão.
        visible_only (bool): Retorna apenas controles visíveis.
        enabled_only (bool): Retorna apenas controles habilitados.
        class_filter (Optional[str]): Filtra por nome da classe.
        text_contains (Optional[str]): Filtra por texto parcial.
        fast_mode (bool): Se True, não coleta texto (melhora performance).

    Returns:
        List[ControlInfo]
    """

    if not isinstance(parent_handle, int) or parent_handle <= 0:
        raise ValueError("parent_handle inválido.")

    if not win32gui.IsWindow(parent_handle):
        raise ValueError("parent_handle não é uma janela válida.")

    if max_depth < 0:
        raise ValueError("max_depth deve ser >= 0.")

    results: list[ControlInfo] = []

    # Pré-normalização segura para evitar chamadas repetidas de lower()
    normalized_class_filter: str | None = (
        class_filter.lower() if class_filter is not None else None
    )

    normalized_text_filter: str | None = (
        text_contains.lower() if text_contains is not None else None
    )

    def collect(hwnd: int, depth: int) -> None:
        try:
            if not win32gui.IsWindow(hwnd):
                return

            visible: bool = bool(win32gui.IsWindowVisible(hwnd))
            enabled: bool = bool(win32gui.IsWindowEnabled(hwnd))

            if visible_only and not visible:
                return

            if enabled_only and not enabled:
                return

            raw_class_name = win32gui.GetClassName(hwnd)

            if raw_class_name is None:
                return

            class_name: str = raw_class_name

            if normalized_class_filter is not None:
                if normalized_class_filter not in class_name.lower():
                    return

            text: str = ""
            if not fast_mode:
                text = win32gui.GetWindowText(hwnd)

                if normalized_text_filter is not None:
                    if normalized_text_filter not in text.lower():
                        return

            rect: tuple[int, int, int, int] = win32gui.GetWindowRect(hwnd)

            control = ControlInfo(
                handle=hwnd,
                auto_id=win32gui.GetDlgCtrlID(hwnd),
                text=text,
                class_name=class_name,
                rect=rect,
                style=win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE),
                ex_style=win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE),
                visible=visible,
                enabled=enabled,
                parent=win32gui.GetParent(hwnd),
                depth=depth,
            )

            results.append(control)

            if recursive and depth < max_depth:

                def child_callback(child_hwnd: int, _: int) -> bool:
                    collect(child_hwnd, depth + 1)
                    return True

                win32gui.EnumChildWindows(hwnd, child_callback, 0)

        except Exception:
            # Não interrompe inspeção inteira
            return

    def root_callback(child_hwnd: int, _: int) -> bool:
        collect(child_hwnd, 0)
        return True

    win32gui.EnumChildWindows(parent_handle, root_callback, 0)

    return results


def get_text_handle(hwnd: int) -> str:
    """
    Retorna o texto de uma janela pelo handle.

    Args:
        hwnd (int): Handle da janela.

    Returns:
        str: Texto da janela (string vazia se não houver).

    Raises:
        ValueError: Se o handle for inválido.
    """
    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError(f"Handle {hwnd} é inválido.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError(f"Handle {hwnd} não corresponde a uma janela válida.")

    text: str | None = win32gui.GetWindowText(hwnd)

    return text if text is not None else ""


def select_combo_item(combo_hwnd: int, index: int) -> None:
    """
    Seleciona item de um ComboBox pelo índice e envia notificação CBN_SELCHANGE.

    Args:
        combo_hwnd (int): Handle do ComboBox.
        index (int): Índice do item a ser selecionado.

    Raises:
        ValueError: Se handle inválido ou índice inválido.
        RuntimeError: Se falhar ao selecionar item.
    """
    if not isinstance(combo_hwnd, int) or combo_hwnd <= 0:
        raise ValueError("combo_hwnd inválido.")

    if not isinstance(index, int) or index < 0:
        raise ValueError("index deve ser inteiro >= 0.")

    if not win32gui.IsWindow(combo_hwnd):
        raise ValueError("Handle não corresponde a uma janela válida.")

    # Seleciona item
    result = cast(
        int,
        win32gui.SendMessage(
            combo_hwnd,
            win32con.CB_SETCURSEL,
            index,
            0,
        ),
    )

    if result == win32con.CB_ERR:
        raise RuntimeError(f"Falha ao selecionar índice {index} no ComboBox.")

    # Obtém parent
    parent_hwnd = win32gui.GetParent(combo_hwnd)

    if parent_hwnd is None or not win32gui.IsWindow(parent_hwnd):
        return  # Não envia notificação se não houver parent válido

    ctrl_id = win32gui.GetDlgCtrlID(combo_hwnd)

    # HIWORD = notification code
    # LOWORD = control id
    wparam = (win32con.CBN_SELCHANGE << 16) | ctrl_id

    win32gui.SendMessage(
        parent_hwnd,
        win32con.WM_COMMAND,
        wparam,
        combo_hwnd,
    )


def focus_window(hwnd: int, wait: float = 0.2) -> int:
    """
    Restaura e tenta colocar a janela em primeiro plano.

    Args:
        hwnd (int): Handle da janela.
        wait (float): Tempo de espera após foco (segundos).

    Returns:
        int: O handle da janela focada.

    Raises:
        ValueError: Se handle inválido.
        RuntimeError: Se falhar ao colocar em foreground.
    """
    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError(f"Handle {hwnd} é inválido.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError(f"Handle {hwnd} não corresponde a uma janela válida.")

    # Restaura se minimizada
    win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)

    # Tenta trazer para frente
    try:
        win32gui.SetForegroundWindow(hwnd)
    except Exception as e:
        raise RuntimeError(
            f"Falha ao definir janela {hwnd} como foreground: {e}"
        ) from e

    if wait > 0:
        time.sleep(wait)

    return hwnd


def kill_thread(thread: Thread) -> None:
    """
    Força encerramento de uma thread injetando SystemExit.

    Args:
        thread (Thread): Thread a ser finalizada.

    Raises:
        ValueError: Se thread inválida.
        RuntimeError: Se falhar ao injetar exceção.
    """
    if not isinstance(thread, Thread):
        raise ValueError("Objeto fornecido não é uma Thread.")

    if not thread.is_alive():
        return

    thread_id = thread.ident

    if thread_id is None:
        raise RuntimeError("Thread não possui identificador válido.")

    result = ctypes.pythonapi.PyThreadState_SetAsyncExc(
        ctypes.c_ulong(thread_id),
        ctypes.py_object(SystemExit),
    )

    if result == 0:
        raise RuntimeError("Thread não encontrada.")

    if result > 1:
        # rollback
        ctypes.pythonapi.PyThreadState_SetAsyncExc(
            ctypes.c_ulong(thread_id),
            None,
        )
        raise RuntimeError("Falha ao injetar exceção na thread.")


def select_listbox_item(
    hwnd: int,
    text_to_find: str,
    *,
    ignore_spaces: bool = False,
    contains: bool = False,
) -> bool:
    """
    Seleciona item de uma ListBox pelo texto.

    Args:
        hwnd (int): Handle da ListBox.
        text_to_find (str): Texto a ser buscado.
        ignore_spaces (bool): Remove todos os espaços antes da comparação.
        contains (bool): Se True, verifica se texto está contido no item.

    Returns:
        bool: True se item foi encontrado e selecionado.
    """

    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError("Handle inválido.")

    if not isinstance(text_to_find, str):
        raise TypeError("text_to_find deve ser str.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError("Handle não corresponde a uma janela válida.")

    # Número total de itens
    count = cast(
        int,
        win32gui.SendMessage(hwnd, win32con.LB_GETCOUNT, 0, 0),
    )

    if count <= 0:
        return False

    # Normalização do texto buscado
    target = text_to_find

    if ignore_spaces:
        target = target.replace(" ", "")

    target = target.lower()

    for index in range(count):
        # Obtém tamanho do texto
        length = cast(
            int,
            win32gui.SendMessage(hwnd, win32con.LB_GETTEXTLEN, index, 0),
        )

        if length <= 0:
            continue

        buffer = ctypes.create_unicode_buffer(length + 1)

        win32gui.SendMessage(
            hwnd,
            win32con.LB_GETTEXT,
            index,
            buffer,
        )

        item_text = buffer.value

        compare_text = item_text

        if ignore_spaces:
            compare_text = compare_text.replace(" ", "")

        compare_text = compare_text.lower()

        match = target in compare_text if contains else target == compare_text

        if match:
            result = cast(
                int,
                win32gui.SendMessage(
                    hwnd,
                    win32con.LB_SETCURSEL,
                    index,
                    0,
                ),
            )

            return result != win32con.LB_ERR

    return False


def monitor_popup(
    titulo_popup: str,
    texto_botao: str,
    *,
    timeout: float = 30.0,
    poll_interval: float = 0.5,
    contains_title: bool = True,
) -> bool:
    """
    Monitora popups e clica automaticamente em um botão quando encontrado.

    Args:
        titulo_popup (str): Título (ou parte do título) da janela.
        texto_botao (str): Texto do botão a ser clicado.
        timeout (float): Tempo máximo de monitoramento.
        poll_interval (float): Intervalo entre verificações.
        contains_title (bool): Se True, busca título parcial.

    Returns:
        bool: True se popup foi encontrado e fechado, False se timeout.
    """

    if not isinstance(titulo_popup, str) or not titulo_popup:
        raise ValueError("titulo_popup inválido.")

    if not isinstance(texto_botao, str) or not texto_botao:
        raise ValueError("texto_botao inválido.")

    start = time.monotonic()
    normalized_title = titulo_popup.lower()
    normalized_button = texto_botao.lower()

    def find_popup() -> int | None:
        found: int | None = None

        def enum_windows(hwnd: int, _: int) -> bool:
            nonlocal found

            if not win32gui.IsWindowVisible(hwnd):
                return True

            title = win32gui.GetWindowText(hwnd)
            if title is None:
                return True

            if contains_title:
                if normalized_title in title.lower():
                    found = hwnd
                    return False
            else:
                if title == titulo_popup:
                    found = hwnd
                    return False

            return True

        win32gui.EnumWindows(enum_windows, 0)
        return found

    while time.monotonic() - start < timeout:
        hwnd = find_popup()

        if hwnd is not None and win32gui.IsWindow(hwnd):

            clicked = False

            def enum_child(hwnd_child: int, _: int) -> bool:
                nonlocal clicked

                text = win32gui.GetWindowText(hwnd_child)
                if text is None:
                    return True

                if normalized_button in text.lower():
                    win32gui.PostMessage(
                        hwnd_child,
                        win32con.BM_CLICK,
                        0,
                        0,
                    )

                    logger.info(
                        f"Popup fechado automaticamente clicando em '{texto_botao}'"
                    )

                    clicked = True
                    return False

                return True

            win32gui.EnumChildWindows(hwnd, enum_child, 0)

            if clicked:
                return True

        time.sleep(poll_interval)

    return False


# Inicialização única (evita custo repetido)
_UIA_MODULE = GetModule("UIAutomationCore.dll")
_UIA = CreateObject(
    "{ff48dba4-60ef-4201-aa87-54103eef594e}",
    interface=_UIA_MODULE.IUIAutomation,
)


def interact_with_tab(
    tab_name: str,
    *,
    timeout: float = 5.0,
) -> bool:
    """
    Seleciona uma aba (TabItem) pelo nome usando UI Automation.

    Args:
        tab_name (str): Nome da aba.
        timeout (float): Tempo máximo para localizar a aba.

    Returns:
        bool: True se selecionado com sucesso.
    """

    if not isinstance(tab_name, str) or not tab_name:
        raise ValueError("tab_name inválido.")

    start = time.monotonic()

    root = _UIA.GetRootElement()

    name_condition = _UIA.CreatePropertyCondition(
        _UIA_MODULE.UIA_NamePropertyId,
        tab_name,
    )

    control_type_condition = _UIA.CreatePropertyCondition(
        _UIA_MODULE.UIA_ControlTypePropertyId,
        _UIA_MODULE.UIA_TabItemControlTypeId,
    )

    combined_condition = _UIA.CreateAndCondition(
        name_condition,
        control_type_condition,
    )

    while time.monotonic() - start < timeout:

        element = root.FindFirst(
            _UIA_MODULE.TreeScope_Descendants,
            combined_condition,
        )

        if element is not None:
            try:
                pattern = element.GetCurrentPattern(
                    _UIA_MODULE.UIA_SelectionItemPatternId
                )

                if pattern is None:
                    return False

                selection = pattern.QueryInterface(
                    _UIA_MODULE.IUIAutomationSelectionItemPattern
                )

                selection.Select()
                return True

            except Exception as e:
                raise RuntimeError(f"Falha ao selecionar aba '{tab_name}': {e}") from e

        time.sleep(0.2)

    return False


def select_validate_item(
    app: Any,
    hwnd: int,
    control_id: int,
    option: str,
    max_options: int,
    *,
    max_tries: int = 3,
    enter_on_each_down: bool = False,
    remove_spaces: bool = False,
    type_first_char: bool = False,
    substring: bool = False,
    inspect: bool = False,
) -> None:
    """
    Localiza e valida uma opção em um dropdown, navegando pelos itens disponíveis
    e realizando múltiplas tentativas caso necessário.

    A função abre o dropdown, percorre até `max_options` itens utilizando navegação
    por teclado (seta para baixo) e compara o valor atualmente selecionado com a
    opção desejada. Caso não encontre na primeira tentativa, reinicia a navegação
    e repete o processo até atingir `max_tries`.

    Args:
        app (Any): Objeto da aplicação que contém a janela ativa e permite envio de teclas.
        hwnd (int): Handle da janela que contém o dropdown.
        control_id (int): ID do controle associado ao dropdown.
        option (str): Texto da opção que deve ser selecionada.
        max_options (int): Número máximo de itens a percorrer em cada tentativa.
        max_tries (int, opcional): Número máximo de tentativas completas. Padrão: 3.
        enter_on_each_down (bool, opcional): Se True, envia ENTER após cada navegação
            para confirmar a seleção intermediária. Padrão: False.
        remove_spaces (bool, opcional): Se True, remove todos os espaços antes da
            comparação entre o texto atual e a opção desejada. Padrão: False.
        type_first_char (bool, opcional): Se True, digita o primeiro caractere da
            opção antes de iniciar a navegação para acelerar a busca. Padrão: False.
        substring (bool, opcional): Se True, considera correspondência parcial
            (opção contida no texto atual). Caso contrário, exige correspondência
            exata. Padrão: False.
        inspect (bool, opcional): Se True, registra logs detalhados das tentativas
            e das opções visitadas. Padrão: False.

    Raises:
        ValueError: Se parâmetros obrigatórios forem inválidos.
        RuntimeError: Se a opção não for encontrada após todas as tentativas.
    """

    if not isinstance(option, str) or not option:
        raise ValueError("option deve ser string não vazia.")

    if max_options <= 0:
        raise ValueError("max_options deve ser > 0.")

    if max_tries <= 0:
        raise ValueError("max_tries deve ser > 0.")

    def normalize(value: str) -> str:
        value = value.replace(" ", "") if remove_spaces else value
        return value.upper()

    target = normalize(option)

    for attempt in range(max_tries):

        btn = wait_for_control(hwnd, control_id)
        click_button(btn)
        time.sleep(0.2)

        if type_first_char and option:
            app.current_window.type_keys(option[0])

        visited: list[str] = []

        for _ in range(max_options):

            current_text = get_edit_text(btn)
            normalized_current = normalize(current_text)

            match = (
                target in normalized_current
                if substring
                else target == normalized_current
            )

            if match:
                logger.info(f"Opção '{option}' encontrada com sucesso.")
                return

            visited.append(current_text)

            time.sleep(0.1)
            app.current_window.type_keys("{DOWN}")

            if enter_on_each_down:
                app.current_window.type_keys("{ENTER}")
                time.sleep(0.5)

        if inspect:
            logger.info(
                f"Tentativa {attempt + 1}: "
                f"Item '{option}' não encontrado. "
                f"Opções visitadas: {set(visited)}"
            )

        app.current_window.type_keys("{HOME}")

    raise RuntimeError(f"Opção '{option}' não encontrada após {max_tries} tentativas.")


def send_key(
    key: str | Iterable[str],
    *,
    repeat: int = 1,
    interval: float = 0.0,
    hotkey: bool = False,
    post_delay: float = 0.0,
) -> None:
    """
    Envia tecla(s) globalmente, sem depender de handle de janela.

    Args:
        key (str | Iterable[str]): Tecla única ("enter") ou sequência
            ("ctrl", "c") se hotkey=True.
        repeat (int, opcional): Número de repetições. Padrão: 1.
        interval (float, opcional): Intervalo entre repetições (segundos).
        hotkey (bool, opcional): Se True, trata `key` como combinação
            (ex: ["ctrl", "c"]). Padrão: False.
        post_delay (float, opcional): Tempo de espera após execução.

    Raises:
        ValueError: Se parâmetros inválidos forem fornecidos.
    """

    if repeat <= 0:
        raise ValueError("repeat deve ser > 0.")

    if interval < 0:
        raise ValueError("interval não pode ser negativo.")

    if post_delay < 0:
        raise ValueError("post_delay não pode ser negativo.")

    if hotkey:
        if not isinstance(key, Iterable):
            raise ValueError("Para hotkey=True, key deve ser iterável.")

        keys = list(key)

        if not keys:
            raise ValueError("Lista de teclas não pode ser vazia.")

        for _ in range(repeat):
            pyautogui.hotkey(*keys)
            if interval > 0:
                time.sleep(interval)

    else:
        if not isinstance(key, str):
            raise ValueError("Para hotkey=False, key deve ser str.")

        for _ in range(repeat):
            pyautogui.press(key)
            if interval > 0:
                time.sleep(interval)

    if post_delay > 0:
        time.sleep(post_delay)


def click_center_screen(
    offset_x: int = 0,
    offset_y: int = 0,
    *,
    clicks: int = 1,
    interval: float = 0.0,
    button: Literal["left", "right", "middle"] = "left",
    move_duration: float = 0.0,
    inspect: bool = False,
) -> None:
    """
    Realiza clique no centro da tela com deslocamento opcional.

    Args:
        offset_x (int): Deslocamento horizontal em pixels.
        offset_y (int): Deslocamento vertical em pixels.
        clicks (int, opcional): Número de cliques. Padrão: 1.
        interval (float, opcional): Intervalo entre cliques.
        button ("left" | "right" | "middle"): Botão do mouse.
        move_duration (float, opcional): Duração do movimento até o ponto.
        inspect (bool, opcional): Se True, registra log da ação.

    Raises:
        ValueError: Se parâmetros inválidos forem fornecidos.
    """

    if clicks <= 0:
        raise ValueError("clicks deve ser > 0.")

    if interval < 0:
        raise ValueError("interval não pode ser negativo.")

    if move_duration < 0:
        raise ValueError("move_duration não pode ser negativo.")

    user32 = ctypes.windll.user32
    screen_width: int = user32.GetSystemMetrics(0)
    screen_height: int = user32.GetSystemMetrics(1)

    center_x: int = (screen_width // 2) + offset_x
    center_y: int = (screen_height // 2) + offset_y

    pyautogui.moveTo(center_x, center_y, duration=move_duration)

    pyautogui.click(
        x=center_x,
        y=center_y,
        clicks=clicks,
        interval=interval,
        button=button,
    )

    if inspect:
        logger.info(
            f"Click no centro da tela em ({center_x}, {center_y}) "
            f"com botão '{button}', {clicks}x."
        )


def send_holding_key(
    hold_key: str | Iterable[str],
    key: str,
    *,
    repeat_key: int = 1,
    interval: float = 0.0,
    post_delay: float = 1.0,
) -> None:
    """
    Mantém uma ou mais teclas pressionadas enquanto envia outra tecla
    repetidamente, liberando todas as teclas de forma segura ao final.

    A função pressiona a(s) tecla(s) especificada(s) em `hold_key`,
    executa a tecla principal (`key`) o número de vezes definido em
    `repeat_key`, respeitando o intervalo opcional entre envios, e
    garante a liberação das teclas mantidas mesmo em caso de erro
    durante a execução.

    Args:
        hold_key (str | Iterable[str]): Tecla ou sequência de teclas
            a serem mantidas pressionadas (ex: "ctrl" ou ["ctrl", "shift"]).
        key (str): Tecla que será enviada enquanto as teclas de hold
            estiverem pressionadas.
        repeat_key (int, opcional): Número de vezes que a tecla principal
            será enviada. Padrão: 1.
        interval (float, opcional): Intervalo em segundos entre cada envio
            da tecla principal. Padrão: 0.0.
        post_delay (float, opcional): Tempo de espera em segundos após a
            liberação das teclas. Padrão: 1.0.

    Raises:
        ValueError: Se `hold_key` estiver vazio.
    """

    if isinstance(hold_key, str):
        hold_keys = [hold_key]
    else:
        hold_keys = list(hold_key)

    if not hold_keys:
        raise ValueError("hold_key não pode ser vazio.")

    # Pressiona
    for hk in hold_keys:
        pyautogui.keyDown(hk)

    try:
        for _ in range(repeat_key):
            pyautogui.press(key)
            if interval > 0:
                time.sleep(interval)
    finally:
        # Libera
        for hk in reversed(hold_keys):
            pyautogui.keyUp(hk)

    if post_delay > 0:
        time.sleep(post_delay)


def max_window(hwnd: int) -> None:
    """
    Maximiza uma janela pelo seu handle.

    Args:
        hwnd (int): Handle da janela a ser maximizada.

    Raises:
        ValueError: Se o handle for inválido.
    """

    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError("Handle inválido.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError("Handle não corresponde a uma janela válida.")

    win32gui.ShowWindow(hwnd, win32con.SW_MAXIMIZE)


def get_rect_handle(hwnd: int) -> tuple[int, int, int, int]:
    """
    Retorna a tupla (left, top, right, bottom) da janela.

    Args:
        hwnd (int): Handle da janela.

    Returns:
        Tuple[int, int, int, int]: Coordenadas da janela.

    Raises:
        ValueError: Se o handle for inválido.
    """

    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError("Handle inválido.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError("Handle não corresponde a uma janela válida.")

    return win32gui.GetWindowRect(hwnd)


def inspect_pos_inside_window(
    window: int,
    offset_x: int = 0,
    offset_y: int = 0,
) -> list[tuple[int, int]]:
    """
    Calcula posições centrais verticais (fatias) dentro da área de uma janela,
    aplicando deslocamento horizontal e vertical opcionais.

    Args:
        window (int): Handle da janela.
        offset_x (int, opcional): Deslocamento horizontal relativo ao centro.
        offset_y (int, opcional): Deslocamento vertical adicional aplicado
            a cada ponto calculado.

    Returns:
        List[Tuple[int, int]]: Lista de coordenadas (x, y) inteiras.

    Raises:
        ValueError: Se parâmetros inválidos ou dimensões inconsistentes.
    """

    if not isinstance(window, int) or window <= 0:
        raise ValueError("window inválido.")

    left, top, right, bottom = get_rect_handle(window)

    if right <= left or bottom <= top:
        raise ValueError("Dimensões da janela inválidas.")

    # Garante inteiro
    height_per_element = _calculate_slice_height(rect=(left, top, right, bottom))

    if height_per_element <= 0:
        raise ValueError("Altura por elemento inválida.")

    height_per_element = int(height_per_element)

    largura = right - left
    altura_total = bottom - top

    center_x = left + (largura // 2) + offset_x

    total_slices = altura_total // height_per_element

    slices: list[tuple[int, int]] = []

    for i in range(total_slices):
        center_y = top + (height_per_element // 2) + (i * height_per_element) + offset_y

        slices.append((center_x, center_y))

    # Ignora última fatia (ex: área de scroll)
    if slices:
        slices.pop()

    return slices


def _calculate_slice_height(
    rect: tuple[int, int, int, int],
    reference_slice_height: int = 20,
    reference_rect_height: int = 200,
) -> int:
    """
    Calcula a altura proporcional de uma fatia com base na altura
    de um retângulo de referência.

    A função ajusta a altura da fatia proporcionalmente de acordo
    com a altura do retângulo informado.

    Args:
        rect (Tuple[int, int, int, int]):
            Retângulo no formato (left, top, right, bottom).
        reference_slice_height (int, opcional):
            Altura da fatia utilizada no retângulo de referência.
            Padrão: 20.
        reference_rect_height (int, opcional):
            Altura do retângulo de referência utilizada para o cálculo
            proporcional. Padrão: 200.

    Returns:
        int: Altura proporcional da fatia em pixels.

    Raises:
        ValueError: Se as dimensões do retângulo forem inválidas.
    """

    left, top, right, bottom = rect  # type: ignore

    total_height = bottom - top

    proportional_height = (
        total_height * reference_slice_height
    ) / reference_rect_height

    return int(round(proportional_height))


def inspect_window_controls(
    hwnd: int,
    *,
    backend: Literal["uia", "win32"] = "uia",
    inspect: bool = True,
) -> None:
    """
    Conecta-se a uma janela existente pelo handle e imprime
    os identificadores de seus controles.

    Args:
        hwnd (int): Handle da janela.
        backend ("uia" | "win32", opcional):
            Backend do pywinauto. Padrão: "uia".
        inspect (bool, opcional):
            Se True, imprime os identificadores no console.

    Raises:
        ValueError: Se o handle for inválido.
        RuntimeError: Se não for possível conectar à janela.
    """

    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError("Handle inválido.")

    try:
        app = Application(backend=backend).connect(handle=hwnd)
        window = app.window(handle=hwnd)

        if inspect:
            window.print_control_identifiers()

    except Exception as e:
        raise RuntimeError(f"Falha ao inspecionar janela {hwnd}: {e}") from e


def move_to_handle(
    hwnd: int,
    *,
    click: bool = True,
    button: Literal["left", "right", "middle"] = "left",
    move_duration: float = 0.0,
) -> None:
    """
    Move o cursor para o centro da janela identificada pelo handle.
    Opcionalmente realiza clique.

    Args:
        hwnd (int): Handle da janela.
        click (bool, opcional): Se True, realiza clique após mover.
        button ("left" | "right" | "middle", opcional):
            Botão do mouse utilizado no clique.
        move_duration (float, opcional):
            Duração do movimento do cursor até o destino.
        inspect (bool, opcional):
            Se True, registra log da operação.

    Raises:
        ValueError: Se o handle for inválido.
    """

    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError(f"Handle {hwnd} é inválido.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError(f"Handle {hwnd} não corresponde a uma janela válida.")

    left, top, right, bottom = win32gui.GetWindowRect(hwnd)

    center_x = (left + right) // 2
    center_y = (top + bottom) // 2

    pyautogui.moveTo(center_x, center_y, duration=move_duration)

    if click:
        pyautogui.click(x=center_x, y=center_y, button=button)


def clear_input(
    *,
    post_delay: float = 0.0,
) -> None:
    """
    Limpa completamente o conteúdo do campo de texto ativo
    utilizando o atalho Ctrl + A seguido de Backspace.

    Args:
        post_delay (float, opcional):Tempo de espera após a limpeza.
    """

    # Seleciona tudo
    send_holding_key(
        hold_key="ctrl",
        key="a",
        repeat_key=1,
    )

    # Apaga conteúdo
    send_key("backspace")

    if post_delay > 0:
        time.sleep(post_delay)


def get_window_style(
    hwnd: int,
    *,
    extended: bool = False,
) -> int:
    """
    Retorna o estilo da janela (GWL_STYLE ou GWL_EXSTYLE).

    Args:
        hwnd (int): Handle da janela.
        extended (bool, opcional):
            Se True, retorna o estilo estendido (GWL_EXSTYLE).
            Caso contrário, retorna o estilo padrão (GWL_STYLE).

    Returns:
        int: Valor inteiro representando o estilo da janela.

    Raises:
        ValueError: Se o handle for inválido.
    """

    if not isinstance(hwnd, int) or hwnd <= 0:
        raise ValueError("Handle inválido.")

    if not win32gui.IsWindow(hwnd):
        raise ValueError("Handle não corresponde a uma janela válida.")

    index = win32con.GWL_EXSTYLE if extended else win32con.GWL_STYLE

    return win32gui.GetWindowLong(hwnd, index)


def find_window_by_style(
    target_style: int,
    *,
    parent_hwnd: int | None = None,
    style_mask: int = 0xFFFFFFFF,
    max_attempts: int = 3,
    wait_time: float = 1.0,
    extended: bool = False,
    visible_only: bool = True,
    inspect: bool = False,
) -> int:
    """
    Procura uma janela cujo estilo corresponda ao valor desejado,
    considerando uma máscara opcional.

    A busca pode ser limitada às janelas filhas de `parent_hwnd`.

    Args:
        target_style (int): Valor de estilo desejado.
        parent_hwnd (int | None, opcional):
            Handle da janela pai para limitar a busca.
        style_mask (int, opcional):
            Máscara aplicada na comparação do estilo.
        max_attempts (int, opcional):
            Número máximo de tentativas.
        wait_time (float, opcional):
            Intervalo entre tentativas (segundos).
        extended (bool, opcional):
            Se True, utiliza estilo estendido (GWL_EXSTYLE).
        visible_only (bool, opcional):
            Se True, considera apenas janelas visíveis.
        inspect (bool, opcional):
            Se True, registra logs detalhados.

    Returns:
        int: Handle da primeira janela encontrada.

    Raises:
        ValueError: Se parâmetros forem inválidos.
        RuntimeError: Se nenhuma janela for encontrada.
    """

    if max_attempts <= 0:
        raise ValueError("max_attempts deve ser > 0.")

    if wait_time < 0:
        raise ValueError("wait_time não pode ser negativo.")

    if parent_hwnd is not None:
        if not isinstance(parent_hwnd, int) or not win32gui.IsWindow(parent_hwnd):
            raise ValueError("parent_hwnd inválido.")

    def enum_handler(hwnd: int, results: list[int]) -> bool:
        if visible_only and not win32gui.IsWindowVisible(hwnd):
            return True

        style = get_window_style(hwnd, extended=extended)

        if (style & style_mask) == (target_style & style_mask):
            results.append(hwnd)

        return True  # necessário para continuar enumeração

    for attempt in range(max_attempts):

        handles_found: list[int] = []

        if parent_hwnd is not None:
            win32gui.EnumChildWindows(parent_hwnd, enum_handler, handles_found)
        else:
            win32gui.EnumWindows(enum_handler, handles_found)

        if handles_found:
            found_hwnd = handles_found[0]

            if inspect:
                logger.info(
                    f"Janela encontrada na tentativa {attempt + 1}: "
                    f"Handle={hex(found_hwnd)}, "
                    f"Style=0x{get_window_style(found_hwnd, extended=extended):08X}"
                )

            return found_hwnd

        if inspect:
            logger.info(
                f"Tentativa {attempt + 1} falhou. "
                f"Aguardando {wait_time} segundos..."
            )

        time.sleep(wait_time)

    search_area = (
        f"dentro da janela pai {hex(parent_hwnd)}"
        if parent_hwnd is not None
        else "em todas as janelas"
    )

    raise RuntimeError(
        f"Nenhuma janela com estilo 0x{target_style:08X} "
        f"encontrada {search_area} após {max_attempts} tentativas."
    )
