"""Token de Pixiv (OAuth de `gppt`) -> `.env`, con reintentos y log.

Lo llama `pixiv_login.bat`; tambien funciona a mano:

    python Engine/pixiv_token.py --login     # login OAuth (navegador + codigo), con reintentos
    python Engine/pixiv_token.py             # guarda el token cacheado en .env
    python Engine/pixiv_token.py --check     # solo prueba el token que ya hay en .env
    python Engine/pixiv_token.py --browser   # plan B: navegador automatizado con tu clave
    python Engine/pixiv_token.py --profile otro --login

Diferencias con `python -m gppt login`:

- **No imprime el token** (gppt lo escribe en stdout; aqui nunca se ve).
- **Reintenta sin relanzar**: la URL y el `code_verifier` valen para toda la
  ejecucion, asi que si el codigo caduca puedes volver a logearte y pegar otro
  sin cerrar la ventana (el fallo tipico es que el codigo dura ~30 s).
- **Todo queda en `Logs/pixiv_login.log`**, incluido el error exacto de Pixiv.
"""
import argparse
import contextlib
import getpass
import sys
import webbrowser
from pathlib import Path

sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace")

BASE_DIR = Path(__file__).resolve().parent.parent
ENV = BASE_DIR / ".env"
LOG = BASE_DIR / "Logs" / "pixiv_login.log"
VAR = "PIXIV_REFRESH_TOKEN"


# ------------------------------------------------------------------ log
class _Tee:
    """Escribe en la consola y, a la vez, en el fichero de log."""

    def __init__(self, stream, fp):
        self.stream, self.fp = stream, fp

    def write(self, text):
        with contextlib.suppress(Exception):
            self.stream.write(text)
            self.stream.flush()
        with contextlib.suppress(Exception):
            self.fp.write(text)
            self.fp.flush()
        return len(text)

    def flush(self):
        for target in (self.stream, self.fp):
            with contextlib.suppress(Exception):
                target.flush()

    def isatty(self):
        try:
            return self.stream.isatty()
        except Exception:
            return False

    def __getattr__(self, name):        # encoding, fileno, ...
        return getattr(self.stream, name)


def attach_log(header: str) -> None:
    LOG.parent.mkdir(parents=True, exist_ok=True)
    fp = LOG.open("a", encoding="utf-8", errors="backslashreplace")
    fp.write("\n" + "=" * 62 + f"\n{header}\n")
    sys.stdout = _Tee(sys.stdout, fp)
    sys.stderr = _Tee(sys.stderr, fp)


# ------------------------------------------------------------------ .env
def read_env_text() -> tuple:
    """(lineas sin saltos, salto de linea dominante)."""
    if not ENV.exists():
        return [], "\r\n"
    raw = ENV.read_bytes().decode("utf-8", errors="replace")
    eol = "\r\n" if "\r\n" in raw else "\n"
    lines = [ln.rstrip("\r") for ln in raw.replace("\r\n", "\n").split("\n")]
    return lines, eol


def update_env(value: str) -> str:
    """Escribe/actualiza `PIXIV_REFRESH_TOKEN` sin tocar el resto del .env."""
    lines, eol = read_env_text()
    out, done = [], False
    for line in lines:
        if line.strip().startswith(VAR):
            out.append(f"{VAR}={value}")
            done = True
        else:
            out.append(line)
    if not done:
        while out and not out[-1].strip():
            out.pop()
        out += ["",
                "# Token de Pixiv (gppt): lee obras y mide co-ocurrencia real de tags.",
                f"{VAR}={value}"]
    ENV.write_bytes((eol.join(out).rstrip() + eol).encode("utf-8"))
    return "actualizado" if done else "anadido"


def env_token() -> str:
    import os
    value = os.environ.get(VAR) or ""
    if not value:
        with contextlib.suppress(Exception):
            from dotenv import load_dotenv
            load_dotenv(ENV)
            value = os.environ.get(VAR) or ""
    return value


