"""
candy.vps — Module VPS Candy v18
==================================
Permet d'accéder à ton espace VPS Candy depuis le terminal.

Le tunnel Cloudflare est auto-récupéré depuis :
  https://client.hubworld.net/candy.json
  → { "is_closed": false, "link": "https://xxxxx.trycloudflare.com" }

Toutes les routes VPS sont préfixées avec ce lien dynamique.

Commandes CLI :
  candy vps --"TOKEN"                   → Session interactive
  candy vps --ls --"TOKEN"              → Lister les fichiers
  candy vps --ls mon_dossier --"TOKEN"  → Lister un sous-dossier
  candy vps --upload "app.py" --"TOKEN" → Envoyer un fichier
  candy vps --jobs --"TOKEN"            → Voir les scripts actifs
  candy vps --kill "job_id" --"TOKEN"   → Arrêter un script
  candy vps --quota --"TOKEN"           → Espace disque
  candy --efile "script.py" --"TOKEN"   → Exécuter un fichier hébergé
"""

import sys
import json
import shlex
import time
from pathlib import Path
from typing import Optional

try:
    import httpx
except ImportError:
    httpx = None

# ── Constantes ────────────────────────────────────────────────────────────────

CANDY_JSON_URL = "https://client.hubworld.net/candy.json"

RESET  = "\033[0m"
CYAN   = "\033[96m"
GREEN  = "\033[92m"
RED    = "\033[91m"
YELLOW = "\033[93m"
GRAY   = "\033[90m"
WHITE  = "\033[97m"
BOLD   = "\033[1m"


# ── Résolution du tunnel (même logique que client.py) ─────────────────────────

class _TunnelResolver:
    """
    Lit candy.json pour obtenir l'URL courante du tunnel Cloudflare.
    Cache l'URL pour éviter de re-fetch à chaque appel.
    Invalide automatiquement en cas d'erreur réseau (tunnel changé).
    """

    def __init__(self):
        self._base_url: Optional[str] = None

    def resolve(self) -> str:
        """Retourne l'URL de base du tunnel (avec cache)."""
        if self._base_url:
            return self._base_url
        return self._fetch()

    def _fetch(self) -> str:
        _check_httpx()
        try:
            resp = httpx.get(CANDY_JSON_URL, timeout=10)
            resp.raise_for_status()
            data = resp.json()
        except Exception as e:
            raise VPSUnavailableError(f"Impossible de lire candy.json : {e}")

        if data.get("is_closed", True):
            raise VPSUnavailableError("Le serveur VPS Candy est actuellement hors ligne.")

        link = data.get("link", "").strip().rstrip("/")
        if not link:
            raise VPSUnavailableError("candy.json ne contient pas d'URL de tunnel valide.")

        self._base_url = link
        return self._base_url

    def invalidate(self):
        """Force un re-fetch au prochain appel (tunnel changé)."""
        self._base_url = None

    def resolve_with_retry(self, max_attempts: int = 2) -> str:
        """
        Tente de résoudre le tunnel.
        Si le premier appel échoue avec une 5xx ou ConnectError, invalide le cache
        et retente une fois (le tunnel a peut-être changé).
        """
        for attempt in range(max_attempts):
            try:
                return self.resolve()
            except VPSUnavailableError:
                if attempt < max_attempts - 1:
                    self.invalidate()
                    time.sleep(0.5)
                else:
                    raise


_tunnel = _TunnelResolver()


# ── Exceptions ────────────────────────────────────────────────────────────────

class VPSUnavailableError(Exception):
    pass

class VPSAuthError(Exception):
    pass

class VPSError(Exception):
    pass


# ── Helpers internes ──────────────────────────────────────────────────────────

def _check_httpx():
    if httpx is None:
        print(f"{RED}Erreur : httpx non installé. Lance : pip install httpx{RESET}")
        sys.exit(1)


def _headers(token: str) -> dict:
    return {
        "Authorization": f"Bearer {token}",
        "Content-Type":  "application/json",
        "X-Candy-Version": "18.0.0",
    }


def _fmt_size(b: int) -> str:
    if b < 1024:         return f"{b} o"
    if b < 1024 ** 2:    return f"{b/1024:.1f} Ko"
    if b < 1024 ** 3:    return f"{b/1024**2:.1f} Mo"
    return f"{b/1024**3:.2f} Go"


