#!/usr/bin/env python3
"""
login_once.py — single login attempt with verbose error reporting.

Behavior:
  1. Reads SOLAR_EMAIL and SOLAR_PASSWORD from env (refuses to default password).
  2. Calls the Shinemonitor login endpoint exactly ONCE.
  3. On success: prints the utoken/ztoken and writes them to
     /root/solar/api_responses/_tokens.json (mode 600) with a timestamp.
     Tokens are valid for 5 days; reuse them with run_all_endpoints.py.
  4. On error: prints the FULL raw JSON response (not truncated) plus the
     status code and headers, and exits non-zero so the caller can detect it.

This is intentionally separate from run_all_endpoints.py — that runner now
re-uses cached tokens and will not retry login if it can avoid it.
"""

import os
import sys
import json
import time
import hashlib
import requests
from pathlib import Path
from datetime import datetime, timezone

# --- Configuration -----------------------------------------------------------

# API endpoint base (matches solarpower_client.py).
BASE_URL = "http://android.shinemonitor.com/public/"

# Endpoint action for the login call. Per EyBond protocol:
#   base_action = "&action=user&user=<account>&company-key=bnrl_frRFjEz8Mkn"
COMPANY_KEY = "bnrl_frRFjEz8Mkn"

# Where the token cache lives. Lives next to the JSON response dir so it's
# trivially git-ignored and easy to nuke.
TOKEN_PATH = Path("/root/solar/api_responses/_tokens.json")

# --- Credentials -------------------------------------------------------------

EMAIL = os.environ.get("SOLAR_EMAIL", "alphanon")
PASSWORD = os.environ.get("SOLAR_PASSWORD")
if not PASSWORD:
    sys.stderr.write(
        "ERROR: SOLAR_PASSWORD env var is required.\n"
        "  Set it with:  export SOLAR_PASSWORD='...'\n"
    )
    sys.exit(2)

# --- Login signature (mirrors solarpower_client.py) -------------------------
#
# Per the EyBond/Shinemonitor spec, the login signature is:
#   sign = sha1( salt + sha1(password) + base_action )
# where salt is a per-request millisecond timestamp. The server validates
# the sign server-side; on success it returns err=0 plus utoken/ztoken.

def _ms():
    return str(int(round(time.time() * 1000)))

def _build_login_url(email: str, password: str) -> str:
    salt = _ms()
    pwd_sha1 = hashlib.sha1(password.encode("utf-8")).hexdigest()
    base_action = (
        f"&action=user&user={email}&company-key={COMPANY_KEY}"
    )
    sign_src = salt + pwd_sha1 + base_action
    sign = hashlib.sha1(sign_src.encode("utf-8")).hexdigest()
    return f"{BASE_URL}?sign={sign}&salt={salt}{base_action}"


# --- Main --------------------------------------------------------------------

def main() -> int:
    url = _build_login_url(EMAIL, PASSWORD)
    print(f"POST/GET  {url}")
    print(f"  salt       = {url.split('salt=')[1].split('&')[0]}")
    print(f"  email      = {EMAIL}")
    print(f"  password   = {'*' * len(PASSWORD)} ({len(PASSWORD)} chars)")

    try:
        resp = requests.get(url, timeout=20)
    except requests.RequestException as e:
        sys.stderr.write(f"\nNETWORK ERROR: {e}\n")
        return 3

    # Surface the HTTP-level facts first, so we always know what came back.
    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', '?')}")

    # Parse JSON; if it's not JSON, dump the raw text instead.
    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 the full response (no truncation) so we can see exactly what
    # the gateway returned.
    print("\n=== FULL API RESPONSE ===")
    print(json.dumps(data, indent=2, ensure_ascii=False))
    print("=== END API RESPONSE ===\n")

    # Decide success.
    code = data.get("err", data.get("code", data.get("status", -1)))
    inner = data.get("dat", data.get("data", data))
    utoken = (inner or {}).get("utoken") if isinstance(inner, dict) else None
    ztoken = (inner or {}).get("ztoken") if isinstance(inner, dict) else None

    if code in (0, "0") and utoken:
        # Persist tokens. Mode 600 so only the owner can read them.
        TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
        cache = {
            "utoken": utoken,
            "ztoken": ztoken,
            "issued_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
            "valid_for_s": 5 * 24 * 3600,   # 5 days, per Shinemonitor spec
            "email": EMAIL,
        }
        TOKEN_PATH.write_text(json.dumps(cache, indent=2))
        os.chmod(TOKEN_PATH, 0o600)

        expires = datetime.fromtimestamp(
            time.time() + cache["valid_for_s"], tz=timezone.utc
        ).isoformat(timespec="seconds")
        print(f"✔ Login successful.")
        print(f"  utoken: {utoken[:16]}...{utoken[-8:]}  ({len(utoken)} chars)")
        print(f"  ztoken: {ztoken[:16]}...{ztoken[-8:] if ztoken else ''}  "
              f"({len(ztoken) if ztoken else 0} chars)")
        print(f"  cached: {TOKEN_PATH}")
        print(f"  expires: {expires}")
        print()
        print("Next steps:")
        print("  - The runner (run_all_endpoints.py) will auto-reuse these tokens")
        print("    on its next invocation. Re-run with:")
        print("        cd /root/solar && SOLAR_PASSWORD=... python3 run_all_endpoints.py")
        return 0

    # Failure path. We already printed the full body above; just summarize.
    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. Usually a\n"
        "            rate-limit or a clock skew > a few seconds. Wait 15 min\n"
        "            and retry, or check that this host's clock is in sync.\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())