# --------------------------------------------------------------- oauth
INSTRUCCIONES = """\
------------------------------------------------------------
 1. En el navegador abre una pestana y pulsa F12
    (pestana Network -> marca "Preserve log").
 2. Vuelve aqui y pulsa Enter: se abrira esta URL:

    {url}

 3. Logeate en Pixiv.
 4. En el Network saldra "callback?state=...&code=..." o "pixiv://...".
 5. Copia SOLO el valor de code= y pegalo aqui abajo
    (tambien vale pegar la URL entera).

 El codigo caduca a los ~30 s: no te entretengas.
 Si falla, no cierres esto: la URL sigue valiendo, vuelve a logearte
 y pega un codigo nuevo.
------------------------------------------------------------
"""

HINTS = (
    ("oauth process", "codigo caducado (30 s), ya usado, o de otra pestana/ejecucion"),
    ("invalid request", "lo pegado no es un `code` valido: copia el valor de code=, no state"),
    ("invalid grant", "codigo caducado o ya usado"),
    ("rate", "demasiados intentos seguidos: espera 1-2 minutos"),
    ("918", "peticion mal formada: el codigo no es el valor de code="),
)


def explicate(message: str) -> str:
    low = message.lower().replace("_", " ")
    for needle, hint in HINTS:
        if needle in low:
            return hint
    return ""


def login_oauth(tries: int = 6):
    """Login OAuth con la MISMA url/verifier para todos los intentos."""
    from gppt.oauth import Authorization, build_login_url, extract_code, generate_pkce
    from gppt.token import TokenError, exchange

    with contextlib.suppress(Exception):
        # Que Pixiv conteste en ingles: el error en japones no se puede leer.
        import gppt.token as _gt
        _gt._HEADERS.setdefault("accept-language", "en-us")

    verifier, challenge = generate_pkce()
    url = build_login_url(challenge)
    # NO abrimos el navegador de golpe: si se abre antes de que el usuario
    # active "Preserve log", el redirect a pixiv:// pasa sin dejar rastro y no
    # hay `code` que copiar. Es el fallo tipico de este flujo.
    print(INSTRUCCIONES.format(url=url))
    try:
        input(">>> Enter cuando tengas F12 abierto y 'Preserve log' marcado ")
    except (EOFError, KeyboardInterrupt):
        raise SystemExit("[X] cancelado: no se ha escrito nada en .env")
    with contextlib.suppress(Exception):
        webbrowser.open(url)
    print("[..] abriendo el navegador...")

    for n in range(1, tries + 1):
        try:
            raw = input(f"code ({n}/{tries}): ")
        except (EOFError, KeyboardInterrupt):
            raise SystemExit("[X] cancelado: no se ha escrito nada en .env")
        try:
            code = extract_code(raw)
        except Exception as e:
            print(f"[!] {e}")
            continue
        if len(code) < 12:
            print(f"[!] eso no es un codigo de Pixiv ({len(code)} caracteres): "
                  f"copia el valor de code=")
            continue
        try:
            return exchange(Authorization(code=code, code_verifier=verifier))
        except TokenError as e:
            print("[X] Pixiv rechazo el codigo.")
            print(f"    {e}")
            hint = explicate(str(e))
            if hint:
                print(f"    -> {hint}")
            print("    La URL de arriba sigue valiendo: vuelve a logearte y pega otro codigo.")
    raise SystemExit("[X] agotados los intentos: no se ha escrito nada en .env")


def login_browser():
    """Plan B: navegador automatizado con tus credenciales (no guarda nada)."""
    import gppt
    print("[..] login con navegador automatizado (Playwright).")
    print("     La primera vez descarga Chromium y tarda un poco.")
    print("     No se guarda ninguna credencial en disco.")
    user = input("pixiv ID o email: ").strip()
    pwd = getpass.getpass("password (no se muestra): ").strip()
    if not user or not pwd:
        raise SystemExit("[X] faltan credenciales")
    try:
        return gppt.login(user, pwd, headless=False)
    except Exception as e:
        raise SystemExit(f"[X] fallo el login con navegador: {type(e).__name__}: {e}")