def _vps_get(token: str, path: str, params: dict = None, timeout: int = 15) -> dict:
    """
    GET vers le VPS avec résolution automatique du tunnel.
    Si le tunnel a changé (ConnectError / 502), invalide et retente une fois.
    """
    _check_httpx()
    for attempt in range(2):
        try:
            base = _tunnel.resolve_with_retry()
            url  = f"{base}/vps{path}"
            r    = httpx.get(url, headers=_headers(token), params=params, timeout=timeout)
            if r.status_code == 401:
                raise VPSAuthError("Token invalide ou expiré.")
            if r.status_code in (502, 503, 504):
                # Tunnel probablement changé
                _tunnel.invalidate()
                if attempt == 0:
                    continue
            r.raise_for_status()
            return r.json()
        except (httpx.ConnectError, httpx.RemoteProtocolError):
            _tunnel.invalidate()
            if attempt == 1:
                raise VPSUnavailableError("Impossible de joindre le VPS. Serveur hors ligne ?")
    return {}


def _vps_post(token: str, path: str, payload: dict = None, timeout: int = 30) -> dict:
    """POST vers le VPS avec résolution automatique du tunnel."""
    _check_httpx()
    for attempt in range(2):
        try:
            base = _tunnel.resolve_with_retry()
            url  = f"{base}/vps{path}"
            r    = httpx.post(url, headers=_headers(token), json=payload or {}, timeout=timeout)
            if r.status_code == 401:
                raise VPSAuthError("Token invalide ou expiré.")
            if r.status_code == 403:
                data = r.json() if r.content else {}
                raise VPSError(data.get("reason", "Accès refusé."))
            if r.status_code == 404:
                raise VPSError("Ressource introuvable sur le VPS.")
            if r.status_code == 413:
                raise VPSError("Fichier trop volumineux (quota dépassé).")
            if r.status_code in (502, 503, 504):
                _tunnel.invalidate()
                if attempt == 0:
                    continue
            r.raise_for_status()
            return r.json()
        except (httpx.ConnectError, httpx.RemoteProtocolError):
            _tunnel.invalidate()
            if attempt == 1:
                raise VPSUnavailableError("Impossible de joindre le VPS. Serveur hors ligne ?")
    return {}


# ── Commandes VPS publiques ───────────────────────────────────────────────────

def vps_ls(token: str, path: str = "/") -> None:
    """Liste les fichiers dans un répertoire du VPS."""
    try:
        data  = _vps_get(token, "/ls", params={"path": path})
        items = data.get("items", [])
        used  = data.get("used_bytes", 0)
        total = data.get("total_bytes", 3 * 1024**3)

        print(f"\n  {CYAN}📁 {path}{RESET}  {GRAY}({_fmt_size(used)} / {_fmt_size(total)}){RESET}\n")
        if not items:
            print(f"  {GRAY}(dossier vide){RESET}")
        else:
            for item in items:
                icon  = "📁" if item.get("type") == "dir" else "📄"
                name  = item.get("name", "?")
                size  = _fmt_size(item.get("size", 0)) if item.get("type") != "dir" else ""
                mtime = item.get("mtime", "")
                print(f"  {icon}  {WHITE}{name:<30}{RESET}  {GRAY}{size:<10}  {mtime}{RESET}")
        print()

    except VPSAuthError as e:
        print(f"{RED}{e}{RESET}")
    except VPSUnavailableError as e:
        print(f"{RED}{e}{RESET}")
    except VPSError as e:
        print(f"{YELLOW}{e}{RESET}")
    except Exception as e:
        print(f"{RED}Erreur ls : {e}{RESET}")


