"""
environment_mapper.py
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Cobre as camadas:
  ✅ Camada 1 — Apps instalados (registro do Windows + pastas)
  ✅ Camada 2 — Processos em execução (tempo real)
  ✅ Camada 3 — Árvore completa de janelas + elementos filhos
  ✅ Camada 5 — Resolução, DPI, múltiplos monitores, normalização
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""

import os
import time
import json
import platform
import winreg
import ctypes
import psutil
import uiautomation as auto
from pathlib import Path
from typing import Optional
from loguru import logger


# ══════════════════════════════════════════════════════
# CAMADA 5 — RESOLUÇÃO, DPI E MÚLTIPLOS MONITORES
# ══════════════════════════════════════════════════════

def mapear_monitores() -> dict:
    """
    Mapeia todos os monitores conectados, resolução, DPI e escala.
    Normaliza coordenadas considerando DPI awareness do Windows.
    """
    try:
        user32   = ctypes.windll.user32
        shcore   = ctypes.windll.shcore

        # Ativar DPI awareness para leituras precisas
        try:
            ctypes.windll.shcore.SetProcessDpiAwareness(2)
        except:
            ctypes.windll.user32.SetProcessDPIAware()

        # Monitor principal
        largura_raw  = user32.GetSystemMetrics(0)
        altura_raw   = user32.GetSystemMetrics(1)
        dpi_sistema  = user32.GetDpiForSystem()

        # Fator de escala (ex: 125% = 1.25)
        fator_escala = dpi_sistema / 96.0

        # Coordenadas virtuais reais (sem escala)
        largura_real = int(largura_raw / fator_escala)
        altura_real  = int(altura_raw  / fator_escala)

        # Múltiplos monitores via EnumDisplayMonitors
        monitores = []

        def enum_monitors_callback(hMonitor, hdcMonitor, lprcMonitor, dwData):
            info = ctypes.create_string_buffer(40)
            ctypes.c_uint32.from_buffer(info, 0).value = 40
            try:
                ctypes.windll.user32.GetMonitorInfoW(hMonitor, info)
                # Coordenadas do monitor
                rc = lprcMonitor.contents
                dpi_x = ctypes.c_uint()
                dpi_y = ctypes.c_uint()
                shcore.GetDpiForMonitor(hMonitor, 0,
                                        ctypes.byref(dpi_x),
                                        ctypes.byref(dpi_y))
                monitores.append({
                    "indice":   len(monitores) + 1,
                    "x":        rc.left,
                    "y":        rc.top,
                    "largura":  rc.right  - rc.left,
                    "altura":   rc.bottom - rc.top,
                    "dpi_x":    dpi_x.value,
                    "dpi_y":    dpi_y.value,
                    "escala":   round(dpi_x.value / 96.0, 2)
                })
            except:
                pass
            return True

        MONITORENUMPROC = ctypes.WINFUNCTYPE(
            ctypes.c_bool,
            ctypes.c_ulong, ctypes.c_ulong,
            ctypes.POINTER(ctypes.c_long * 4),
            ctypes.c_double
        )

        try:
            ctypes.windll.user32.EnumDisplayMonitors(
                None, None, MONITORENUMPROC(enum_monitors_callback), 0
            )
        except:
            # Fallback se EnumDisplayMonitors falhar
            monitores = [{
                "indice":  1,
                "x":       0, "y": 0,
                "largura": largura_raw,
                "altura":  altura_raw,
                "dpi_x":   dpi_sistema,
                "dpi_y":   dpi_sistema,
                "escala":  round(fator_escala, 2)
            }]

        resultado = {
            "monitor_principal": {
                "largura_raw":  largura_raw,
                "altura_raw":   altura_raw,
                "largura_real": largura_real,
                "altura_real":  altura_real,
                "dpi":          dpi_sistema,
                "fator_escala": round(fator_escala, 2),
                "zoom_percent": f"{int(fator_escala * 100)}%"
            },
            "total_monitores": len(monitores) if monitores else 1,
            "monitores":        monitores
        }

        logger.debug(f"Tela: {largura_raw}x{altura_raw} | DPI: {dpi_sistema} | Escala: {fator_escala:.0%} | Monitores: {len(monitores)}")
        return resultado

    except Exception as e:
        logger.error(f"Erro ao mapear monitores: {e}")
        return {"erro": str(e)}


def normalizar_coordenadas(x: int, y: int, monitor_idx: int = 1) -> dict:
    """
    Converte coordenadas considerando escala de DPI.
    Essencial para clicar no lugar certo em telas 4K ou com zoom.
    """
    try:
        monitores = mapear_monitores()
        escala = 1.0

        if monitor_idx <= len(monitores.get("monitores", [])):
            escala = monitores["monitores"][monitor_idx - 1].get("escala", 1.0)

        return {
            "x_original":    x,
            "y_original":    y,
            "x_normalizado": int(x * escala),
            "y_normalizado": int(y * escala),
            "escala_usada":  escala
        }
    except Exception as e:
        return {"x_normalizado": x, "y_normalizado": y, "erro": str(e)}


# ══════════════════════════════════════════════════════
# CAMADA 1 — APLICATIVOS INSTALADOS
# ══════════════════════════════════════════════════════

def mapear_apps_instalados() -> list:
    """
    Escaneia 3 fontes para garantir cobertura máxima:
      1. Registro HKLM (64-bit)
      2. Registro HKLM (32-bit via WOW6432Node)
      3. Registro HKCU (apps do usuário atual)
      4. Pastas comuns de instalação
    """
    apps = {}  # dict para deduplicar por nome

    chaves_registro = [
        (winreg.HKEY_LOCAL_MACHINE,
         r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
        (winreg.HKEY_LOCAL_MACHINE,
         r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
        (winreg.HKEY_CURRENT_USER,
         r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
    ]

    for hive, chave_path in chaves_registro:
        try:
            chave = winreg.OpenKey(hive, chave_path)
            total = winreg.QueryInfoKey(chave)[0]

            for i in range(total):
                try:
                    sub_nome = winreg.EnumKey(chave, i)
                    sub      = winreg.OpenKey(chave, sub_nome)

                    try:
                        nome, _ = winreg.QueryValueEx(sub, "DisplayName")
                    except:
                        continue  # sem nome → ignorar

                    # Campos opcionais
                    def ler(campo):
                        try:
                            v, _ = winreg.QueryValueEx(sub, campo)
                            return v
                        except:
                            return ""

                    exe      = ler("InstallLocation")
                    versao   = ler("DisplayVersion")
                    editor   = ler("Publisher")
                    exe_path = ler("DisplayIcon")

                    if nome not in apps:
                        apps[nome] = {
                            "nome":    nome,
                            "versao":  versao,
                            "editor":  editor,
                            "path":    exe or "",
                            "exe":     exe_path or "",
                        }
                except:
                    continue
        except:
            continue

    # Pastas comuns de instalação (busca por .exe)
    pastas_comuns = [
        Path(os.environ.get("PROGRAMFILES",       r"C:\Program Files")),
        Path(os.environ.get("PROGRAMFILES(X86)",  r"C:\Program Files (x86)")),
        Path(os.environ.get("LOCALAPPDATA",       "")) / "Programs",
        Path(os.environ.get("APPDATA",            "")) / "Programs",
    ]

    for pasta in pastas_comuns:
        if not pasta.exists():
            continue
        try:
            for exe in pasta.rglob("*.exe"):
                nome = exe.stem
                if nome not in apps:
                    apps[nome] = {
                        "nome":   nome,
                        "versao": "",
                        "editor": "",
                        "path":   str(exe.parent),
                        "exe":    str(exe),
                    }
        except:
            continue

    resultado = list(apps.values())
    logger.info(f"Apps instalados mapeados: {len(resultado)}")
    return resultado


def encontrar_executavel_app(nome_app: str) -> Optional[str]:
    """
    Dado um nome de app em linguagem natural (ex: 'Bloco de Notas'),
    retorna o caminho do executável.
    """
    apps = mapear_apps_instalados()
    nome_lower = nome_app.lower()

    # Busca por correspondência parcial
    for app in apps:
        if nome_lower in app["nome"].lower():
            if app.get("exe"):
                return app["exe"]
            if app.get("path"):
                # Buscar .exe dentro da pasta
                pasta = Path(app["path"])
                exes  = list(pasta.glob("*.exe"))
                if exes:
                    return str(exes[0])

    # Apelidos comuns
    apelidos = {
        "bloco de notas": "notepad.exe",
        "notepad":        "notepad.exe",
        "calculadora":    "calc.exe",
        "paint":          "mspaint.exe",
        "explorer":       "explorer.exe",
        "word":           "winword.exe",
        "excel":          "excel.exe",
        "powerpoint":     "powerpnt.exe",
        "chrome":         r"C:\Program Files\Google\Chrome\Application\chrome.exe",
        "firefox":        r"C:\Program Files\Mozilla Firefox\firefox.exe",
        "edge":           "msedge.exe",
        "vscode":         r"C:\Users\{user}\AppData\Local\Programs\Microsoft VS Code\Code.exe",
        "vs code":        r"C:\Users\{user}\AppData\Local\Programs\Microsoft VS Code\Code.exe",
        "discord":        r"C:\Users\{user}\AppData\Local\Discord\Update.exe",
    }

    usuario = os.environ.get("USERNAME", "")
    for apelido, exe in apelidos.items():
        if apelido in nome_lower:
            return exe.replace("{user}", usuario)

    return None


# ══════════════════════════════════════════════════════
# CAMADA 2 — PROCESSOS EM EXECUÇÃO (TEMPO REAL)
# ══════════════════════════════════════════════════════

def mapear_processos() -> list:
    """
    Lista todos os processos em execução com detalhes completos.
    Inclui qual processo está em foco no momento.
    """
    processos = []

    # Descobrir PID da janela em foco
    pid_em_foco = 0
    try:
        hwnd = ctypes.windll.user32.GetForegroundWindow()
        pid_foco = ctypes.c_ulong()
        ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid_foco))
        pid_em_foco = pid_foco.value
    except:
        pass

    for proc in psutil.process_iter(
        ["pid", "name", "exe", "status", "cpu_percent",
         "memory_info", "create_time", "username"]
    ):
        try:
            mem = proc.info["memory_info"]
            processos.append({
                "pid":       proc.info["pid"],
                "nome":      proc.info["name"],
                "exe":       proc.info["exe"] or "",
                "status":    proc.info["status"],
                "cpu":       round(proc.info["cpu_percent"], 1),
                "memoria_mb": round(mem.rss / 1024 / 1024, 1) if mem else 0,
                "usuario":   proc.info["username"] or "",
                "em_foco":   proc.info["pid"] == pid_em_foco,
                "iniciado":  time.strftime(
                    "%H:%M:%S",
                    time.localtime(proc.info["create_time"])
                ) if proc.info["create_time"] else ""
            })
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue

    logger.debug(f"Processos mapeados: {len(processos)}")
    return processos


def app_esta_aberto(nome_app: str) -> Optional[dict]:
    """
    Verifica se um app está em execução.
    Retorna o processo se encontrado, None caso contrário.
    """
    nome_lower = nome_app.lower().replace(".exe", "")
    for proc in psutil.process_iter(["pid", "name", "exe"]):
        try:
            if nome_lower in proc.info["name"].lower():
                return {
                    "pid":  proc.info["pid"],
                    "nome": proc.info["name"],
                    "exe":  proc.info["exe"] or ""
                }
        except:
            continue
    return None


# ══════════════════════════════════════════════════════
# CAMADA 3 — ÁRVORE COMPLETA DE JANELAS + ELEMENTOS FILHOS
# ══════════════════════════════════════════════════════

def mapear_elementos_janela(janela_control, profundidade: int = 0,
                             max_profundidade: int = 4) -> list:
    """
    Mapeia recursivamente TODOS os elementos filhos de uma janela.
    Retorna nome, tipo, estado, posição de cada elemento interativo.
    """
    if profundidade > max_profundidade:
        return []

    elementos = []

    try:
        filhos = janela_control.GetChildren()
    except:
        return []

    for filho in filhos:
        try:
            rect   = filho.BoundingRectangle
            estado = {
                "visivel":    filho.IsOffscreen is False,
                "habilitado": filho.IsEnabled,
                "focado":     filho.HasKeyboardFocus,
            }

            elemento = {
                "nome":        filho.Name or "",
                "tipo":        filho.ControlTypeName,
                "classe":      filho.ClassName or "",
                "automation_id": getattr(filho, "AutomationId", "") or "",
                "estado":      estado,
                "posicao": {
                    "x":       rect.left,
                    "y":       rect.top,
                    "largura": rect.width(),
                    "altura":  rect.height(),
                    "centro_x": rect.left + rect.width()  // 2,
                    "centro_y": rect.top  + rect.height() // 2,
                },
                "valor": "",
                "filhos": []
            }

            # Tentar ler valor do elemento (campos de texto, etc.)
            try:
                padrao_valor = filho.GetValuePattern()
                if padrao_valor:
                    elemento["valor"] = padrao_valor.Value or ""
            except:
                pass

            # Recursão para filhos
            if profundidade < max_profundidade:
                elemento["filhos"] = mapear_elementos_janela(
                    filho,
                    profundidade + 1,
                    max_profundidade
                )

            elementos.append(elemento)

        except Exception:
            continue

    return elementos


def mapear_janelas_ativas(com_elementos_filhos: bool = True,
                           max_profundidade: int = 3) -> list:
    """
    Lista todas as janelas abertas.
    Se com_elementos_filhos=True, mapeia a árvore completa de cada janela.
    """
    janelas = []

    try:
        root = auto.GetRootControl()

        # Janela em foco
        nome_foco = ""
        try:
            foco = auto.GetFocusedControl()
            if foco:
                nome_foco = foco.Name or ""
        except:
            pass

        for janela in root.GetChildren():
            try:
                if not janela.Name and not janela.ClassName:
                    continue

                rect = janela.BoundingRectangle

                info = {
                    "nome":        janela.Name or "",
                    "tipo":        janela.ControlTypeName,
                    "classe":      janela.ClassName or "",
                    "pid":         janela.ProcessId,
                    "em_foco":     janela.Name == nome_foco,
                    "posicao": {
                        "x":       rect.left,
                        "y":       rect.top,
                        "largura": rect.width(),
                        "altura":  rect.height()
                    },
                    "elementos_filhos": []
                }

                # Mapear árvore de elementos filhos
                if com_elementos_filhos and janela.Name:
                    info["elementos_filhos"] = mapear_elementos_janela(
                        janela,
                        profundidade=0,
                        max_profundidade=max_profundidade
                    )

                janelas.append(info)

            except Exception:
                continue

    except Exception as e:
        logger.error(f"Erro ao mapear janelas: {e}")

    logger.info(f"Janelas ativas mapeadas: {len(janelas)}")
    return janelas


def encontrar_elemento_na_janela(janela_nome: str,
                                  elemento_nome: str,
                                  tipo: Optional[str] = None) -> Optional[dict]:
    """
    Busca um elemento específico pelo nome dentro de uma janela.
    Retorna posição e estado do elemento encontrado.
    """
    try:
        janela = auto.WindowControl(Name=janela_nome, searchDepth=1)
        if not janela.Exists(maxSearchSeconds=3):
            return None

        # Busca pelo nome e tipo
        if tipo == "button":
            el = janela.ButtonControl(Name=elemento_nome)
        elif tipo == "edit":
            el = janela.EditControl(Name=elemento_nome)
        elif tipo == "menu":
            el = janela.MenuControl(Name=elemento_nome)
        elif tipo == "list":
            el = janela.ListControl(Name=elemento_nome)
        elif tipo == "checkbox":
            el = janela.CheckBoxControl(Name=elemento_nome)
        elif tipo == "combobox":
            el = janela.ComboBoxControl(Name=elemento_nome)
        else:
            el = janela.Control(Name=elemento_nome)

        if not el.Exists(maxSearchSeconds=2):
            return None

        rect = el.BoundingRectangle
        return {
            "nome":        el.Name,
            "tipo":        el.ControlTypeName,
            "habilitado":  el.IsEnabled,
            "posicao": {
                "x":       rect.left,
                "y":       rect.top,
                "largura": rect.width(),
                "altura":  rect.height(),
                "centro_x": rect.left + rect.width()  // 2,
                "centro_y": rect.top  + rect.height() // 2,
            }
        }
    except Exception as e:
        logger.warning(f"Elemento '{elemento_nome}' não encontrado em '{janela_nome}': {e}")
        return None


# ══════════════════════════════════════════════════════
# MAPEAMENTO COMPLETO — TODAS AS CAMADAS
# ══════════════════════════════════════════════════════

def mapear_ambiente_completo(com_elementos_filhos: bool = True) -> dict:
    """
    Monta o mapa 360° do computador do usuário.
    Cobre as camadas 1, 2, 3 e 5 completamente.
    """
    logger.info("━━━ Iniciando mapeamento completo do ambiente ━━━")
    inicio = time.time()

    ambiente = {
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        "sistema": {
            "os":          platform.system(),
            "versao":      platform.version(),
            "arquitetura": platform.architecture()[0],
            "hostname":    platform.node(),
            "usuario":     os.environ.get("USERNAME", ""),
            "python":      platform.python_version(),
        },

        # Camada 5 — tela e monitores
        "tela": mapear_monitores(),

        # Camada 1 — apps instalados
        "apps_instalados": mapear_apps_instalados(),

        # Camada 2 — processos em execução
        "processos": mapear_processos(),

        # Camada 3 — janelas com árvore de elementos
        "janelas_abertas": mapear_janelas_ativas(
            com_elementos_filhos=com_elementos_filhos
        ),
    }

    duracao = round(time.time() - inicio, 2)
    logger.success(
        f"✅ Mapeamento concluído em {duracao}s — "
        f"{len(ambiente['apps_instalados'])} apps | "
        f"{len(ambiente['processos'])} processos | "
        f"{len(ambiente['janelas_abertas'])} janelas"
    )

    return ambiente


# Teste direto
if __name__ == "__main__":
    resultado = mapear_ambiente_completo(com_elementos_filhos=False)
    print(json.dumps({
        "apps":      len(resultado["apps_instalados"]),
        "processos": len(resultado["processos"]),
        "janelas":   len(resultado["janelas_abertas"]),
        "tela":      resultado["tela"]["monitor_principal"]
    }, indent=2, ensure_ascii=False))
