#!/usr/bin/env python3
"""
SolarPower / EyBond Console Client
===================================
Reverse-engineered from SolarPower_1.6.1.0 APK.

Connects to the EyBond cloud API to:
  1. Log in with email or phone credentials
  2. List plants (solar installations)
  3. List devices (inverters/collectors) per plant
  4. Show device status and live data

API Gateway: http://cfb.eybond.com/cfb/api?action=...
Auth tokens: utoken (returned on login, sent as header)
"""

import requests
import json
import sys
import getpass
import hashlib
import urllib.parse
from datetime import datetime

# ──────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────

# API servers discovered in the APK
API_SERVERS = {
    "cfb":  "http://cfb.eybond.com/cfb/api",        # Main API gateway (action-based)
    "pro":  "https://pro.eybond.com",                # REST API server 1
    "pro2": "https://pro2.eybond.com",               # REST API server 2
    "pro3": "https://pro3.eybond.com",               # REST API server 3
}

BASE_URL = API_SERVERS["cfb"]

# Colors for terminal output
class C:
    HEADER  = "\033[95m"
    BLUE    = "\033[94m"
    CYAN    = "\033[96m"
    GREEN   = "\033[92m"
    YELLOW  = "\033[93m"
    RED     = "\033[91m"
    BOLD    = "\033[1m"
    DIM     = "\033[2m"
    RESET   = "\033[0m"


# ──────────────────────────────────────────────
# API Client
# ──────────────────────────────────────────────

