#!/usr/bin/env python3
"""LiChat ohne Oberfläche – fürs Team und zum Ausprobieren gegen den eigenen Server.

    lichat-cli [--profil NAME] register <server> <einladung> <benutzername>
    lichat-cli [--profil NAME] koppeln <server> [name]          # dieses Gerät zu einem vorhandenen Konto (zeigt einen Code)
    lichat-cli [--profil NAME] geraete | geraet-hinzufuegen <code> | geraet-entfernen <gerät>   # meine Geräte
    lichat-cli [--profil NAME] ich                          # Benutzer, Gerät, Server
    lichat-cli [--profil NAME] neu [--titel T] <person …>   # Gespräch beginnen (auch Gruppe)
    lichat-cli [--profil NAME] senden [--sofort] <gespräch> <text …>   # tippt so lange wie ein Mensch (mit „…“)
    lichat-cli [--profil NAME] abholen                      # Postfach holen, Neues zeigen
    lichat-cli [--profil NAME] warten [--bis SEKUNDEN]      # wartet, bis etwas kommt, zeigt es und endet (für Claude-Instanzen)
    lichat-cli [--profil NAME] reagieren <gespräch> <textstück|letzte> <emoji> | zuruecknehmen <gespräch> <textstück|letzte>
    lichat-cli [--profil NAME] bearbeiten <gespräch> <text …> | widerrufen <gespräch>   # meine letzte Nachricht (auch: zurueckrufen)
    lichat-cli [--profil NAME] gespraeche                   # alle Gespräche
    lichat-cli [--profil NAME] verlauf <gespräch>
    lichat-cli [--profil NAME] mitglieder <gespräch>
    lichat-cli [--profil NAME] code <gespräch>              # Sicherheitscode zum Vergleichen
    lichat-cli [--profil NAME] entfernen <gespräch> <person …>  # aus dem Gespräch nehmen
    lichat-cli [--profil NAME] hinzufuegen <gespräch> <person …> # in eine Gruppe aufnehmen
    lichat-cli [--profil NAME] umbenennen <gespräch> <name …>   # Gruppenname (nur Ende-zu-Ende)
    lichat-cli [--profil NAME] verlassen <gespräch>
    lichat-cli [--profil NAME] datei <gespräch> <pfad> [--einmal] [--geheim] [--text T]   # Datei/Foto/Video schicken (ohne Metadaten); Fotos auch als Einmal-Bild / verpixelt; --text: Bildunterschrift
    lichat-cli [--profil NAME] anhang <gespräch> <nr> <ziel> # Anhang Nr. (aus „verlauf“) entschlüsselt speichern
    lichat-cli [--profil NAME] name <vorname> [nachname …]  # Anzeigename (Groß/klein, Leerzeichen; nur Ende-zu-Ende)
    lichat-cli [--profil NAME] kontakt                       # mein Kontakt als Link + QR (zum Scannen am Handy)
    lichat-cli [--profil NAME] kontakt-hinzufuegen <link>     # Kontakt-Link/QR-Text eines anderen (prüft den Schlüssel)
    lichat-cli [--profil NAME] pin | pin-einloesen <pin>      # Kontakt per PIN (10 min, einmal)
    lichat-cli [--profil NAME] kontakte
    lichat-cli [--profil NAME] tippanzeige an|aus             # „…“ senden und sehen (ab Werk aus, gegenseitig)
    lichat-cli [--profil NAME] tippen <gespräch> [sekunden]   # „tippt“ schicken, mit Sekunden alle 3 s wiederholt (zum Testen)
    lichat-cli [--profil NAME] bild <datei>|--weg           # Profilbild (512 px, ohne EXIF, nur in den Gesprächen)

Daten: $XDG_DATA_HOME/lichat-cli/<profil> (Standard „standard“, oder LICHAT_PROFIL). Ein <gespräch> ist die kurze
Kennung aus „gespraeche“ (8 Zeichen, ein eindeutiger Anfang reicht).
"""

import os
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from lichat.account import TYPING_EVERY, Account, LiChatError, default_folder, short  # noqa: E402


def when(at):
    return time.strftime("%d.%m. %H:%M", time.localtime(at))


