#!/usr/bin/env python3
"""
Bigg Adventure / Heroes Journey EQEmu Server - Client Patcher & Launcher
RoF2 Client Patcher (Port 5999) with PyQt6 GUI & Editable Host Selection
"""

import os
import sys
import json
import hashlib
import urllib.request
import subprocess
import threading
import socket
import time


def retire_client_files(names, client_dir, log):
    """Move files the server has retired (manifest "retire") into <client>/_retired/<date>/. Never deletes."""
    import datetime, shutil
    stamp = datetime.date.today().isoformat()
    moved = 0
    for rel in names or []:
        if os.path.isabs(rel) or ".." in rel.replace("\\", "/").split("/"):
            continue
        src = os.path.join(client_dir, rel)
        if not os.path.lexists(src):
            continue
        dst_dir = os.path.join(client_dir, "_retired", stamp, os.path.dirname(rel))
        os.makedirs(dst_dir, exist_ok=True)
        dst = os.path.join(dst_dir, os.path.basename(rel))
        while os.path.lexists(dst):
            dst += ".old"
        shutil.move(src, dst)
        log(f"[✓] Retired {rel} -> _retired/{stamp}/")
        moved += 1
    return moved

# Server Presets for RoF2 (Port 5999 for LoginServer)
CANDIDATE_SERVERS = [
    {
        "name": "biggs-adventure.duckdns.org:5999 (Bigg Adventure Direct DuckDNS)",
        "manifest": "http://192.168.4.78:8080/patch_manifest.json",
        "base_url": "http://192.168.4.78:8080/patch_files/",
        "host": "biggs-adventure.duckdns.org:5999"
    },
    {
        "name": "192.168.4.78:5999 (Bigg Adventure LAN Host)",
        "manifest": "http://192.168.4.78:8080/patch_manifest.json",
        "base_url": "http://192.168.4.78:8080/patch_files/",
        "host": "192.168.4.78:5999"
    },
    {
        "name": "login.eqemulator.net:5999 (Official EQEmu LoginServer)",
        "manifest": "http://192.168.4.78:8080/patch_manifest.json",
        "base_url": "http://192.168.4.78:8080/patch_files/",
        "host": "login.eqemulator.net:5999"
    },
]

DEFAULT_EQHOST = "Host=biggs-adventure.duckdns.org:5999"

def get_sha256(filepath):
    if not os.path.exists(filepath):
        return None
    sha256 = hashlib.sha256()
    try:
        with open(filepath, 'rb') as f:
            while chunk := f.read(65536):
                sha256.update(chunk)
        return sha256.hexdigest()
    except Exception:
        return None

def update_eqhost_file(host_string, client_dir=None):
    if client_dir is None:
        client_dir = os.getcwd()
    eqhost_path = os.path.join(client_dir, "eqhost.txt")
    clean_host = host_string.strip()
    if clean_host.startswith("Host="):
        host_line = clean_host
    else:
        host_line = f"Host={clean_host}"
    content = f"[LoginServer]\n{host_line}\n"
    try:
        with open(eqhost_path, "w") as f:
            f.write(content)
        return True, f"Configured eqhost.txt -> {host_line}"
    except Exception as e:
        return False, f"Could not write eqhost.txt: {e}"

def read_current_eqhost(client_dir=None):
    if client_dir is None:
        client_dir = os.getcwd()
    eqhost_path = os.path.join(client_dir, "eqhost.txt")
    if os.path.exists(eqhost_path):
        try:
            with open(eqhost_path, "r") as f:
                for line in f:
                    line = line.strip()
                    if line.startswith("Host="):
                        return line.split("=", 1)[1].strip()
        except Exception:
            pass
    return "login.eqemulator.net:5999"