class SolarPowerClient:
    """Client for the EyBond SolarPower cloud API."""

    def __init__(self, base_url=BASE_URL):
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "SolarPower/1.6.1.0 Android",
            "Accept": "application/json",
        })
        self.utoken = None
        self.ztoken = None
        self.user_info = None

    def _request(self, action, extra_params="", method="GET"):
        """Make an authenticated API request with the proper Shinemonitor signature."""
        
        # 1. Action string
        action_str = f"&action={action}"
        if extra_params:
            action_str += f"&{extra_params.lstrip('&')}"
            
        # 2. Base action
        base_action = self._get_base_action(action_str)
        
        # 3. Time salt
        salt = str(int(datetime.now().timestamp() * 1000))
        
        # 4. Signature for authenticated requests: SHA1(salt + secret + token + base_action)
        # If we are logged in, we must include the token and secret in the signature
        if self.utoken and self.ztoken:
            sign_input = salt + self.ztoken + self.utoken + base_action
            sign = hashlib.sha1(sign_input.encode('utf-8')).hexdigest().lower()
            url = f"http://android.shinemonitor.com/public/?sign={sign}&salt={salt}&token={self.utoken}{base_action}"
        else:
            # Fallback for unauthenticated or alternative gateway
            parts = []
            if self.utoken:
                parts.append(f"utoken={urllib.parse.quote(self.utoken, safe='')}")
            parts.append(f"action={action}")
            if extra_params:
                parts.append(extra_params.lstrip("&"))
            url = f"{self.base_url}?{'&'.join(parts)}"

        print(f"\n{C.DIM}--> [{method.upper()}] {url}{C.RESET}")

        try:
            if method.upper() == "GET":
                resp = self.session.get(url, timeout=15)
            else:
                resp = self.session.post(url, timeout=15)

            resp.raise_for_status()
            return resp.json()
        except requests.exceptions.ConnectionError:
            print(f"\n{C.RED}[ERROR] Could not connect to {self.base_url}{C.RESET}")
            print(f"{C.DIM}Check your internet connection or try a different server.{C.RESET}")
            return None
        except requests.exceptions.Timeout:
            print(f"\n{C.RED}[ERROR] Request timed out.{C.RESET}")
            return None
        except requests.exceptions.HTTPError as e:
            print(f"\n{C.RED}[ERROR] HTTP Error: {e}{C.RESET}")
            return None
        except json.JSONDecodeError:
            print(f"\n{C.RED}[ERROR] Invalid JSON response.{C.RESET}")
            print(f"{C.DIM}Response text: {resp.text[:500]}{C.RESET}")
            return None

    # ── Auth ─────────────────────────────────

    def _get_base_action(self, action_str):
        """Construct the base action string with app metadata."""
        app_id = "com.eybond.solarpower"
        app_version = "1.6.1.0"
        return f"{action_str}&i18n=en&lang=en&source=1&_app_client_=android&_app_id_={app_id}&_app_version_={app_version}"

    def _build_login_url(self, usr, pwd):
        """Build the complex login URL using Shinemonitor public API signature."""
        company_key = "bnrl_frRFjEz8Mkn"
        usr_encoded = urllib.parse.quote(usr, safe='')
        
        # 1. Action string
        action_str = f"&action=authSource&usr={usr_encoded}&company-key={company_key}"
        
        # 2. Base action
        base_action = self._get_base_action(action_str)
        
        # 3. Time salt
        salt = str(int(datetime.now().timestamp() * 1000))
        
        # 4. Password SHA1
        pwd_sha1 = hashlib.sha1(pwd.encode('utf-8')).hexdigest().lower()
        
        # 5. Signature = SHA1(salt + pwd_sha1 + base_action)
        sign_input = salt + pwd_sha1 + base_action
        sign = hashlib.sha1(sign_input.encode('utf-8')).hexdigest().lower()
        
        # 6. Final URL
        url = f"http://android.shinemonitor.com/public/?sign={sign}&salt={salt}{base_action}"
        return url

    def login_by_email(self, email, password):
        """Login using email and password."""
        url = self._build_login_url(email, password)
        print(f"\n{C.DIM}--> [GET] {url}{C.RESET}")
        try:
            resp = self.session.get(url, timeout=15)
            resp.raise_for_status()
            data = resp.json()
            return self._handle_login_response(data)
        except Exception as e:
            print(f"\n{C.RED}[ERROR] Login request failed: {e}{C.RESET}")
            return False

    def login_by_phone(self, phone, password):
        """Login using phone number and password."""
        url = self._build_login_url(phone, password)
        print(f"\n{C.DIM}--> [GET] {url}{C.RESET}")
        try:
            resp = self.session.get(url, timeout=15)
            resp.raise_for_status()
            data = resp.json()
            return self._handle_login_response(data)
        except Exception as e:
            print(f"\n{C.RED}[ERROR] Login request failed: {e}{C.RESET}")
            return False

    def _handle_login_response(self, data):
        """Process login response and extract tokens."""
        if data is None:
            return False

        # Debug: show the raw response structure
        print(f"\n{C.DIM}Raw response: {json.dumps(data, indent=2, ensure_ascii=False)[:1000]}{C.RESET}")

        # Try various response formats the API might use
        success = False

        # Format 1: {"code": 0, "data": {"utoken": "...", ...}} or {"err": 0, "dat": {"token": "...", ...}}
        if isinstance(data, dict):
            code = data.get("code", data.get("status", data.get("errCode", data.get("err", -1))))
            msg = data.get("msg", data.get("message", data.get("errMsg", data.get("desc", ""))))

            if code == 0 or code == 200 or code == "0" or code == "200":
                inner = data.get("data", data.get("dat", data))
                self.utoken = inner.get("utoken", inner.get("token", inner.get("access_token", "")))
                self.ztoken = inner.get("ztoken", inner.get("secret", ""))
                self.user_info = inner
                success = True
            elif "utoken" in data:
                # Format 2: flat response with utoken at top level
                self.utoken = data.get("utoken", "")
                self.ztoken = data.get("ztoken", "")
                self.user_info = data
                success = True
            else:
                print(f"\n{C.RED}[LOGIN FAILED] Code: {code}, Message: {msg}{C.RESET}")
                return False

        if success and self.utoken:
            print(f"\n{C.GREEN}{C.BOLD}✔ Login successful!{C.RESET}")
            print(f"  {C.DIM}utoken: {self.utoken[:40]}...{C.RESET}")
            if self.ztoken:
                print(f"  {C.DIM}ztoken: {self.ztoken[:40]}...{C.RESET}")
            return True
        elif success:
            print(f"\n{C.YELLOW}[WARNING] Login response OK but no token found.{C.RESET}")
            return True
        else:
            print(f"\n{C.RED}[LOGIN FAILED] Unexpected response format.{C.RESET}")
            return False

    # ── Plants ───────────────────────────────

    def get_plant_count(self):
        """Get the total number of plants."""
        return self._request("queryPlantCount")

    def get_plants(self, page=0, pagesize=10):
        """Get list of plants (solar installations)."""
        return self._request("webQueryPlants", f"&page={page}&pagesize={pagesize}")

    def get_plant_info(self, plant_id):
        """Get detailed info for a specific plant."""
        return self._request("queryPlantInfo", f"plantid={plant_id}")

    def get_plants_energy_day(self):
        """Get today's energy data for all plants."""
        return self._request("queryPlantsEnergyDay")

    def get_plants_active_power(self):
        """Get current active output power for all plants."""
        return self._request("queryPlantsActiveOuputPowerCurrent")

    def get_plants_active_power_one_day(self, date):
        """Get output power history curve for all plants on a specific day."""
        return self._request("queryPlantsActiveOuputPowerOneDay", f"date={date}")

    # ── Devices ──────────────────────────────

    def get_device_count(self):
        """Get the total number of devices."""
        return self._request("queryDeviceCount")

    def get_devices(self, plant_id, page=0, pagesize=10):
        """Get devices for a specific plant."""
        return self._request("queryDevices", f"plantid={plant_id}&page={page}&pagesize={pagesize}")

    def get_collectors(self, plant_id, page=0, pagesize=10):
        """Get collectors for a specific plant."""
        return self._request("queryCollectors", f"pagesize={pagesize}&page={page}&plantid={plant_id}")

    def get_collector_devices(self, pn):
        """Get devices connected to a specific collector."""
        return self._request("queryCollectorDevices", f"pn={pn}")

    def get_collector_devices_status(self, pn):
        """Get status of devices connected to a collector."""
        return self._request("queryCollectorDevicesStatus", f"pn={pn}")

    def get_device_last_data(self, pn, devcode, devaddr, sn):
        """Get last known data from a specific device."""
        return self._request("querySPDeviceLastData",
                             f"pn={pn}&devcode={devcode}&devaddr={devaddr}&sn={sn}")

    def get_device_energy_flow(self, pn, sn, devaddr, devcode):
        """Get energy flow data for a device."""
        return self._request("webQueryDeviceEnergyFlowEs",
                             f"pn={pn}&sn={sn}&devaddr={devaddr}&devcode={devcode}")

    def get_device_data_one_day(self, pn, devcode, sn, devaddr, date):
        """Get one day of historical data for a device."""
        return self._request("queryDeviceDataOneDay",
                             f"pn={pn}&devcode={devcode}&sn={sn}&devaddr={devaddr}&date={date}")

    def get_device_ctrl_fields(self, pn, devcode, devaddr, sn):
        """Get controllable fields for a device."""
        return self._request("webQueryDeviceCtrlField",
                             f"pn={pn}&devcode={devcode}&devaddr={devaddr}&sn={sn}")

    def get_account_info(self):
        """Get current account information."""
        return self._request("queryAccountInfo")

    # ── Web-style queries (ES = Elasticsearch) ──

    def web_query_devices(self, page=0, pagesize=50, pn=None):
        """Query all devices (Elasticsearch-backed)."""
        params = f"&page={page}&pagesize={pagesize}"
        if pn:
            params += f"&pn={pn}"
        return self._request("webQueryDeviceEs", params)

    def web_query_collectors(self, page=0, pagesize=50):
        """Query all collectors (Elasticsearch-backed)."""
        return self._request("webQueryCollectorsEs",
                             f"&page={page}&pagesize={pagesize}&loaded=2")

    def web_query_device_status(self):
        """Query device status view."""
        return self._request("webQueryDeviceStatusViewEs")