def vps_upload(token: str, local_path: str, remote_path: Optional[str] = None) -> None:
    """Envoie un fichier local vers le VPS."""
    p = Path(local_path)
    if not p.exists():
        print(f"{RED}Fichier introuvable : {local_path}{RESET}")
        return

    dest = remote_path or p.name
    size = p.stat().st_size
    print(f"  {CYAN}⬆ Upload{RESET}  {p.name}  {GRAY}({_fmt_size(size)}){RESET} → {dest} ...")

    try:
        content = p.read_bytes()
        # Essayer UTF-8, sinon base64
        try:
            text = content.decode("utf-8")
            encoding = "utf-8"
        except UnicodeDecodeError:
            import base64
            text = base64.b64encode(content).decode()
            encoding = "base64"

        data = _vps_post(token, "/upload", {
            "filename": dest,
            "content":  text,
            "encoding": encoding,
        }, timeout=60)

        saved_size = data.get("size", size)
        print(f"  {GREEN}✓ Fichier '{dest}' envoyé{RESET}  {GRAY}({_fmt_size(saved_size)}){RESET}\n")

    except VPSAuthError as e:
        print(f"{RED}{e}{RESET}")
    except VPSError as e:
        print(f"{RED}{e}{RESET}")
    except VPSUnavailableError as e:
        print(f"{RED}{e}{RESET}")
    except Exception as e:
        print(f"{RED}Erreur upload : {e}{RESET}")


def vps_jobs(token: str) -> None:
    """Affiche les scripts actifs sur le VPS."""
    try:
        data = _vps_get(token, "/jobs")
        jobs = data.get("jobs", [])

        print(f"\n  {CYAN}⚡ Scripts actifs{RESET}\n")
        if not jobs:
            print(f"  {GRAY}Aucun script en cours.{RESET}\n")
            return

        print(f"  {GRAY}{'ID':<12}  {'SCRIPT':<25}  {'DURÉE':<10}  STATUT{RESET}")
        print(f"  {GRAY}{'─'*60}{RESET}")
        for job in jobs:
            jid    = str(job.get("id", "?"))[:10]
            script = str(job.get("script", "?"))[:23]
            dur    = str(job.get("duration", "?"))
            status = job.get("status", "running")
            col    = GREEN if status == "running" else YELLOW
            print(f"  {col}{jid:<12}{RESET}  {WHITE}{script:<25}{RESET}  {GRAY}{dur:<10}{RESET}  {col}{status}{RESET}")
        print()

    except VPSAuthError as e:
        print(f"{RED}{e}{RESET}")
    except VPSUnavailableError as e:
        print(f"{RED}{e}{RESET}")
    except Exception as e:
        print(f"{RED}Erreur jobs : {e}{RESET}")


def vps_kill(token: str, job_id: str) -> None:
    """Arrête un script en cours."""
    try:
        _vps_post(token, f"/kill/{job_id}")
        print(f"  {GREEN}✓ Job '{job_id}' arrêté.{RESET}\n")
    except VPSError as e:
        print(f"{YELLOW}{e}{RESET}")
    except VPSAuthError as e:
        print(f"{RED}{e}{RESET}")
    except VPSUnavailableError as e:
        print(f"{RED}{e}{RESET}")
    except Exception as e:
        print(f"{RED}Erreur kill : {e}{RESET}")


def vps_quota(token: str) -> None:
    """Affiche l'espace disque utilisé sur le VPS."""
    try:
        d     = _vps_get(token, "/quota")
        used  = d.get("used_bytes", 0)
        total = d.get("total_bytes", 3 * 1024**3)
        pct   = int(used / total * 30) if total else 0
        bar   = "█" * pct + "░" * (30 - pct)
        col   = GREEN if pct < 20 else YELLOW if pct < 26 else RED
        print(f"\n  {CYAN}Stockage VPS{RESET}")
        print(f"  {col}{bar}{RESET}  {_fmt_size(used)} / {_fmt_size(total)}\n")
    except VPSAuthError as e:
        print(f"{RED}{e}{RESET}")
    except VPSUnavailableError as e:
        print(f"{RED}{e}{RESET}")
    except Exception as e:
        print(f"{RED}Erreur quota : {e}{RESET}")


def vps_efile(token: str, filename: str) -> None:
    """Lance l'exécution d'un fichier hébergé et stream l'output."""
    print(f"\n  {CYAN}[candy vps] Lancement de '{filename}'...{RESET}")
    try:
        data   = _vps_post(token, "/run", {"filename": filename})
        job_id = data.get("job_id", "?")
        pid    = data.get("pid", "?")
        print(f"  {GREEN}▶ Job '{job_id}' démarré (PID {pid}){RESET}\n")
        _stream_job_output(token, job_id)

    except VPSError as e:
        print(f"{RED}Script bloqué : {e}{RESET}")
    except VPSAuthError as e:
        print(f"{RED}{e}{RESET}")
    except VPSUnavailableError as e:
        print(f"{RED}{e}{RESET}")
    except Exception as e:
        print(f"{RED}Erreur efile : {e}{RESET}")


