# -*- coding: utf-8 -*-
"""
CloudDrive2 Client - HTTP bridge to the gRPC proxy.

To avoid Kodi sub-interpreter conflicts with grpcio's Cython extension,
all gRPC calls run in a separate proxy process (cd2_grpc_proxy.py).
This module is a thin HTTP client that talks to the proxy on localhost:19799.
"""

import os
import json
import subprocess
import urllib.request
import urllib.error

from .cd2_constants import CD2_DEFAULT_HOST, CD2_DEFAULT_PORT
from . import cd2_utils

log = cd2_utils.log

PROXY_HOST = "127.0.0.1"
PROXY_PORT = 19799
PROXY_URL = f"http://{PROXY_HOST}:{PROXY_PORT}"

_proxy_process = None

# Bypass any HTTP proxy for localhost connections.
# Kodi may set system-wide proxy that urllib picks up, causing 502 errors.
_proxy_handler = urllib.request.ProxyHandler({})
_opener = urllib.request.build_opener(_proxy_handler)


def _proxy_script_path():
    return os.path.join(os.path.dirname(os.path.abspath(__file__)),
                        "cd2_grpc_proxy.py")


def _start_proxy():
    """Start the gRPC proxy process if not already running."""
    global _proxy_process
    if _proxy_process is not None and _proxy_process.poll() is None:
        return True  # Already running
    if _proxy_running():
        return True
    try:
        _proxy_process = subprocess.Popen(
            ["python3", _proxy_script_path()],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        # Wait briefly for the proxy to start
        import time
        for _ in range(20):  # Wait up to 2 seconds
            if _proxy_running():
                return True
            time.sleep(0.1)
        return False
    except Exception as e:
        log(f"Failed to start gRPC proxy: {e}")
        return False


def _proxy_running():
    """Check if the gRPC proxy is running."""
    try:
        req = urllib.request.Request(
            f"{PROXY_URL}/health",
            method="GET",
        )
        resp = _opener.open(req, timeout=2)
        return resp.status == 200
    except Exception as e:
        log(f"_proxy_running failed: {e}")
        return False


def _call(endpoint, body=None, timeout=10):
    """Make a POST request to the proxy, return parsed JSON."""
    if not _start_proxy():
        raise RuntimeError("Cannot connect to gRPC proxy")

    data = json.dumps(body or {}, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(
        f"{PROXY_URL}{endpoint}",
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        resp = _opener.open(req, timeout=timeout)
        result = json.loads(resp.read().decode("utf-8"))
        if result.get("success"):
            return AttrDict(result.get("data"))
        else:
            raise RuntimeError(result.get("error", "Unknown error"))
    except urllib.error.URLError as e:
        raise RuntimeError(f"Proxy connection failed: {e}")
    except RuntimeError:
        raise
    except Exception as e:
        raise RuntimeError(str(e))


class AttrDict:
    """Wrap a dict (or list of dicts) to allow attribute access.
    Handles nested structures and camelCase field names from protobuf."""

    def __init__(self, data):
        if isinstance(data, dict):
            self._data = data
            for k, v in data.items():
                if isinstance(v, dict):
                    v = AttrDict(v)
                elif isinstance(v, list):
                    v = [AttrDict(i) if isinstance(i, dict) else i for i in v]
                object.__setattr__(self, k, v)
        elif isinstance(data, list):
            self._data = data
            self._list = [AttrDict(i) if isinstance(i, dict) else i for i in data]
        else:
            self._data = data

    def __getattr__(self, name):
        if name.startswith("_"):
            raise AttributeError(name)
        raise AttributeError(name)

    def __repr__(self):
        return f"AttrDict({self._data!r})"

    def __bool__(self):
        return bool(self._data)

    def __iter__(self):
        if hasattr(self, "_list"):
            return iter(self._list)
        raise TypeError(f"'{type(self).__name__}' object is not iterable")

    def __len__(self):
        if hasattr(self, "_list"):
            return len(self._list)
        if isinstance(self._data, dict):
            return len(self._data)
        return 0

    def __getitem__(self, index):
        if hasattr(self, "_list"):
            return self._list[index]
        raise TypeError(f"'{type(self).__name__}' object does not support indexing")

    def get(self, key, default=None):
        if isinstance(self._data, dict):
            return self._data.get(key, default)
        return default

    def items(self):
        if isinstance(self._data, dict):
            return self._data.items()
        return {}.items()

    def keys(self):
        if isinstance(self._data, dict):
            return self._data.keys()
        return {}.keys()

    def values(self):
        if isinstance(self._data, dict):
            return self._data.values()
        return {}.values()


def get_client(host=CD2_DEFAULT_HOST, port=CD2_DEFAULT_PORT):
    """Lazy singleton getter for CloudDriveClient."""
    if CloudDriveClient._instance is None:
        CloudDriveClient(host, port)
    return CloudDriveClient._instance


class CloudDriveClient:
    """HTTP client that talks to the gRPC proxy.
    Same interface as before - consumers don't need to change."""

    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self, host=CD2_DEFAULT_HOST, port=CD2_DEFAULT_PORT):
        if self._initialized:
            return
        self._initialized = True
        # Ensure proxy is running
        _start_proxy()

    def close(self):
        CloudDriveClient._instance = None

    # ------------------------------------------------------------------
    # PUBLIC methods
    # ------------------------------------------------------------------

    def get_system_info(self, timeout=5):
        return _call("/SystemInfo", timeout=timeout)

    def register(self, username, password, timeout=10):
        return _call("/Register", {"userName": username, "password": password},
                     timeout=timeout)

    def login(self, username, password, timeout=10):
        return _call("/Login", {"userName": username, "password": password},
                     timeout=timeout)

    def get_token(self, username, password, timeout=10):
        return _call("/GetToken", {"userName": username, "password": password},
                     timeout=timeout)

    def logout(self, logout_from_cloud_fs=True, timeout=10):
        return _call("/Logout", {"logoutFromCloudFS": logout_from_cloud_fs},
                     timeout=timeout)

    def is_logged_in(self):
        try:
            info = self.get_system_info(timeout=5)
            return info.IsLogin
        except Exception:
            return False

    def get_runtime_info(self, timeout=5):
        return _call("/RuntimeInfo", timeout=timeout)

    def get_system_settings(self, timeout=5):
        return _call("/SystemSettings", timeout=timeout)

    def set_system_settings(self, settings, timeout=10):
        return _call("/SetSystemSettings", {"settings": settings}, timeout=timeout)

    def get_running_info(self, timeout=5):
        return _call("/RunningInfo", timeout=timeout)

    def get_all_cloud_apis(self, timeout=10):
        return _call("/GetAllCloudApis", timeout=timeout)

    def get_cloud_memberships(self, timeout=10):
        return _call("/GetCloudMemberships", timeout=timeout)

    def can_add_more_cloud_apis(self, timeout=5):
        return _call("/CanAddMoreCloudApis", timeout=timeout)

    def can_mount_both_local_and_cloud(self, timeout=5):
        return _call("/CanAddMoreCloudApis", timeout=timeout)

    # ---------- File Operations ----------

    def get_sub_files(self, path, force_refresh=False, timeout=30):
        return _call("/GetSubFiles",
                     {"path": path, "forceRefresh": force_refresh},
                     timeout=timeout)

    def create_folder(self, parent_path, folder_name, timeout=10):
        return _call("/CreateFolder",
                     {"parentPath": parent_path, "folderName": folder_name},
                     timeout=timeout)

    def delete_file(self, path, timeout=10):
        return _call("/DeleteFile", {"path": path}, timeout=timeout)

    def rename_file(self, path, new_name, timeout=10):
        return _call("/RenameFile",
                     {"path": path, "newName": new_name},
                     timeout=timeout)

    def get_download_url(self, path, timeout=10):
        return _call("/GetDownloadUrl", {"path": path}, timeout=timeout)

    # ---------- Mount Operations ----------

    def get_mount_points(self, timeout=5):
        return _call("/GetMountPoints", timeout=timeout)

    def add_mount_point(self, mount_request, timeout=10):
        # mount_request can be a dict, AttrDict, or have cloudName/mountPath fields
        def _get(key, default):
            try:
                return getattr(mount_request, key)
            except AttributeError:
                return mount_request.get(key, default) if hasattr(mount_request, 'get') else default

        body = {
            "mountPoint": _get("mountPoint", ""),
            "sourceDir": _get("sourceDir", "/"),
            "localMount": _get("localMount", True),
            "autoMount": _get("autoMount", True),
            "name": _get("name", _get("cloudName", "")),
        }
        return _call("/AddMountPoint", body, timeout=timeout)

    def remove_mount_point(self, cloud_name, timeout=10):
        return _call("/RemoveMountPoint", {"cloudName": cloud_name}, timeout=timeout)

    def mount(self, cloud_name, timeout=10):
        return _call("/Mount", {"cloudName": cloud_name}, timeout=timeout)

    def unmount(self, cloud_name, timeout=10):
        return _call("/Unmount", {"cloudName": cloud_name}, timeout=timeout)

    # ---------- Cloud API Management ----------

    def search_files(self, search_for, path="/", fuzzy_match=True, timeout=30):
        return _call("/SearchFiles", {
            "path": path,
            "searchFor": search_for,
            "fuzzyMatch": fuzzy_match,
        }, timeout=timeout)

    def add_webdav(self, server_url, user_name, password, do_not_sync=False, timeout=10):
        return _call("/LoginWebDav", {
            "serverUrl": server_url,
            "userName": user_name,
            "password": password,
            "doNotSyncToCloud": do_not_sync,
        }, timeout=timeout)

    def discover_smb_servers(self, timeout=15):
        return _call("/DiscoverSmbServers", timeout=timeout)

    def discover_smb_shares(self, server, user_name, password, port=445, workgroup="", timeout=15):
        body = {
            "server": server,
            "userName": user_name,
            "password": password,
            "port": port,
        }
        if workgroup:
            body["workgroup"] = workgroup
        return _call("/DiscoverSmbShares", body, timeout=timeout)

    def add_smb(self, server, share, user_name, password, port=445,
                workgroup="", root_path="", do_not_sync=False, timeout=10):
        body = {
            "server": server,
            "share": share,
            "userName": user_name,
            "password": password,
            "port": port,
            "doNotSyncToCloud": do_not_sync,
        }
        if workgroup:
            body["workgroup"] = workgroup
        if root_path:
            body["rootPath"] = root_path
        return _call("/LoginSmb", body, timeout=timeout)

    def remove_cloud_api(self, cloud_name, user_name="", timeout=10):
        return _call("/RemoveCloudAPI", {"cloudName": cloud_name, "userName": user_name, "permanentRemove": True}, timeout=timeout)

    def get_space_info(self, cloud_name, timeout=10):
        return _call("/GetSpaceInfo", {"cloudName": cloud_name}, timeout=timeout)

    def get_cloud_api_config(self, cloud_name, timeout=10):
        return _call("/GetCloudAPIConfig", {"cloudName": cloud_name}, timeout=timeout)

    # ---------- QR Code Login (session-based) ----------

    def qr_login_start(self, provider, timeout=120):
        """Start QR login, returns (first_message, session_id)."""
        resp = _call("/QRLoginStart", {"provider": provider, "timeout": timeout}, timeout=30)
        return resp

    def qr_login_poll(self, session_id, timeout=5):
        """Poll for next QR message. Returns the message or None."""
        resp = _call("/QRLoginPoll", {"session": session_id}, timeout=timeout)
        return resp

    def qr_login_cancel(self, session_id):
        """Cancel QR login session."""
        try:
            _call("/QRLoginCancel", {"session": session_id}, timeout=3)
        except Exception:
            pass

    # ---------- Update ----------

    def has_update(self, timeout=10):
        return _call("/HasUpdate", timeout=timeout)

    def check_update(self, timeout=10):
        return _call("/CheckUpdate", timeout=timeout)

    def download_update(self, timeout=300):
        return _call("/DownloadUpdate", timeout=timeout)

    def update_system(self, timeout=60):
        return _call("/UpdateSystem", timeout=timeout)

    # ---------- System Management ----------

    def get_file_buffer_disk_cache_stats(self, timeout=5):
        raise NotImplementedError("Not yet implemented in proxy")

    def get_service_capabilities(self, timeout=5):
        raise NotImplementedError("Not yet implemented in proxy")

    def get_account_status(self, timeout=10):
        return _call("/GetAccountStatus", timeout=timeout)

    def get_online_devices(self, timeout=10):
        return _call("/GetOnlineDevices", timeout=timeout)