# ──────────────────────────────────────────────
# Console UI
# ──────────────────────────────────────────────

def print_banner():
    banner = f"""
{C.CYAN}{C.BOLD}╔══════════════════════════════════════════════════════╗
║          ☀  SolarPower Console Client  ☀             ║
║          EyBond API v1.6.1.0 (RE'd)                  ║
╚══════════════════════════════════════════════════════╝{C.RESET}
"""
    print(banner)


def print_section(title):
    print(f"\n{C.BOLD}{C.BLUE}{'─' * 50}")
    print(f"  {title}")
    print(f"{'─' * 50}{C.RESET}")


def print_table(headers, rows):
    """Print a formatted table to the console."""
    if not rows:
        print(f"  {C.DIM}(no data){C.RESET}")
        return

    # Calculate column widths
    widths = [len(h) for h in headers]
    for row in rows:
        for i, cell in enumerate(row):
            if i < len(widths):
                widths[i] = max(widths[i], len(str(cell)))

    # Print header
    header_line = "  "
    for i, h in enumerate(headers):
        header_line += f"{C.BOLD}{h:<{widths[i]}}{C.RESET}  "
    print(header_line)
    print(f"  {'  '.join(['─' * w for w in widths])}")

    # Print rows
    for row in rows:
        line = "  "
        for i, cell in enumerate(row):
            if i < len(widths):
                line += f"{str(cell):<{widths[i]}}  "
        print(line)