# --------------------------------------------------------------- token
def load_gppt():
    try:
        import gppt
    except ImportError:
        raise SystemExit("[!] falta gppt:  python -m pip install --upgrade gppt")
    return gppt


def obtain(profile: str, fresh: bool, browser: bool):
    gppt = load_gppt()
    if browser:
        tok = login_browser()
    elif fresh:
        print("[..] login manual de Pixiv (OAuth), sin escribir el token en pantalla")
        tok = login_oauth()
    else:
        try:
            return gppt.get_token(profile=profile, method="oauth", save=True)
        except Exception as e:
            raise SystemExit(f"[X] no hay token utilizable en cache: {e}")
    gppt.token.save(profile, tok)
    print(f"[OK] token guardado en {gppt.config.token_path(profile)}")
    return tok


def check(value: str) -> tuple:
    """(sirve?, token_posiblemente_rotado) leyendo obras R-18 de verdad."""
    try:
        from pixivpy3 import AppPixivAPI
    except ImportError:
        print("[!] falta pixivpy3:  python -m pip install pixivpy3")
        return False, value
    if not value:
        print("[X] no hay token que probar")
        return False, value
    try:
        api = AppPixivAPI()
        api.auth(refresh_token=value)
        api.set_accept_language("en-us")
        res = api.search_illust(word="原神 R-18",
                                search_target="partial_match_for_tags",
                                sort="date_desc", filter="for_ios")
    except Exception as e:
        print(f"[X] el token no funciona: {type(e).__name__}: {e}")
        return False, value
    illusts = (res.get("illusts") if isinstance(res, dict)
               else getattr(res, "illusts", None)) or []
    print(f"[OK] token valido: {len(illusts)} obras R-18 leidas de verdad")
    return True, getattr(api, "refresh_token", "") or value


# ----------------------------------------------------------------- main
def main() -> None:
    p = argparse.ArgumentParser(description="Token de Pixiv -> .env")
    p.add_argument("--profile", default="default", help="perfil de gppt (default)")
    p.add_argument("--login", action="store_true",
                   help="login OAuth interactivo (navegador + codigo)")
    p.add_argument("--browser", action="store_true",
                   help="plan B: navegador automatizado con tus credenciales")
    p.add_argument("--check", action="store_true",
                   help="no toca .env: solo prueba el token guardado")
    args = p.parse_args()

    from datetime import datetime
    attach_log(f"run {datetime.now():%Y-%m-%d %H:%M:%S}  "
               f"modo={'check' if args.check else 'browser' if args.browser else 'login' if args.login else 'cache'}")

    if args.check:
        value = env_token()
        if not value:
            raise SystemExit(f"[X] {VAR} esta vacio en .env")
        print(f"[..] probando el token guardado ({len(value)} caracteres)")
        ok, rotated = check(value)
        if ok and rotated and rotated != value:
            print(f"[..] Pixiv roto el refresh_token: {update_env(rotated)} en .env")
        raise SystemExit(0 if ok else 1)

    tok = obtain(args.profile, fresh=args.login, browser=args.browser)
    who = tok.user_name or "?"
    print(f"[OK] sesion de Pixiv: {who} (@{tok.user_account or '?'}, "
          f"id {tok.user_id or '?'})")
    state = update_env(tok.refresh_token)
    print(f"[OK] {VAR} {state} en .env ({len(tok.refresh_token)} caracteres)")
    ok, rotated = check(tok.refresh_token)
    if ok and rotated and rotated != tok.refresh_token:
        print(f"[..] Pixiv roto el refresh_token: {update_env(rotated)} en .env")
    print(f"[..] log completo: {LOG}")
    raise SystemExit(0 if ok else 1)


if __name__ == "__main__":
    main()