def launch_eqgame(client_dir=None, launch_mq=True):
    if client_dir is None:
        client_dir = os.getcwd()
    eqgame_exe = os.path.join(client_dir, "eqgame.exe")
    if not os.path.exists(eqgame_exe):
        return False, f"eqgame.exe not found in {client_dir}"

    env = os.environ.copy()
    if "WINEPREFIX" not in env:
        env["WINEPREFIX"] = "/home/tj/.wine"

    mq_started = False
    if launch_mq:
        for candidate in ["MacroQuest.exe", "MacroQuest2.exe", "../E3_MQ2_RoF2/MacroQuest2.exe"]:
            mq_path = os.path.normpath(os.path.join(client_dir, candidate))
            if os.path.exists(mq_path):
                mq_dir = os.path.dirname(mq_path)
                try:
                    if sys.platform.startswith("win"):
                        subprocess.Popen([mq_path], cwd=mq_dir)
                    else:
                        subprocess.Popen(["wine", mq_path], cwd=mq_dir, env=env)
                    mq_started = True
                    time.sleep(1.5)
                except Exception:
                    pass
                break

    if sys.platform.startswith("win"):
        cmd = [eqgame_exe, "patchme"]
    else:
        cmd = ["wine", eqgame_exe, "patchme"]

    try:
        subprocess.Popen(cmd, cwd=client_dir, env=env)
        msg = f"Launched EverQuest ({' '.join(cmd)})"
        if mq_started:
            msg += " + MacroQuest"
        return True, msg
    except Exception as e:
        return False, f"Launch failed: {e}"