def extract_items(data, key_candidates=None):
    """Extract list items from various API response formats."""
    if data is None:
        return []

    if key_candidates is None:
        key_candidates = ["data", "dat", "rows", "list", "items", "result", "records",
                          "plants", "devices", "collectors", "plant", "device", "collector"]

    if isinstance(data, list):
        return data

    if isinstance(data, dict):
        # Extract from inner 'dat' or 'data' if present
        inner = data.get("dat", data.get("data", data))
        
        # Check for nested data
        for key in key_candidates:
            if key in inner:
                val = inner[key]
                if isinstance(val, list):
                    return val
                elif isinstance(val, dict):
                    # Recurse one level
                    for k2 in key_candidates:
                        if k2 in val and isinstance(val[k2], list):
                            return val[k2]
                    return [val]  # Single item

        # If there's a 'code' field, the data might be at top level
        if "code" in data or "status" in data:
            return []

        # Return the dict itself as a single-item list
        return [data]

    return []


def display_plants(client):
    """Fetch and display all plants."""
    print_section("Solar Plants")

    # Get plant count first
    count_data = client.get_plant_count()
    if count_data:
        inner = count_data.get("dat", count_data.get("data", count_data))
        count = inner.get("count", inner.get("total", "?"))
        print(f"  {C.GREEN}Total plants: {count}{C.RESET}")

    # Get plant list
    plants_data = client.get_plants(page=0, pagesize=50)
    if plants_data is None:
        print(f"  {C.RED}Failed to fetch plants.{C.RESET}")
        return []

    plants = extract_items(plants_data)
    if not plants:
        print(f"  {C.YELLOW}No plants found. Raw response:{C.RESET}")
        print(f"  {C.DIM}{json.dumps(plants_data, indent=2, ensure_ascii=False)[:800]}{C.RESET}")
        return []

    # Display plant table
    headers = ["#", "Plant ID", "Name", "Capacity", "Status", "Location"]
    rows = []
    for i, p in enumerate(plants):
        pid = p.get("pid", p.get("plantid", p.get("plantId", p.get("id", "?"))))
        name = p.get("name", p.get("plantName", "?"))
        cap = p.get("nominalPower", p.get("capacity", p.get("installedCapacity", "?")))
        status = p.get("status", p.get("plantStatus", "?"))
        addr = p.get("address", {})
        if isinstance(addr, dict):
            location = addr.get("address", addr.get("country", "?"))
        else:
            location = str(addr) if addr else "?"
        rows.append([i + 1, pid, name, cap, status, location])

    print_table(headers, rows)
    return plants