def _stream_job_output(token: str, job_id: str) -> None:
    """
    Stream l'output d'un job via SSE.
    Résout le tunnel au moment du stream (il peut avoir changé).
    """
    _check_httpx()
    try:
        base = _tunnel.resolve_with_retry()
        url  = f"{base}/vps/output/{job_id}"

        with httpx.stream("GET", url, headers=_headers(token), timeout=300) as r:
            if r.status_code == 401:
                print(f"{RED}Token invalide.{RESET}")
                return
            if r.status_code == 404:
                print(f"{YELLOW}Job introuvable ou déjà terminé.{RESET}")
                return
            if r.status_code in (502, 503):
                print(f"{YELLOW}Tunnel injoignable. Retente avec : candy vps --jobs --\"TOKEN\"{RESET}")
                return

            for line in r.iter_lines():
                if not line:
                    continue
                if line.startswith("data: "):
                    payload = line[6:]
                    if payload == "[DONE]":
                        break
                    try:
                        d    = json.loads(payload)
                        kind = d.get("type", "stdout")
                        text = d.get("text", "")
                        if kind == "stderr":
                            print(f"{YELLOW}{text}{RESET}", end="", flush=True)
                        elif kind == "exit":
                            code = d.get("code", 0)
                            col  = GREEN if code == 0 else RED
                            print(f"\n  {col}✓ Terminé (code {code}){RESET}\n")
                            return
                        else:
                            print(text, end="", flush=True)
                    except json.JSONDecodeError:
                        print(payload, end="", flush=True)

    except (httpx.ConnectError, httpx.RemoteProtocolError):
        _tunnel.invalidate()
        print(f"\n{RED}Connexion au tunnel perdue.{RESET}")
        print(f"  Le tunnel Cloudflare a peut-être changé.")
        print(f"  Vérifie : https://client.hubworld.net/candy.json\n")
    except KeyboardInterrupt:
        print(f"\n  {YELLOW}↯ Interruption (Ctrl+C) — le script tourne toujours sur le VPS.{RESET}")
        print(f"  Pour l'arrêter : {CYAN}candy vps --kill {job_id} --\"TOKEN\"{RESET}\n")


# ── Session interactive ───────────────────────────────────────────────────────

_VPS_CMDS = {
    "ls":    "Lister les fichiers",
    "cd":    "Changer de dossier",
    "pwd":   "Afficher le chemin courant",
    "quota": "Voir l'espace utilisé",
    "help":  "Afficher cette aide",
    "exit":  "Quitter la session",
}


