#!/usr/bin/env python3
"""Verschluesselte Spielkonto-Konfiguration an den Scanner-Dienst uebertragen."""
import getpass
import http.cookiejar
import json
import queue
import sys
import threading
import urllib.error
import urllib.parse
import urllib.request
import webbrowser


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, request, fp, code, message, headers, new_url):
        return None


def scanner_url(value):
    parsed = urllib.parse.urlsplit(value.strip())
    if (parsed.scheme not in ("https", "http") or not parsed.hostname
            or parsed.username is not None or parsed.password is not None
            or parsed.query or parsed.fragment):
        raise ValueError("Bitte eine vollstaendige Scanner-URL ohne Anmeldedaten angeben.")
    if parsed.scheme != "https" and parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
        raise ValueError("Spielzugangsdaten duerfen nur ueber HTTPS uebertragen werden.")
    return value.strip().rstrip("/")


def build_opener():
    cookies = http.cookiejar.CookieJar()
    return urllib.request.build_opener(
        urllib.request.HTTPCookieProcessor(cookies), NoRedirect()
    )


def request_json(opener, url, method, value=None):
    data = json.dumps(value).encode("utf-8") if value is not None else None
    headers = {"Content-Type": "application/json"} if data is not None else {}
    request = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with opener.open(request, timeout=30) as response:
            if response.status == 204:
                return None
            payload = response.read(65537)
            if len(payload) > 65536:
                raise RuntimeError("Die Scanner-Antwort ist zu gross.")
            result = json.loads(payload.decode("utf-8"))
            if not isinstance(result, dict):
                raise RuntimeError("Die Scanner-Antwort hat ein ungueltiges Format.")
            return result
    except urllib.error.HTTPError as error:
        if 300 <= error.code < 400:
            raise RuntimeError("Weiterleitungen werden zum Schutz der Zugangsdaten nicht ausgefuehrt.") from None
        try:
            detail = json.loads(error.read(65536).decode("utf-8")).get("error", "Anfrage fehlgeschlagen.")
        except (ValueError, AttributeError):
            detail = "Anfrage fehlgeschlagen."
        if not isinstance(detail, str):
            detail = "Anfrage fehlgeschlagen."
        raise RuntimeError(detail) from None


def link_windows_account(base_url, email, password, *, cancelled, progress, read_session=None):
    base_url = scanner_url(base_url)
    if read_session is None:
        from windows_session import read_active_session
        read_session = read_active_session
    opener = build_opener()
    progress("Anmeldung am Scanner wird geprueft.")
    request_json(opener, base_url + "/api/auth/login", "POST", {"email": email, "password": password})
    password = None
    access = request_json(opener, base_url + "/api/scanner/access", "GET")
    if access.get("authorized") is not True:
        raise RuntimeError("Das Scannerkonto ist noch nicht freigegeben. Bitte den Betreiber kontaktieren.")
    if cancelled.is_set():
        raise RuntimeError("Verknuepfung abgebrochen.")
    progress("Aktive eigene Windows-Spielsitzung wird gelesen. Es wird keine Tokendatei erstellt.")
    config = read_session(cancelled)
    try:
        if cancelled.is_set():
            raise RuntimeError("Verknuepfung abgebrochen.")
        progress("Spielkonto wird sicher uebertragen. Bitte den Abschluss abwarten.", committing=True)
        linked = request_json(opener, base_url + "/api/scanner/game-account", "PUT", config)
        account = linked.get("account", {})
        if account.get("linked") is not True or account.get("iggId") != config.get("igg_id"):
            raise RuntimeError("Der Scanner hat die Verknuepfung nicht eindeutig bestaetigt.")
        return account
    finally:
        config.clear()