def display_devices_for_plant(client, plant):
    """Fetch and display devices for a given plant."""
    pid = plant.get("plantid", plant.get("plantId", plant.get("id")))
    pname = plant.get("name", plant.get("plantName", "?"))
    print_section(f"Devices for Plant: {pname} (ID: {pid})")

    # Try fetching devices
    devices_data = client.get_devices(pid, page=0, pagesize=50)
    devices = extract_items(devices_data)

    if not devices:
        # Try collectors approach
        print(f"  {C.DIM}Trying collectors endpoint...{C.RESET}")
        collectors_data = client.get_collectors(pid, page=0, pagesize=50)
        collectors = extract_items(collectors_data)

        if collectors:
            print(f"\n  {C.GREEN}Found {len(collectors)} collector(s):{C.RESET}")
            headers = ["#", "PN", "Alias", "Status", "Online"]
            rows = []
            for i, col in enumerate(collectors):
                pn = col.get("pn", col.get("collectorPn", "?"))
                alias = col.get("alias", col.get("name", "?"))
                status = col.get("status", "?")
                online = col.get("online", col.get("isOnline", "?"))
                rows.append([i + 1, pn, alias, status, online])
            print_table(headers, rows)

            # Get devices for each collector
            for col in collectors:
                pn = col.get("pn", col.get("collectorPn"))
                if pn:
                    col_devices_data = client.get_collector_devices(pn)
                    col_devices = extract_items(col_devices_data)
                    if col_devices:
                        print(f"\n  {C.CYAN}Devices on collector {pn}:{C.RESET}")
                        display_device_list(col_devices)

                    # Also try status
                    status_data = client.get_collector_devices_status(pn)
                    if status_data:
                        statuses = extract_items(status_data)
                        if statuses:
                            print(f"\n  {C.CYAN}Device status on collector {pn}:{C.RESET}")
                            display_device_list(statuses)
        else:
            print(f"  {C.YELLOW}No devices or collectors found.{C.RESET}")
            if devices_data:
                print(f"  {C.DIM}Devices response: {json.dumps(devices_data, indent=2, ensure_ascii=False)[:500]}{C.RESET}")
            if collectors_data:
                print(f"  {C.DIM}Collectors response: {json.dumps(collectors_data, indent=2, ensure_ascii=False)[:500]}{C.RESET}")
    else:
        display_device_list(devices)

    return devices


def display_device_list(devices):
    """Display a list of devices in table format."""
    headers = ["#", "SN", "Name/Alias", "Type", "DevCode", "DevAddr", "Status"]
    rows = []
    for i, d in enumerate(devices):
        sn = d.get("sn", d.get("deviceSn", d.get("serialNumber", "?")))
        alias = d.get("alias", d.get("name", d.get("deviceName", "?")))
        dtype = d.get("devTypeName", d.get("deviceType", d.get("type", d.get("deviceTypeName", "?"))))
        devcode = d.get("devcode", d.get("deviceCode", "?"))
        devaddr = d.get("devaddr", d.get("deviceAddr", "?"))
        status = d.get("status", d.get("deviceStatus", d.get("runStatus", "?")))
        rows.append([i + 1, sn, alias, dtype, devcode, devaddr, status])

    print_table(headers, rows)


def display_device_data(client, device, pn=None):
    """Fetch and display live data for a specific device."""
    sn = device.get("sn", device.get("deviceSn", ""))
    devcode = device.get("devcode", device.get("deviceCode", ""))
    devaddr = device.get("devaddr", device.get("deviceAddr", ""))
    pn_val = pn or device.get("pn", device.get("collectorPn", ""))

    if not all([sn, devcode, devaddr, pn_val]):
        print(f"  {C.YELLOW}Missing device identifiers (sn/devcode/devaddr/pn).{C.RESET}")
        return

    print_section(f"Live Data for Device: {sn}")

    data = client.get_device_last_data(pn_val, devcode, devaddr, sn)
    if data:
        items = extract_items(data)
        if items:
            for item in items:
                if isinstance(item, dict):
                    for k, v in item.items():
                        print(f"  {C.CYAN}{k:<30}{C.RESET} {v}")
        else:
            print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:1500]}{C.RESET}")
    else:
        print(f"  {C.RED}Failed to fetch device data.{C.RESET}")