def vps_session(token: str) -> None:
    """
    Session VPS interactive (style SSH).
    Résolution du tunnel au démarrage + ré-vérification si déconnexion.
    """
    _check_httpx()

    # ── Vérification connexion + infos compte ──────────────────────────────
    print(f"\n  {GRAY}Connexion au VPS Candy...{RESET}", end="", flush=True)
    try:
        info  = _vps_get(token, "/whoami")
        email = info.get("email", "?")
        used  = info.get("used_bytes", 0)
        total = info.get("total_bytes", 3 * 1024**3)
        tunnel_url = _tunnel.resolve()
    except VPSAuthError:
        print(f"\r  {RED}Token invalide.{RESET}")
        print(f"  Récupère ton token sur candy-gate.hubworld.net → Dashboard → VPS\n")
        return
    except VPSUnavailableError as e:
        print(f"\r  {RED}VPS hors ligne : {e}{RESET}\n")
        return
    except Exception as e:
        print(f"\r  {RED}Erreur : {e}{RESET}\n")
        return

    # ── Bandeau de bienvenue ───────────────────────────────────────────────
    short_tunnel = tunnel_url.replace("https://", "").split(".")[0][:20]
    print(f"\r  {GREEN}{BOLD}🍬 Candy VPS — Connecté{RESET}")
    print(f"  {GRAY}Compte    : {WHITE}{email}{RESET}")
    print(f"  {GRAY}Stockage  : {WHITE}{_fmt_size(used)} / {_fmt_size(total)}{RESET}")
    print(f"  {GRAY}Tunnel    : {WHITE}{short_tunnel}...trycloudflare.com{RESET}")
    print(f"  {GRAY}Répertoire: /{RESET}")
    print(f"  {GRAY}Tapez 'help' pour les commandes, 'exit' pour quitter.{RESET}\n")

    cwd = "/"

    while True:
        try:
            prompt_str = f"  {CYAN}candy@vps:{WHITE}{cwd}{CYAN}${RESET} "
            try:
                line = input(prompt_str).strip()
            except EOFError:
                break

            if not line:
                continue

            try:
                parts = shlex.split(line)
            except ValueError:
                parts = line.split()

            cmd = parts[0].lower()

            # ── exit ──────────────────────────────────────────────────────
            if cmd == "exit":
                print(f"\n  {GRAY}👋 Session VPS fermée.{RESET}\n")
                break

            # ── help ──────────────────────────────────────────────────────
            elif cmd == "help":
                print(f"\n  {CYAN}Commandes disponibles :{RESET}")
                for c, desc in _VPS_CMDS.items():
                    print(f"  {GREEN}{c:<10}{RESET}  {GRAY}{desc}{RESET}")
                print(f"\n  {GRAY}Tu peux aussi exécuter n'importe quelle commande shell :{RESET}")
                print(f"  {GRAY}python script.py · node app.js · bash run.sh · cat fichier.txt...{RESET}\n")

            # ── pwd ───────────────────────────────────────────────────────
            elif cmd == "pwd":
                print(f"  {cwd}")

            # ── quota ─────────────────────────────────────────────────────
            elif cmd == "quota":
                vps_quota(token)

            # ── ls ────────────────────────────────────────────────────────
            elif cmd == "ls":
                target = parts[1] if len(parts) > 1 else cwd
                if not target.startswith("/"):
                    target = f"{cwd.rstrip('/')}/{target}"
                vps_ls(token, target)

            # ── cd ────────────────────────────────────────────────────────
            elif cmd == "cd":
                if len(parts) < 2 or parts[1] in ("~", "/"):
                    cwd = "/"
                elif parts[1] == "..":
                    parent = str(Path(cwd).parent)
                    cwd = parent if parent.startswith("/") else "/"
                else:
                    new = parts[1] if parts[1].startswith("/") else f"{cwd.rstrip('/')}/{parts[1]}"
                    # Vérifier existence du dossier
                    try:
                        _vps_get(token, "/ls", params={"path": new})
                        cwd = new
                    except VPSError:
                        print(f"  {RED}cd: {new}: Dossier introuvable{RESET}")
                    except Exception:
                        cwd = new  # optimiste si le serveur ne répond pas

            # ── Commandes shell → exécutées sur le VPS ────────────────────
            else:
                _run_remote_command(token, line, cwd)

        except KeyboardInterrupt:
            print(f"\n  {GRAY}(Ctrl+C — tape 'exit' pour quitter){RESET}")
        except VPSAuthError:
            print(f"  {RED}Session expirée. Ton token n'est plus valide.{RESET}")
            break
        except VPSUnavailableError:
            print(f"  {YELLOW}Tunnel injoignable. Nouvelle tentative...{RESET}")
            _tunnel.invalidate()
        except Exception as e:
            print(f"  {RED}Erreur inattendue : {e}{RESET}")


def _run_remote_command(token: str, command: str, cwd: str) -> None:
    """Exécute une commande shell sur le VPS et stream l'output."""
    try:
        data   = _vps_post(token, "/exec", {"command": command, "cwd": cwd})
        job_id = data.get("job_id")

        if job_id:
            # Commande longue → stream
            _stream_job_output(token, job_id)
        else:
            # Réponse directe (commandes courtes type cat, echo...)
            output = data.get("output", "")
            if output:
                for line in output.splitlines():
                    print(f"  {line}")

    except VPSError as e:
        print(f"  {RED}✕ Bloqué : {e}{RESET}")
    except VPSAuthError as e:
        raise  # remonté à la session pour couper la boucle
    except VPSUnavailableError:
        _tunnel.invalidate()
        print(f"  {YELLOW}Connexion perdue. Tunnel peut-être changé.{RESET}")
    except Exception as e:
        print(f"  {RED}Erreur : {e}{RESET}")