def run_windows_window(base_url=""):
    import tkinter as tk
    from tkinter import messagebox, ttk

    root = tk.Tk()
    root.title("Lords-Mobile-Scanner - Windows-Helfer")
    root.geometry("660x650")
    root.minsize(660, 650)
    frame = ttk.Frame(root, padding=24)
    frame.pack(fill="both", expand=True)
    ttk.Label(frame, text="Aktive Spielsitzung verbinden", font=("Segoe UI", 16, "bold")).pack(anchor="w")
    ttk.Label(frame, wraplength=600, text=(
        "Melde dich zuerst im offiziellen Lords-Mobile-Windows-Spiel an. "
        "Der Helfer liest nur deine eigene Spielinstanz. Suche und Ergebnisse erscheinen danach im Browser."
    )).pack(anchor="w", pady=(8, 14))
    fields = {}
    for key, label, value, secret in (
        ("url", "Scanner-Adresse (HTTPS)", base_url, False),
        ("email", "E-Mail-Adresse deines Scannerkontos", "", False),
        ("password", "Scanner-Passwort (nicht das Spielpasswort)", "", True),
    ):
        ttk.Label(frame, text=label).pack(anchor="w", pady=(4, 3))
        entry = ttk.Entry(frame, show="*" if secret else "", width=70)
        entry.insert(0, value)
        entry.pack(fill="x", pady=(0, 5))
        fields[key] = entry
    consent = tk.BooleanVar(value=False)
    ttk.Checkbutton(frame, variable=consent, text=(
        "Mein eigenes Spielkonto an diesen Scanner uebertragen."
    )).pack(anchor="w", pady=(10, 4))
    ttk.Label(frame, wraplength=600, text=(
        "Beim spaeteren Suchstart kann der Scanner deine Spielsitzung verdraengen. "
        "Waehrend der Suche nicht erneut mit demselben Konto im Spiel anmelden."
    )).pack(anchor="w", pady=(0, 10))
    status = tk.StringVar(value=(
        "Nicht verbunden. Starte das Spiel, melde dich an und klicke dann auf "
        "\"Mit Scanner verbinden\". Es erfolgt keine automatische Verbindung."
    ))
    status_frame = ttk.Frame(frame, height=70)
    status_frame.pack(anchor="w", fill="x")
    status_frame.pack_propagate(False)
    ttk.Label(status_frame, textvariable=status, wraplength=600).pack(fill="both", expand=True)
    buttons = ttk.Frame(frame)
    buttons.pack(side="bottom", fill="x", pady=(12, 0))
    messages = queue.Queue()
    cancelled = threading.Event()
    state = {"busy": False, "committing": False}

    def progress(text, committing=False):
        messages.put(("progress", (text, committing)))

    def worker(url, email, password):
        try:
            account = link_windows_account(
                url, email, password, cancelled=cancelled, progress=progress
            )
        except (OSError, ValueError, RuntimeError, ImportError, urllib.error.URLError) as error:
            message = ("Das Windows-Lesemodul fehlt. Bitte den vollstaendigen Helfer verwenden."
                       if isinstance(error, ImportError) else str(error))
            messages.put(("error", message))
        else:
            messages.put(("done", (url, account)))

    def start():
        if state["busy"]:
            return
        try:
            url = scanner_url(fields["url"].get())
            email = fields["email"].get().strip()
            password = fields["password"].get()
            if not email or not password or not consent.get():
                raise ValueError("Bitte Scanner-Anmeldung ausfuellen und die Uebertragung deines Kontos bestaetigen.")
        except ValueError as error:
            messagebox.showerror("Verbindung", str(error), parent=root)
            return
        cancelled.clear()
        state.update(busy=True, committing=False)
        connect.configure(state="disabled")
        for field in fields.values():
            field.configure(state="disabled")
        fields["password"].configure(state="normal")
        fields["password"].delete(0, "end")
        fields["password"].configure(state="disabled")
        back.configure(text="Abbrechen")
        status.set("Verbindung wird vorbereitet.")
        threading.Thread(target=worker, args=(url, email, password), daemon=True).start()

    def close():
        if state["committing"]:
            status.set("Die Uebertragung laeuft bereits. Bitte den Abschluss abwarten.")
        elif state["busy"]:
            cancelled.set()
            status.set("Abbruch angefordert. Es wird keine neue Uebertragung gestartet.")
        else:
            root.destroy()

    def finish():
        state.update(busy=False, committing=False)
        connect.configure(state="normal")
        back.configure(state="normal", text="Schliessen")
        for field in fields.values():
            field.configure(state="normal")

    def receive():
        while True:
            try:
                kind, value = messages.get_nowait()
            except queue.Empty:
                break
            if kind == "progress":
                text, committing = value
                status.set(text)
                state["committing"] = committing
                if committing:
                    back.configure(state="disabled")
            elif kind == "error":
                finish()
                status.set(value)
                messagebox.showerror("Verknuepfung nicht abgeschlossen", value, parent=root)
            elif kind == "done":
                finish()
                url, account = value
                status.set(
                    f"Spielkonto {account['iggId']} verknuepft. Im Browser ggf. mit demselben "
                    "Scannerkonto anmelden und die E28-Suche einstellen."
                )
                try:
                    opened = webbrowser.open(url, new=2)
                except OSError:
                    opened = False
                if not opened:
                    messagebox.showwarning(
                        "Browser", "Das Konto ist verknuepft. Bitte die Scanner-Adresse im Browser oeffnen.",
                        parent=root,
                    )
        root.after(100, receive)

    back = ttk.Button(buttons, text="Schliessen", command=close)
    back.pack(side="left")
    connect = ttk.Button(buttons, text="Mit Scanner verbinden", command=start)
    connect.pack(side="right")
    root.bind("<Escape>", lambda _event: close())
    root.protocol("WM_DELETE_WINDOW", close)
    fields["url" if not base_url else "email"].focus_set()
    root.after(100, receive)
    root.mainloop()
    return 0


def main(argv=None):
    arguments = list(sys.argv[1:] if argv is None else argv)
    if not arguments or arguments == ["--windows"]:
        return run_windows_window()
    if len(arguments) == 2 and arguments[0] == "--windows":
        return run_windows_window(arguments[1])
    if len(arguments) != 2:
        print("Aufruf: python scanner-account-helper.py https://scanner.example CONFIGURATION.json", file=sys.stderr)
        return 2
    base_url, config_path = arguments
    try:
        base_url = scanner_url(base_url)
        with open(config_path, "r", encoding="utf-8") as source:
            config = json.load(source)
        if isinstance(config, dict) and type(config.get("igg_id")) is int:
            config["igg_id"] = str(config["igg_id"])
        email = input("Scanner-E-Mail-Adresse: ").strip()
        password = getpass.getpass("Scanner-Passwort: ")
        opener = build_opener()
        request_json(opener, base_url + "/api/auth/login", "POST", {"email": email, "password": password})
        linked = request_json(opener, base_url + "/api/scanner/game-account", "PUT", config)
    except (OSError, ValueError, RuntimeError, urllib.error.URLError) as error:
        print("Verknuepfung fehlgeschlagen:", str(error), file=sys.stderr)
        return 1

    account = linked.get("account", {})
    print("Spielkonto wurde sicher mit dem Scannerkonto verknuepft.")
    if account.get("iggId"):
        print("IGG-ID:", account["iggId"])
    print("Die Scan-Ausfuehrung erfolgt ab jetzt auf dem Scanner-Server.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