def display_all_devices(client):
    """Fetch and display all devices across all plants (web query)."""
    print_section("All Devices (Web Query)")
    data = client.web_query_devices(page=0, pagesize=50)
    if data is None:
        print(f"  {C.RED}Failed to fetch devices.{C.RESET}")
        return []

    devices = extract_items(data)
    if devices:
        display_device_list(devices)
    else:
        print(f"  {C.YELLOW}No devices found. Raw response:{C.RESET}")
        print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:800]}{C.RESET}")
    return devices


def display_all_collectors(client):
    """Fetch and display all collectors."""
    print_section("All Collectors (Web Query)")
    data = client.web_query_collectors(page=0, pagesize=50)
    if data is None:
        print(f"  {C.RED}Failed to fetch collectors.{C.RESET}")
        return

    collectors = extract_items(data)
    if collectors:
        headers = ["#", "PN", "Alias", "Status", "Plant ID"]
        rows = []
        for i, col in enumerate(collectors):
            pn = col.get("pn", col.get("collectorPn", "?"))
            alias = col.get("alias", col.get("name", "?"))
            status = col.get("status", "?")
            pid = col.get("plantid", col.get("plantId", "?"))
            rows.append([i + 1, pn, alias, status, pid])
        print_table(headers, rows)
    else:
        print(f"  {C.YELLOW}No collectors found. Raw response:{C.RESET}")
        print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:800]}{C.RESET}")


def display_account_info(client):
    """Fetch and display account information."""
    print_section("Account Information")
    data = client.get_account_info()
    if data:
        info = data.get("data", data)
        if isinstance(info, dict):
            for k, v in info.items():
                if k not in ("code", "status", "msg", "message"):
                    print(f"  {C.CYAN}{k:<25}{C.RESET} {v}")
        else:
            print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:800]}{C.RESET}")
    else:
        print(f"  {C.RED}Failed to fetch account info.{C.RESET}")