def main(argv):
    profile = os.environ.get("LICHAT_PROFIL", "standard")
    if argv[:1] == ["--profil"] and len(argv) > 1:
        profile, argv = argv[1], argv[2:]
    if not argv or argv[0] in ("-h", "--help", "hilfe"):
        print(__doc__.strip())
        return 0 if argv else 2
    folder = default_folder(profile)
    command, args = argv[0], argv[1:]

    if command == "register":
        if len(args) != 3:
            raise LiChatError("register <server> <einladung> <benutzername>")
        try:
            account = Account.register(folder, *args)
        except LiChatError as error:
            if error.suggestions:          # ebc3d629
                raise LiChatError(f"{error} – frei wären: {', '.join(error.suggestions)}") from None
            raise
        print(f"Angemeldet als {account.username} (Gerät {account.device_id}) bei {account.info['server']}")
        return 0

    if command == "koppeln":
        # Neues Gerät (card da5cbfcb): zeigt einen Code, ein vorhandenes Gerät nimmt ihn an („geraet-hinzufuegen“).
        if not args:
            raise LiChatError("koppeln <server> [gerätename]")
        url, code = Account.link_open(args[0])
        print(f"Code für dein anderes Gerät: {code}   (10 Minuten; dort „Gerät hinzufügen“ – Code eingeben oder QR scannen)", flush=True)
        from lichat.qrcodegen import QrCode
        qr = QrCode.encode_text(Account.link_uri(url, code), QrCode.Ecc.MEDIUM)
        size = qr.get_size()
        for y in range(-1, size + 1, 2):
            print("".join("█" if (qr.get_module(x, y) and qr.get_module(x, y + 1)) else "▀" if qr.get_module(x, y) else "▄" if qr.get_module(x, y + 1) else " "
                          for x in range(-1, size + 1)), flush=True)
        while not Account.link_state(url, code).get("username"):
            time.sleep(2)
        account = Account.link_finish(folder, url, code, " ".join(args[1:]) or "Konsole")
        print(f"Gekoppelt als {account.username} (Gerät {account.device_id}) – Ältere Nachrichten bleiben auf den anderen Geräten.")
        return 0

    account = Account(folder)
    if command == "ich":
        print(f"{account.username} · Gerät {account.device_id} · {account.info['server']} · Profil {profile}")
    elif command == "neu":
        title = ""
        if args[:1] == ["--titel"] and len(args) > 1:
            title, args = args[1], args[2:]
        cid = account.start(args, title)
        print(f"Gespräch {short(bytes.fromhex(cid))} mit {account.title(cid)} begonnen")
    elif command == "senden":
        # „--sofort“ before or right after the conversation – never part of the text (19:38: it went into the team chat)
        now = "--sofort" in args[:2] or os.environ.get("LICHAT_SOFORT") == "1"
        args = [a for i, a in enumerate(args) if not (i < 2 and a == "--sofort")]
        if len(args) < 2:
            raise LiChatError("senden [--sofort] <gespräch> <text …>")
        cid = account.chat(args[0])
        text = " ".join(args[1:])
        if not now:
            account.type_like_a_person(cid, text)     # Olaf 09.10.: so lange wie ein Mensch am Handy, mit „…“
        answer = account.send(cid, text)
        if answer.get("unknown") or answer.get("full"):
            print(f"Nicht zugestellt an: {', '.join(answer.get('unknown', []) + answer.get('full', []))}")
        # gesendet_ms: the moment the server took it – Michelle measures the way to the phone's notification (Z5).
        print(f"gesendet an {len(answer.get('delivered', []))} Gerät(e) · gesendet_ms={int(time.time() * 1000)}")
    elif command in ("abholen", "warten"):
        # warten: one standing connection after the other until something arrives – then show it and end, so a waiting
        # Claude instance (Monitor, background task) is woken exactly when Olaf writes. Without --bis: no end.
        limit = None
        if command == "warten" and args[:1] == ["--bis"] and len(args) > 1:
            limit = time.time() + float(args[1])
        pause = 2
        while True:
            wait = 0 if command == "abholen" else int(min(50, max(1, (limit - time.time()) if limit else 50)))
            try:
                # Receipts (✓✓) and new profile pictures change nothing to answer – keep waiting.
                news = [n for n in account.fetch(wait=wait) if not n.get("receipt") and not n.get("typing") and not n.get("settings") and not (command == "warten" and n.get("picture"))]
                pause = 2
            except LiChatError as error:
                # Server restarting (502), down (5xx) or no network: „warten“ keeps listening (Michelle – else an instance
                # is deaf after every restart), with a growing pause; „abholen“ says it and ends.
                if command == "abholen" or not (str(error).startswith("Server nicht erreichbar") or "(5" in str(error)):
                    raise
                print(f"({error} – neuer Versuch in {pause} s)", file=sys.stderr, flush=True)
                time.sleep(pause)
                pause = min(pause * 2, 60)
                news = []
            if news or command == "abholen" or (limit and time.time() >= limit):
                break
        for item in news:
            chat = short(bytes.fromhex(item["chat"])) if item.get("chat") else "–"
            if item.get("synced"):
                print("Verlauf vom anderen Gerät übernommen")
            elif item.get("joined"):
                if not account.chats.get(item["chat"], {}).get("intern"):
                    print(f"[{chat}] neues Gespräch")
            elif item.get("system"):
                print(f"[{chat}] {item['system']}")
            elif item.get("removed"):
                print(f"[{chat}] {item['removed']} hat dich aus dem Gespräch entfernt")
            elif "reaction" in item:
                if item["reaction"]:
                    print(f"[{chat}] {item['from']} reagiert {item['reaction']} auf „{item.get('target', '')}“")
                else:
                    print(f"[{chat}] {item['from']} nimmt die Reaktion {item.get('before') or ''} auf „{item.get('target', '')}“ zurück".replace("  ", " "))
            elif item.get("picture"):
                print(f"[{chat}] neues Profilbild von {item['picture']}")
            elif item.get("edited"):
                edited = next((m for m in account.chats[item["chat"]]["messages"] if m.get("mid") == item["edited"]), {})
                print(f"[{chat}] {item['from']} hat bearbeitet: {edited.get('text', '')}")
            elif item.get("unsent"):
                print(f"[{chat}] {item['from']} hat eine Nachricht widerrufen")
            elif item.get("problem"):
                print(f"[{chat}] ⚠ {item['problem']}")
            elif "opened" in item:
                # fe9f2029 (Michelle 84a2318e: this crashed „abholen“ – the event has no sender of its own)
                print(f"[{chat}] Einmal-Bild geöffnet")
            elif item.get("text") is None:
                continue                # anything else without a line of its own
            else:
                quote = item.get("antwort")
                about = f" (↪ {quote.get('from')}: {quote.get('text', '')[:40]})" if quote else ""
                mark = " (@dich)" if item.get("mention") else ""
                who = "ich" if item.get("from") == account.username else item.get("from", "?")     # mine, from another of my devices
                print(f"[{chat}] {when(item['at'])} {who}{mark}{about}: {item['text']}")
        if not news:
            print("nichts Neues")
    elif command == "gespraeche":
        for cid, chat in account.visible_chats().items():
            last = chat["messages"][-1] if chat["messages"] else None
            tail = (f" – {last['text'][:40]}" if last.get("system") else f" – {last['from']}: {last['text'][:40]}") if last else ""
            print(f"{short(bytes.fromhex(cid))}  {account.title(cid)}{tail}")
        if not account.chats:
            print("noch keine Gespräche")
    elif command == "verlauf":
        cid = account.chat(args[0] if args else "")
        print(f"— {account.title(cid)} —")
        count = 0
        marks = {"wartet": "◷", "fehler": "! nicht gesendet", "gesendet": "✓", "zugestellt": "✓✓", "gelesen": "✓✓ gelesen"}
        for m in account.chats[cid]["messages"]:
            if m.get("system"):
                print(f"{when(m['at'])} — {m['text']}")
            else:
                number = ""
                if m.get("anhang"):
                    count += 1
                    number = f" [Anhang {count}]"
                who = "ich" if m["from"] == account.username else m["from"]
                tick = f"  {marks[account.tick(cid, m)[0]]}" if m["from"] == account.username else ""
                quote = m.get("antwort")
                about = f" (↪ {quote.get('from')}: {quote.get('text', '')[:40]})" if quote else ""
                reactions = "".join(f"  [{u} {e}]" for u, e in (m.get("reaktionen") or {}).items())
                edited = "  (bearbeitet)" if m.get("bearbeitet") else ""
                # fe9f2029 (Michelle): where a one-time picture stands – „geöffnet von …“ on the sender's side
                state = account.once_state(m)
                if state == "eigen":
                    edited += f"  (geöffnet von {', '.join(sorted(m['geoeffnet_von']))})" if m.get("geoeffnet_von") else "  (noch nicht geöffnet)"
                elif state:
                    edited += {"neu": "  (nur auf dem Handy ansehen)", "geoeffnet": "  (geöffnet)", "abgelaufen": "  (abgelaufen)"}[state]
                print(f"{when(m['at'])} {who}{about}: {account.describe(m) or m['text']}{number}{edited}{tick}{reactions}")
    elif command in ("reagieren", "zuruecknehmen"):
        # Michelle: Reaktionen ohne Handy prüfen. Die Nachricht: ein Stück ihres Textes oder „letzte“ (die letzte der anderen).
        if len(args) < (3 if command == "reagieren" else 2):
            raise LiChatError("reagieren <gespräch> <textstück|letzte> <emoji>  |  zuruecknehmen <gespräch> <textstück|letzte>")
        cid = account.chat(args[0])
        candidates = [m for m in account.chats[cid]["messages"] if m.get("mid") and not m.get("system") and not m.get("zurueckgerufen")]
        if args[1] == "letzte":
            candidates = [m for m in candidates if m.get("from") != account.username]
        else:
            candidates = [m for m in candidates if args[1].lower() in (account.describe(m) or "").lower()]
        if not candidates:
            raise LiChatError(f"Keine Nachricht mit „{args[1]}“")
        target = candidates[-1]
        emoji = args[2] if command == "reagieren" else ""
        account.react(cid, target["mid"], emoji)
        print(f"{'reagiert ' + emoji if emoji else 'Reaktion zurückgenommen'} auf „{(account.describe(target) or '')[:60]}“")
    elif command in ("bearbeiten", "zurueckrufen", "widerrufen"):
        # Meine letzte Nachricht in diesem Gespräch (iOS: 15 min / 5× bearbeiten, 2 min zurückrufen).
        if not args or (command == "bearbeiten" and len(args) < 2):
            raise LiChatError("bearbeiten <gespräch> <neuer text …>  |  widerrufen <gespräch>")
        cid = account.chat(args[0])
        mine = [m for m in account.chats[cid]["messages"] if m.get("from") == account.username and m.get("mid")
                and not m.get("zurueckgerufen")]
        if not mine:
            raise LiChatError("Hier gibt es keine Nachricht von dir")
        if command == "bearbeiten":
            account.edit(cid, mine[-1]["mid"], " ".join(args[1:]))
            print("bearbeitet")
        else:
            account.unsend(cid, mine[-1]["mid"])
            print("widerrufen")
    elif command == "geraete":
        for d in account.my_devices():
            since = time.strftime("%d.%m.%Y %H:%M", time.localtime(d["since"])) if d["since"] else "?"
            print(f"{d['device']}  seit {since}{'  ← dieses Gerät' + (' (' + d['name'] + ')' if d['name'] else '') if d['this'] else ''}")
    elif command == "geraet-hinzufuegen":
        if not args:
            raise LiChatError("geraet-hinzufuegen <code vom neuen Gerät>")
        print("Warte auf das neue Gerät …", flush=True)
        device = account.accept_link(args[0])
        print(f"Neues Gerät {device} ist in allen Gesprächen")
    elif command == "geraet-entfernen":
        if not args:
            raise LiChatError("geraet-entfernen <gerät> (siehe „geraete“)")
        account.remove_my_device(args[0])
        print("Gerät abgemeldet und aus allen Gesprächen genommen")
    elif command == "mitglieder":
        print("\n".join(account.members(account.chat(args[0] if args else ""))))
    elif command == "code":
        print(account.safety_code(account.chat(args[0] if args else "")))
    elif command == "entfernen":
        if len(args) < 2:
            raise LiChatError("entfernen <gespräch> <person …>")
        cid = account.chat(args[0])
        account.remove(cid, args[1:])
        print(f"{', '.join(args[1:])} aus {short(bytes.fromhex(cid))} entfernt")
    elif command == "hinzufuegen":
        if len(args) < 2:
            raise LiChatError("hinzufuegen <gespräch> <person …>")
        account.add(account.chat(args[0]), args[1:])
        print(f"{', '.join(args[1:])} hinzugefügt")
    elif command == "umbenennen":
        account.rename(account.chat(args[0]), " ".join(args[1:]))
        print("Gruppenname gesetzt")
    elif command == "verlassen":
        account.leave(account.chat(args[0] if args else ""))
        print("Unterhaltung verlassen")
    elif command == "datei":
        import mimetypes
        once, secret = "--einmal" in args, "--geheim" in args       # fe9f2029, f9e3f055
        args = [a for a in args if a not in ("--einmal", "--geheim")]
        caption = ""
        if "--text" in args:                                        # 2fcc3eef: the caption in the same message
            at = args.index("--text")
            caption = " ".join(args[at + 1:])
            args = args[:at]
        if len(args) != 2:
            raise LiChatError("datei <gespräch> <pfad> [--einmal] [--geheim] [--text T]")
        cid, path = account.chat(args[0]), Path(args[1])
        mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
        kind = "bild" if mime.startswith("image/") and mime != "image/svg+xml" else "video" if mime.startswith("video/") \
            else "sprache" if mime.startswith("audio/") else "datei"
        account.send_attachment(cid, path.read_bytes(), path.name, mime, kind, once=once, secret=secret, caption=caption)
        print(f"{account.describe(account.chats[cid]['messages'][-1])} gesendet · gesendet_ms={int(time.time() * 1000)}")
    elif command == "anhang":
        if len(args) != 3:
            raise LiChatError("anhang <gespräch> <nr> <ziel>")
        cid = account.chat(args[0])
        attachments = [m["anhang"] for m in account.chats[cid]["messages"] if m.get("anhang")]
        number = int(args[1])
        if not 1 <= number <= len(attachments):
            raise LiChatError(f"Es gibt {len(attachments)} Anhänge (Nr. 1–{len(attachments)})")
        if attachments[number - 1].get("einmal"):
            raise LiChatError("Einmal-Bilder lassen sich nur auf dem Handy ansehen – nie als Datei")
        target = Path(args[2])
        if target.is_dir():
            target = target / attachments[number - 1]["name"]
        data = account.open_attachment(attachments[number - 1])
        target.write_bytes(data)
        print(f"{target} ({len(data)} Bytes)")
    elif command == "name":
        if not args:
            raise LiChatError("name <vorname> [nachname …]")
        account.set_name(args[0], " ".join(args[1:]))
        print(f"Anzeigename: {account.display(account.username)}")
    elif command == "kontakt":
        from lichat.qrcodegen import QrCode
        link = account.contact_link()
        qr = QrCode.encode_text(link, QrCode.Ecc.LOW)
        size = qr.get_size()
        for y in range(-1, size + 1, 2):   # two rows per line: ▀ ▄ █
            print("".join("█" if (qr.get_module(x, y) and qr.get_module(x, y + 1)) else "▀" if qr.get_module(x, y) else "▄" if qr.get_module(x, y + 1) else " "
                          for x in range(-1, size + 1)))
        print(link)
    elif command == "kontakt-hinzufuegen":
        name, contact = account.add_contact_link(args[0] if args else "")
        print(f"{name} hinzugefügt" + (" – verifiziert (Schlüssel passt)" if contact["verifiziert"] else ""))
    elif command == "pin":
        print(f"PIN: {account.new_pin()}  (10 Minuten gültig, einmal)")
    elif command == "pin-einloesen":
        name, contact = account.redeem_pin(" ".join(args))
        print(f"{name} hinzugefügt (nicht verifiziert – zum Prüfen den QR-Code scannen oder den Sicherheitscode vergleichen)")
    elif command == "kontakte":
        for name, contact in sorted(account.contacts.items()):
            print(f"{account.display(name)} ({name})" + ("  ✓ verifiziert" if contact.get("verifiziert") else ""))
        if not account.contacts:
            print("noch keine Kontakte")
    elif command == "tippanzeige":
        if args[:1] not in (["an"], ["aus"]):
            raise LiChatError(f"tippanzeige an|aus (jetzt: {'an' if account.typing_indicator else 'aus'})")
        account.set_typing_indicator(args[0] == "an")
        print(f"Tipp-Anzeige {args[0]} (gegenseitig: aus heißt auch, dass du sie bei anderen nicht siehst)")
    elif command == "tippen":
        if not args:
            raise LiChatError("tippen <gespräch> [sekunden]")
        if not account.typing_indicator:
            raise LiChatError("Die Tipp-Anzeige ist aus – erst „lichat-cli tippanzeige an“")
        cid = account.chat(args[0])
        until = time.time() + (float(args[1]) if len(args) > 1 else 0)
        sent = 0
        while True:
            account._typed.pop(cid, None)
            sent += account.typing(cid)
            if time.time() + TYPING_EVERY > until:
                break
            time.sleep(TYPING_EVERY)
        print(f"„tippt“ {sent}× geschickt")
    elif command == "bild":
        if not args:
            raise LiChatError("bild <datei> | bild --weg")
        account.set_picture(None if args[0] == "--weg" else args[0])
        print("Profilbild entfernt" if args[0] == "--weg" else "Profilbild gesetzt und in alle Gespräche geschickt")
    else:
        raise LiChatError(f"Unbekannter Befehl „{command}“ – lichat-cli hilfe")
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main(sys.argv[1:]))
    except LiChatError as error:
        print(f"lichat-cli: {error}", file=sys.stderr)
        sys.exit(1)
    except ValueError as error:   # the core refused (wrong key, broken state …)
        print(f"lichat-cli: Kern: {error}", file=sys.stderr)
        sys.exit(1)
