import json
import sys
import time
import traceback
from pathlib import Path

repo = Path.home() / 'TibiaPilotNG'
sys.path.insert(0, str(repo))

from src.utils.dpi import enablePerMonitorAwareness

enablePerMonitorAwareness()

import win32api
import win32con
import win32gui
import win32process

from src.utils.desktopBridge import invoke_background
from src.utils.display import getSelectedDisplay

(repo / '.play-tibia-error.txt').unlink(missing_ok=True)


def report_error(error_type, error, trace):
    (repo / '.play-tibia-error.txt').write_text(
        ''.join(traceback.format_exception(error_type, error, trace)), encoding='utf-8'
    )


sys.excepthook = report_error


def bot_monitor():
    display = getSelectedDisplay()
    return (
        display['x'], display['y'],
        display['x'] + display['width'],
        display['y'] + display['height'],
    )


def _is_tibia_process(hwnd):
    # Browser-Tabs o.ae. koennen ebenfalls "Tibia" im Titel tragen -
    # nur Fenster des echten Clients zaehlen.
    try:
        pid = win32process.GetWindowThreadProcessId(hwnd)[1]
        process = win32api.OpenProcess(
            win32con.PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
        try:
            image = win32process.GetModuleFileNameEx(process, 0).lower()
        finally:
            win32api.CloseHandle(process)
    except (OSError, win32api.error):
        # Nur "Prozess weg / kein Zugriff" abfangen. Ein zu breites
        # except verschluckt Programmierfehler und verwirft dann
        # stillschweigend jedes Fenster.
        return False
    return image.endswith(('\\client.exe', '\\tibia.exe'))


def tibia_windows():
    windows = []

    def collect(hwnd, _):
        if not win32gui.IsWindowVisible(hwnd):
            return
        title = win32gui.GetWindowText(hwnd)
        if title == 'Tibia' or title.startswith('Tibia - '):
            if _is_tibia_process(hwnd):
                windows.append((hwnd, title, win32gui.GetWindowRect(hwnd)))

    win32gui.EnumWindows(collect, None)
    return windows


def restore_foreground(hwnd):
    if not hwnd or not win32gui.IsWindow(hwnd):
        return
    try:
        win32gui.SetForegroundWindow(hwnd)
    except Exception:
        pass


def place_on_bot_monitor(hwnd, bounds):
    if not hwnd or not win32gui.IsWindow(hwnd):
        return False
    left, top, right, bottom = bounds
    try:
        win32gui.ShowWindow(hwnd, win32con.SW_SHOWNOACTIVATE)
        # HWND_BOTTOM places the client behind the virtual desktop itself.
        # Keep it visible on the isolated bot screen without stealing focus.
        win32gui.SetWindowPos(
            hwnd, win32con.HWND_TOP, left, top, right - left, bottom - top,
            win32con.SWP_SHOWWINDOW | win32con.SWP_NOACTIVATE,
        )
        return True
    except win32gui.error:
        return False


bounds = bot_monitor()
previous_foreground = win32gui.GetForegroundWindow()
launcher = None
# Der Launcher braucht je nach Update-Check 8-40 Sekunden.
deadline = time.time() + 60
while time.time() < deadline:
    candidates = [window for window in tibia_windows() if window[1] == 'Tibia']
    if candidates:
        launcher = max(
            candidates,
            key=lambda window: (window[2][2] - window[2][0]) *
            (window[2][3] - window[2][1]),
        )
        break
    time.sleep(1)
if launcher is None:
    raise RuntimeError('Das Tibia-Launcher-Fenster ist nicht erschienen.')
launcher_seen_at = time.time()
launcher_pid = win32process.GetWindowThreadProcessId(launcher[0])[1]

PLAY_BUTTON = ('QApplication.tibia::launcher::TLauncherMainWindow'
               '.QWidget.QWidget.QGroupBox.QPushButton')


def press_play():
    return invoke_background(launcher_pid, PLAY_BUTTON)


# Der Launcher braucht nach dem Erscheinen einen Moment, bis der
# Play-Knopf wirklich reagiert. Ohne diese Pause geht der erste Klick
# gelegentlich ins Leere und der Client startet nie.
time.sleep(2)
if not press_play():
    raise RuntimeError('Tibia Play button could not be invoked in background.')

game = None
# Tibia schliesst zuerst den Launcher und startet den Client danach.
# Grosszuegiges Fenster: ein Client-Update kann Minuten dauern. Bleibt
# der Launcher stehen, wird Play noch einmal gedrueckt (erster Klick
# verpufft, wenn der Launcher noch initialisiert).
candidate_id = None
candidate_since = None
retried = False
deadline = time.time() + 180
while time.time() < deadline:
    current_games = [
        window for window in tibia_windows()
        if win32process.GetWindowThreadProcessId(window[0])[1] != launcher_pid
    ]
    candidate = max(
        current_games,
        key=lambda window: (window[2][2] - window[2][0]) *
        (window[2][3] - window[2][1]),
        default=None,
    )
    if candidate is None:
        candidate_id = None
        candidate_since = None
    elif candidate_id != (candidate[0], win32process.GetWindowThreadProcessId(candidate[0])[1]):
        candidate_id = (candidate[0], win32process.GetWindowThreadProcessId(candidate[0])[1])
        candidate_since = time.time()
    elif time.time() - candidate_since >= 3:
        game = candidate
        break
    if (not retried and candidate is None
            and time.time() - launcher_seen_at >= 20
            and any(window for window in tibia_windows()
                    if win32process.GetWindowThreadProcessId(
                        window[0])[1] == launcher_pid)):
        # Launcher steht nach 20 s immer noch da -> Play erneut druecken.
        retried = True
        press_play()
    restore_foreground(previous_foreground)
    time.sleep(0.05)

if game is None:
    raise RuntimeError(
        'Der Tibia-Client ist nach dem Play-Klick nicht erschienen '
        '(3 Minuten gewartet). Laeuft im Launcher noch ein Update?')

place_on_bot_monitor(game[0], bounds)
settle_deadline = time.time() + 7
while time.time() < settle_deadline:
    # The client can replace its first HWND while it initializes.
    # Re-resolving by client PID prevents a stale HWND from failing the launch.
    game_pid = win32process.GetWindowThreadProcessId(game[0])[1]
    current_games = [
        window for window in tibia_windows()
        if win32process.GetWindowThreadProcessId(window[0])[1] == game_pid
    ]
    if current_games:
        game = max(
            current_games,
            key=lambda window: (window[2][2] - window[2][0]) *
            (window[2][3] - window[2][1]),
        )
        place_on_bot_monitor(game[0], bounds)
    restore_foreground(previous_foreground)
    time.sleep(0.05)

game_pid = win32process.GetWindowThreadProcessId(game[0])[1]
current_games = [
    window for window in tibia_windows()
    if win32process.GetWindowThreadProcessId(window[0])[1] == game_pid
]
if not current_games:
    raise RuntimeError('Tibia game window closed while it was starting.')
game = max(
    current_games,
    key=lambda window: (window[2][2] - window[2][0]) *
    (window[2][3] - window[2][1]),
)
if not place_on_bot_monitor(game[0], bounds):
    raise RuntimeError('Tibia game window was no longer ready.')

(repo / '.play-tibia.json').write_text(json.dumps({
    'launcher': launcher[1],
    'game': game[1],
    'gameHwnd': int(game[0]),
    'gameRect': game[2],
    'monitor': bounds,
}), encoding='utf-8')