# ----------------- PyQt6 Implementation -----------------
def run_pyqt6_gui():
    from PyQt6.QtWidgets import (
        QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
        QLabel, QPushButton, QTextEdit, QProgressBar, QComboBox, QLineEdit, QMessageBox, QCheckBox
    )
    from PyQt6.QtCore import Qt, pyqtSignal, QObject
    from PyQt6.QtGui import QColor, QPalette

    class WorkerSignals(QObject):
        log = pyqtSignal(str)
        status = pyqtSignal(str)
        progress = pyqtSignal(int)
        finished = pyqtSignal()

    class PatcherWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("Bigg Adventure - Heroes Journey Patcher (RoF2 • Port 5999)")
            self.resize(760, 540)
            self.setMinimumSize(700, 500)
            self.signals = WorkerSignals()
            self.signals.log.connect(self.append_log)
            self.signals.status.connect(self.set_status)
            self.signals.progress.connect(self.set_progress)
            self.signals.finished.connect(self.on_patch_finished)

            self.client_dir = os.path.dirname(os.path.abspath(__file__))
            self.init_ui()

        def init_ui(self):
            palette = QPalette()
            palette.setColor(QPalette.ColorRole.Window, QColor("#121212"))
            palette.setColor(QPalette.ColorRole.WindowText, QColor("#E0E0E0"))
            palette.setColor(QPalette.ColorRole.Base, QColor("#1E1E1E"))
            palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#2A2A2A"))
            palette.setColor(QPalette.ColorRole.Text, QColor("#D4D4D4"))
            palette.setColor(QPalette.ColorRole.Button, QColor("#222222"))
            palette.setColor(QPalette.ColorRole.ButtonText, QColor("#E0E0E0"))
            self.setPalette(palette)

            central = QWidget()
            self.setCentralWidget(central)
            layout = QVBoxLayout(central)
            layout.setContentsMargins(20, 15, 20, 15)
            layout.setSpacing(10)

            # Header
            header_layout = QVBoxLayout()
            title_lbl = QLabel("HEROES JOURNEY • EVERQUEST RoF2")
            title_lbl.setStyleSheet("color: #00E5FF; font-size: 20px; font-weight: bold;")
            subtitle_lbl = QLabel("Dragons of Norrath / DoDH Era • 3-Class Multiclass Server (Login Port 5999)")
            subtitle_lbl.setStyleSheet("color: #FFB300; font-size: 13px; font-style: italic;")
            header_layout.addWidget(title_lbl)
            header_layout.addWidget(subtitle_lbl)
            layout.addLayout(header_layout)

            # Server Presets Bar
            preset_layout = QHBoxLayout()
            preset_lbl = QLabel("Preset Server:")
            preset_lbl.setStyleSheet("color: #AAAAAA; font-size: 12px; min-width: 90px;")
            self.server_combo = QComboBox()
            self.server_combo.setStyleSheet("background-color: #222222; color: #FFFFFF; padding: 4px; border: 1px solid #444444; border-radius: 4px;")
            for s in CANDIDATE_SERVERS:
                self.server_combo.addItem(s["name"], s)
            self.server_combo.currentIndexChanged.connect(self.on_preset_selected)
            preset_layout.addWidget(preset_lbl)
            preset_layout.addWidget(self.server_combo, 1)
            layout.addLayout(preset_layout)

            # Custom Host Edit Bar
            host_layout = QHBoxLayout()
            host_lbl = QLabel("Active eqhost:")
            host_lbl.setStyleSheet("color: #AAAAAA; font-size: 12px; min-width: 90px;")
            self.host_input = QLineEdit()
            current_h = read_current_eqhost(self.client_dir)
            self.host_input.setText(current_h)
            self.host_input.setStyleSheet("background-color: #1E1E1E; color: #00E5FF; padding: 5px; border: 1px solid #444444; border-radius: 4px; font-family: monospace; font-weight: bold;")
            
            self.apply_host_btn = QPushButton("Save Host")
            self.apply_host_btn.setStyleSheet("background-color: #37474F; color: white; padding: 5px 12px; border-radius: 4px;")
            self.apply_host_btn.clicked.connect(self.save_custom_host)

            host_layout.addWidget(host_lbl)
            host_layout.addWidget(self.host_input, 1)
            host_layout.addWidget(self.apply_host_btn)
            layout.addLayout(host_layout)

            # Log / News text area
            self.log_text = QTextEdit()
            self.log_text.setReadOnly(True)
            self.log_text.setStyleSheet(
                "background-color: #1A1A1A; color: #D4D4D4; border: 1px solid #333333; border-radius: 4px; "
                "font-family: Consolas, monospace; font-size: 12px; padding: 8px;"
            )
            layout.addWidget(self.log_text, 1)

            # Status label & progress bar
            self.status_lbl = QLabel("Ready to connect and play.")
            self.status_lbl.setStyleSheet("color: #B0BEC5; font-size: 12px;")
            layout.addWidget(self.status_lbl)

            self.progress_bar = QProgressBar()
            self.progress_bar.setRange(0, 100)
            self.progress_bar.setValue(0)
            self.progress_bar.setStyleSheet(
                "QProgressBar { background-color: #222222; border: 1px solid #333333; border-radius: 4px; height: 16px; text-align: center; color: #FFFFFF; font-size: 11px; } "
                "QProgressBar::chunk { background-color: #00E5FF; border-radius: 3px; }"
            )
            layout.addWidget(self.progress_bar)

            # Buttons
            btn_layout = QHBoxLayout()
            self.check_btn = QPushButton("CHECK FOR UPDATES")
            self.check_btn.setStyleSheet(
                "QPushButton { background-color: #1976D2; color: #FFFFFF; font-weight: bold; font-size: 13px; padding: 10px 20px; border-radius: 4px; border: none; } "
                "QPushButton:hover { background-color: #2196F3; } "
                "QPushButton:disabled { background-color: #444444; color: #888888; }"
            )
            self.check_btn.clicked.connect(self.start_patch_thread)

            self.mq_checkbox = QCheckBox("Auto-Launch MacroQuest (Next / ImGui)")
            self.mq_checkbox.setChecked(True)
            self.mq_checkbox.setStyleSheet("color: #76FF03; font-weight: bold; font-size: 12px; margin-left: 15px;")

            self.play_btn = QPushButton("PLAY HEROES JOURNEY")
            self.play_btn.setStyleSheet(
                "QPushButton { background-color: #2E7D32; color: #FFFFFF; font-weight: bold; font-size: 14px; padding: 10px 25px; border-radius: 4px; border: none; } "
                "QPushButton:hover { background-color: #388E3C; }"
            )
            self.play_btn.clicked.connect(self.on_play_clicked)

            btn_layout.addWidget(self.check_btn)
            btn_layout.addWidget(self.mq_checkbox)
            btn_layout.addStretch()
            btn_layout.addWidget(self.play_btn)
            layout.addLayout(btn_layout)

            # Initial Welcome Message
            self.append_log("Welcome to Bigg Adventure Patcher (RoF2)!\n• RoF2 Login Server uses Port 5999.\n• Log in with your EQEmu account and select Bigg Adventure from the server list!\n" + "="*65)
            self.save_custom_host()

        def on_preset_selected(self):
            srv = self.server_combo.currentData()
            if srv:
                self.host_input.setText(srv["host"])
                self.save_custom_host()

        def save_custom_host(self):
            target = self.host_input.text().strip()
            if not target:
                target = "login.eqemulator.net:5999"
                self.host_input.setText(target)
            ok, msg = update_eqhost_file(target, self.client_dir)
            if ok:
                self.append_log(f"[✓] {msg}")

        def append_log(self, text):
            self.log_text.append(text)
            self.log_text.verticalScrollBar().setValue(self.log_text.verticalScrollBar().maximum())

        def set_status(self, text):
            self.status_lbl.setText(text)

        def set_progress(self, val):
            self.progress_bar.setValue(val)

        def start_patch_thread(self):
            self.check_btn.setEnabled(False)
            self.save_custom_host()
            srv = self.server_combo.currentData()
            target_host = self.host_input.text().strip()
            threading.Thread(target=self.run_patch_worker, args=(srv, target_host), daemon=True).start()

        def run_patch_worker(self, srv_info, target_host):
            self.signals.status.emit("Checking manifest server...")
            manifest_url = srv_info["manifest"] if srv_info else "http://192.168.4.78:8080/patch_manifest.json"
            base_url = srv_info["base_url"] if srv_info else "http://192.168.4.78:8080/patch_files/"

            self.signals.log.emit(f"\nChecking manifest at {manifest_url}...")
            manifest_data = None

            test_urls = [manifest_url] + [s["manifest"] for s in CANDIDATE_SERVERS if s["manifest"] != manifest_url]

            for u in test_urls:
                try:
                    req = urllib.request.Request(u, headers={'User-Agent': 'HeroesJourneyPatcher/2.0'})
                    with urllib.request.urlopen(req, timeout=2) as resp:
                        if resp.status == 200:
                            manifest_data = json.loads(resp.read().decode())
                            base_url = u.rsplit('/', 1)[0] + "/patch_files/"
                            self.signals.log.emit(f"[✓] Connected to manifest server: {u}")
                            break
                except Exception:
                    continue

            if not manifest_data:
                self.signals.log.emit("[!] Notice: HTTP patch server is offline. Using direct connection mode.")
                update_eqhost_file(target_host, self.client_dir)
                self.signals.log.emit(f"[✓] eqhost.txt is set to: Host={target_host}")
                self.signals.status.emit(f"Ready to connect to {target_host}")
                self.signals.progress.emit(100)
                self.signals.finished.emit()
                return

            server_news = manifest_data.get("news", "")
            if server_news:
                self.signals.log.emit(f"\nSERVER NEWS:\n{server_news}\n" + "-"*65)

            eqhost_setting = manifest_data.get("eqhost", f"Host={target_host}")
            update_eqhost_file(eqhost_setting, self.client_dir)
            retire_client_files(manifest_data.get("retire"), self.client_dir, self.signals.log.emit)
            self.signals.log.emit(f"[✓] Configured eqhost.txt -> {eqhost_setting}")

            files_to_patch = manifest_data.get("files", [])
            total_files = len(files_to_patch)

            if total_files == 0:
                self.signals.log.emit("[✓] No custom patch files required.")
                self.signals.status.emit("Client is up to date!")
                self.signals.progress.emit(100)
                self.signals.finished.emit()
                return

            updated_count = 0
            for idx, file_info in enumerate(files_to_patch):
                rel_path = file_info["path"]
                expected_hash = file_info["sha256"]
                local_path = os.path.join(self.client_dir, rel_path)

                # Protect 32-bit RoF2 client from being replaced by 64-bit Live eqgame.exe
                if rel_path.lower() == "eqgame.exe":
                    continue

                local_hash = get_sha256(local_path)

                progress_pct = int(((idx + 1) / total_files) * 100)
                self.signals.progress.emit(progress_pct)
                self.signals.status.emit(f"Checking file ({idx+1}/{total_files}): {rel_path}")

                if local_hash != expected_hash:
                    self.signals.log.emit(f"[-->] Downloading updated asset: {rel_path}")
                    download_url = base_url + rel_path
                    dir_name = os.path.dirname(local_path)
                    if dir_name:
                        os.makedirs(dir_name, exist_ok=True)
                    try:
                        urllib.request.urlretrieve(download_url, local_path)
                        updated_count += 1
                        if rel_path.lower().endswith(".zip"):
                            self.signals.log.emit(f"[-->] Extracting archive: {rel_path}...")
                            import zipfile
                            with zipfile.ZipFile(local_path, 'r') as zip_ref:
                                zip_ref.extractall(self.client_dir)
                            self.signals.log.emit(f"[✓] Successfully extracted {rel_path}")
                        elif rel_path.startswith("mq/lua/"):
                            import shutil
                            dest_lua = os.path.join(self.client_dir, "lua", os.path.basename(rel_path))
                            os.makedirs(os.path.dirname(dest_lua), exist_ok=True)
                            shutil.copy2(local_path, dest_lua)
                    except Exception as e:
                        self.signals.log.emit(f"[!] Error downloading/extracting {rel_path}: {e}")

            self.signals.log.emit(f"\n[✓] Patch check completed! ({updated_count} files updated)")
            self.signals.status.emit("Client is fully patched and ready!")
            self.signals.finished.emit()

        def on_patch_finished(self):
            self.check_btn.setEnabled(True)

        def on_play_clicked(self):
            self.save_custom_host()
            launch_mq = self.mq_checkbox.isChecked()
            self.append_log(f"Launching EverQuest RoF2{' with MacroQuest' if launch_mq else ''}...")
            ok, msg = launch_eqgame(self.client_dir, launch_mq=launch_mq)
            if ok:
                self.append_log(f"[✓] {msg}")
                self.status_lbl.setText("Game launched!")
            else:
                self.append_log(f"[!] Error: {msg}")
                QMessageBox.critical(self, "Launch Error", msg)

    app = QApplication(sys.argv)
    win = PatcherWindow()
    win.show()
    sys.exit(app.exec())

# ----------------- CLI Mode Fallback -----------------
def run_cli_patcher():
    client_dir = os.path.dirname(os.path.abspath(__file__))
    print("="*65)
    print("Bigg Adventure - Heroes Journey Patcher (RoF2 Port 5999 CLI)")
    print("="*65)
    print(f"Working Directory: {client_dir}")

    host = "login.eqemulator.net:5999"
    if len(sys.argv) > 1 and not sys.argv[1].startswith("--"):
        host = sys.argv[1]

    update_eqhost_file(f"Host={host}", client_dir)
    print(f"[✓] Configured eqhost.txt -> Host={host}")

    if "--launch" in sys.argv:
        ok, msg = launch_eqgame(client_dir)
        print(f"[{'✓' if ok else '!'}] {msg}")

if __name__ == "__main__":
    script_dir = os.path.dirname(os.path.abspath(__file__))
    if os.path.exists(os.path.join(script_dir, "eqgame.exe")):
        os.chdir(script_dir)

    if "--cli" in sys.argv or "--launch" in sys.argv:
        run_cli_patcher()
    else:
        try:
            run_pyqt6_gui()
        except Exception:
            run_cli_patcher()
