#!/usr/bin/env python3
"""
probe_login.py — single login attempt against the Shinemonitor/EyBond API.

Uses the REAL action name (`authSource`) that the reverse-engineered
SolarPower_1.6.1.0 APK uses, not the placeholder `user` action that fails
with ERR_FORBIDDEN.

Usage:
    pip install requests          # only third-party dep
    SOLAR_EMAIL='you@example.com' SOLAR_PASSWORD='yourpass' python3 probe_login.py

Output on success:
    ✔ Login successful.
      utoken: <short prefix...short suffix>  (N chars)
      ztoken: <short prefix...short suffix>  (M chars)
      cached: ./tokens.json
      expires: <ISO8601 timestamp, ~5 days from now>

Output on failure: prints the FULL raw response body, exit code 1.

The cached tokens are written to ./tokens.json (mode 600) and are good for
~5 days.  Re-run run_all_endpoints.py to use them.
"""

import os
import sys
import json
import time
import hashlib
import urllib.parse
from datetime import datetime, timezone
from pathlib import Path

try:
    import requests
except ImportError:
    sys.exit("ERROR: 'requests' is required.  Install with:  pip install requests")

# ---------------------------------------------------------------------------
# Constants — match the APK
# ---------------------------------------------------------------------------
BASE_URL      = "http://android.shinemonitor.com/public/"
COMPANY_KEY   = "bnrl_frRFjEz8Mkn"
APP_ID        = "com.eybond.solarpower"
APP_VERSION   = "1.6.1.0"
TOKEN_PATH    = Path(os.environ.get("SOLAR_TOKEN_PATH", "./tokens.json"))


def _base_action(action: str, extra: str = "") -> str:
    """Build the i18n/lang/source/app meta suffix the gateway expects."""
    meta = (
        f"&i18n=en&lang=en&source=1"
        f"&_app_client_=android"
        f"&_app_id_={APP_ID}"
        f"&_app_version_={APP_VERSION}"
    )
    suffix = f"&{extra.lstrip('&')}" if extra else ""
    return f"&action={action}{suffix}{meta}"


def _login_url(email: str, password: str) -> tuple[str, str]:
    """Return (url, salt).  Sign = SHA1(salt + sha1(pwd) + base_action)."""
    base = _base_action("authSource", f"usr={urllib.parse.quote(email, safe='')}&company-key={COMPANY_KEY}")
    salt = str(int(datetime.now().timestamp() * 1000))
    pwd_sha1 = hashlib.sha1(password.encode("utf-8")).hexdigest().lower()
    sign = hashlib.sha1((salt + pwd_sha1 + base).encode("utf-8")).hexdigest().lower()
    return f"{BASE_URL}?sign={sign}&salt={salt}{base}", salt


def main() -> int:
    email    = os.environ.get("SOLAR_EMAIL")
    password = os.environ.get("SOLAR_PASSWORD")
    if not email or not password:
        sys.exit(
            "ERROR: SOLAR_EMAIL and SOLAR_PASSWORD env vars are required.\n"
            "  Example:\n"
            "    SOLAR_EMAIL='you@example.com' SOLAR_PASSWORD='yourpass' python3 probe_login.py\n"
            "  Or in a shell:\n"
            "    export SOLAR_EMAIL='you@example.com'\n"
            "    export SOLAR_PASSWORD='yourpass'\n"
            "    python3 probe_login.py\n"
        )

    url, salt = _login_url(email, password)
    print(f"GET  {url}")
    print(f"  email    = {email}")
    print(f"  password = {'*' * len(password)} ({len(password)} chars)")
    print(f"  salt     = {salt}")

    try:
        resp = requests.get(url, timeout=20, headers={"User-Agent": f"SolarPower/{APP_VERSION} Android"})
    except requests.RequestException as e:
        sys.stderr.write(f"\nNETWORK ERROR: {e}\n")
        return 3

    print(f"\nHTTP {resp.status_code} {resp.reason}")
    print(f"Content-Type:   {resp.headers.get('Content-Type', '?')}")
    print(f"Content-Length: {resp.headers.get('Content-Length', '?')}")

    try:
        data = resp.json()
    except ValueError:
        sys.stderr.write("\nNON-JSON RESPONSE BODY:\n")
        sys.stderr.write(resp.text)
        sys.stderr.write("\n")
        return 4

    print("\n=== FULL API RESPONSE ===")
    print(json.dumps(data, indent=2, ensure_ascii=False))
    print("=== END API RESPONSE ===\n")

    code  = data.get("err", data.get("code", data.get("status", -1)))
    inner = data.get("dat", data.get("data", data))
    utok  = inner.get("utoken") if isinstance(inner, dict) else None
    ztok  = inner.get("ztoken") if isinstance(inner, dict) else None

    if code in (0, "0") and utok:
        TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
        cache = {
            "utoken":      utok,
            "ztoken":      ztok,
            "issued_at":   datetime.now(timezone.utc).isoformat(timespec="seconds"),
            "valid_for_s": 5 * 24 * 3600,
            "email":       email,
            "base_url":    BASE_URL,
        }
        TOKEN_PATH.write_text(json.dumps(cache, indent=2))
        os.chmod(TOKEN_PATH, 0o600)
        print("✔ Login successful.")
        print(f"  utoken:  {utok[:16]}…{utok[-8:]}  ({len(utok)} chars)")
        if ztok:
            print(f"  ztoken:  {ztok[:16]}…{ztok[-8:]}  ({len(ztok)} chars)")
        else:
            print("  ztoken:  (none returned)")
        print(f"  cached:  {TOKEN_PATH.resolve()}")
        expires = datetime.fromtimestamp(
            time.time() + cache["valid_for_s"], tz=timezone.utc
        ).isoformat(timespec="seconds")
        print(f"  expires: {expires}")
        print(
            "\nNext:\n"
            "  • Tokens are good for ~5 days.  Re-running this script will\n"
            "    get a fresh pair (and overwrite the cache).\n"
            "  • To exercise the rest of the API, run:\n"
            "        python3 run_all_endpoints.py\n"
            "    (the runner auto-uses the cached tokens)."
        )
        return 0

    err = data.get("err", "?")
    msg = (
        data.get("desc")
        or data.get("errMsg")
        or data.get("msg")
        or data.get("message")
        or "(no message field)"
    )
    sys.stderr.write(f"\n✘ LOGIN FAILED: err={err}  message={msg!r}\n")
    sys.stderr.write(
        "  Common causes:\n"
        "    err=16  ERR_PASSWORD_VERIF_FAIL  signature rejected (rate-limit\n"
        "                                       or clock skew); back off 15 min\n"
        "    err=17  ERR_USER_NOT_EXIST       email not registered\n"
        "    err=18  ERR_PASSWORD             wrong password\n"
        "    err=20  ERR_FREQ                 too many logins; back off\n"
    )
    return 1


if __name__ == "__main__":
    sys.exit(main())