def interactive_menu(client, plants, all_devices):
    """Show interactive menu for further actions."""
    while True:
        print(f"\n{C.BOLD}{C.CYAN}┌─ Menu ──────────────────────────────────────┐{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  1. Refresh plants list                     {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  2. Show devices for a plant                {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  3. Show all devices (web query)            {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  4. Show all collectors                     {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  5. Show account info                       {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  6. Show current active power               {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  7. Show today's energy                     {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  8. Show device live data                   {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  9. Show plant history curve for a day      {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET} 10. Show device history data for a day      {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET} 11. Raw API call (custom action)            {C.CYAN}│{C.RESET}")
        print(f"{C.CYAN}│{C.RESET}  0. Exit                                    {C.CYAN}│{C.RESET}")
        print(f"{C.BOLD}{C.CYAN}└─────────────────────────────────────────────┘{C.RESET}")

        choice = input(f"\n{C.BOLD}Select option: {C.RESET}").strip()

        if choice == "1":
            plants = display_plants(client)

        elif choice == "2":
            if not plants:
                plants = display_plants(client)
            if plants:
                try:
                    idx_str = input(f"  Enter plant number (1-{len(plants)}) [1]: ").strip()
                    idx = int(idx_str) - 1 if idx_str else 0
                    if 0 <= idx < len(plants):
                        display_devices_for_plant(client, plants[idx])
                    else:
                        print(f"  {C.RED}Invalid selection.{C.RESET}")
                except ValueError:
                    print(f"  {C.RED}Invalid input.{C.RESET}")

        elif choice == "3":
            all_devices = display_all_devices(client)

        elif choice == "4":
            display_all_collectors(client)

        elif choice == "5":
            display_account_info(client)

        elif choice == "6":
            print_section("Current Active Output Power")
            data = client.get_plants_active_power()
            if data:
                print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:1500]}{C.RESET}")

        elif choice == "7":
            print_section("Today's Energy Data")
            data = client.get_plants_energy_day()
            if data:
                print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:1500]}{C.RESET}")

        elif choice == "8":
            if not all_devices:
                all_devices = display_all_devices(client)
            if all_devices:
                try:
                    idx_str = input(f"  Enter device number (1-{len(all_devices)}) [1]: ").strip()
                    idx = int(idx_str) - 1 if idx_str else 0
                    if 0 <= idx < len(all_devices):
                        display_device_data(client, all_devices[idx])
                    else:
                        print(f"  {C.RED}Invalid selection.{C.RESET}")
                except ValueError:
                    print(f"  {C.RED}Invalid input.{C.RESET}")

        elif choice == "9":
            date_str = input(f"  Enter date (YYYY-MM-DD) [today]: ").strip()
            if not date_str:
                date_str = datetime.now().strftime("%Y-%m-%d")
            print_section(f"Plant History Curve for {date_str}")
            data = client.get_plants_active_power_one_day(date_str)
            if data:
                print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:3000]}{C.RESET}")

        elif choice == "10":
            if not all_devices:
                all_devices = display_all_devices(client)
            if all_devices:
                try:
                    idx_str = input(f"  Enter device number (1-{len(all_devices)}) [1]: ").strip()
                    idx = int(idx_str) - 1 if idx_str else 0
                    if 0 <= idx < len(all_devices):
                        device = all_devices[idx]
                        sn = device.get("sn", device.get("deviceSn", ""))
                        devcode = device.get("devcode", device.get("deviceCode", ""))
                        devaddr = device.get("devaddr", device.get("deviceAddr", ""))
                        pn = device.get("pn", device.get("collectorPn", ""))
                        
                        date_str = input(f"  Enter date (YYYY-MM-DD) [today]: ").strip()
                        if not date_str:
                            date_str = datetime.now().strftime("%Y-%m-%d")
                            
                        print_section(f"Device History for {sn} on {date_str}")
                        data = client.get_device_data_one_day(pn, devcode, sn, devaddr, date_str)
                        if data:
                            print(f"  {C.DIM}{json.dumps(data, indent=2, ensure_ascii=False)[:3000]}{C.RESET}")
                    else:
                        print(f"  {C.RED}Invalid selection.{C.RESET}")
                except ValueError:
                    print(f"  {C.RED}Invalid input.{C.RESET}")

        elif choice == "11":
            action = input(f"  Enter action name: ").strip()
            params = input(f"  Enter extra params (key=val&...): ").strip()
            if action:
                print_section(f"Custom API Call: {action}")
                data = client._request(action, params)
                if data:
                    print(f"  {json.dumps(data, indent=2, ensure_ascii=False)[:3000]}")

        elif choice == "0":
            print(f"\n{C.GREEN}Goodbye! ☀{C.RESET}\n")
            break
        else:
            print(f"  {C.YELLOW}Invalid option.{C.RESET}")


# ──────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────

def main():
    print_banner()

    # ── Server selection ──
    # Defaulting to 1 (cfb / shinemonitor)
    base_url = API_SERVERS["cfb"]
    client = SolarPowerClient(base_url)

    # ── Login method selection ──
    # Defaulting to 1 (Email)
    print(f"\n  {C.BOLD}Login (Email + Password){C.RESET}")
    
    email = input(f"  {C.BOLD}Email: {C.RESET}").strip()
    password = getpass.getpass(f"  {C.BOLD}Password: {C.RESET}")
    if not email or not password:
        print(f"  {C.RED}Email and password are required.{C.RESET}")
        sys.exit(1)
        
    success = client.login_by_email(email, password)

    if not success:
        print(f"\n{C.RED}Login failed. Please check your credentials and try again.{C.RESET}")
        print(f"{C.DIM}If the API server is unreachable, try a different server.{C.RESET}")
        sys.exit(1)

    # ── Post-login: fetch and display data ──
    print(f"\n{C.GREEN}{'═' * 50}")
    print(f"  Fetching your solar installation data...")
    print(f"{'═' * 50}{C.RESET}")

    # Display plants
    plants = display_plants(client)

    # Display all devices
    all_devices = display_all_devices(client)

    # If we have plants, show devices for the first one
    if plants and not all_devices:
        display_devices_for_plant(client, plants[0])

    # Interactive menu
    interactive_menu(client, plants, all_devices)


if __name__ == "__main__":
    main()
