#!/usr/bin/env python3
"""MSP360 Managed Backup Service (MBS) API client.

Full CRUD over the provider REST API at https://api.mspbackups.com. Auth is a Bearer
token from POST /api/Provider/Login (login+password generated in MSP360 console
Settings > General, vaulted at msp-tools/msp360-api.sops.yaml).

Reuses `.claude/scripts/backup_common.py` for repo-root resolution, the canonical SOPS
vault read, and stdlib JSON HTTP -- same plumbing as the seafile/owncloud/datto-workplace
sibling skills. Standalone: stdlib-only transport.
"""
from __future__ import annotations

import sys
from pathlib import Path
from typing import Any, Optional

# backup_common lives at <root>/.claude/scripts/ ; this file is
# <root>/.claude/skills/msp360/scripts/msp360_client.py -> parents[3] == <root>/.claude
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "scripts"))
import backup_common as bc  # noqa: E402

VAULT_ENTRY = "msp-tools/msp360-api.sops.yaml"
DEFAULT_BASE = "https://api.mspbackups.com"

# Monitoring plan Status codes (observed + MBS convention).
STATUS_TEXT = {
    0: "Success", 1: "Success", 2: "Warning", 3: "Failed",
    4: "Running", 5: "Unknown", 6: "Interrupted", 7: "Completed/warnings",
}
# Statuses that mean "this run did NOT produce a clean backup".
BAD_STATUS = {2, 3, 6, 7}


class Msp360Error(bc.BackupError):
    """MSP360 transport / auth / API error (carries optional HTTP status)."""


class Msp360Client:
    """Thin MSP360 MBS API client: login once, then typed helpers over the endpoints."""

    def __init__(self, base: Optional[str] = None):
        self.base = (base or DEFAULT_BASE).rstrip("/")
        self._token: Optional[str] = None

    # --- auth -----------------------------------------------------------------
    def login(self) -> None:
        login = bc.vault_get_field(VAULT_ENTRY, "credentials.login")
        pw = bc.vault_get_field(VAULT_ENTRY, "credentials.password")
        st, body = bc.http_json(
            "POST", f"{self.base}/api/Provider/Login",
            body={"UserName": login, "Password": pw})
        if st != 200 or not isinstance(body, dict) or not body.get("access_token"):
            raise Msp360Error(f"MSP360 login failed (HTTP {st}): {body}", status=st)
        self._token = body["access_token"]

    def _auth(self) -> dict:
        if not self._token:
            self.login()
        return {"Authorization": "Bearer " + str(self._token)}

    def request(self, method: str, path: str, body: Optional[dict] = None) -> Any:
        """One API call. Raises Msp360Error on any non-2xx (with the API's error body)."""
        st, data = bc.http_json(method, f"{self.base}{path}", headers=self._auth(), body=body)
        if st < 200 or st >= 300:
            raise Msp360Error(f"{method} {path} -> HTTP {st}: {data}", status=st)
        return data

    # --- reads ----------------------------------------------------------------
    def monitoring(self, user_id: Optional[str] = None) -> list:
        return self.request("GET", f"/api/Monitoring/{user_id}" if user_id else "/api/Monitoring")

    def companies(self) -> list:
        return self.request("GET", "/api/Companies")

    def users(self) -> list:
        return self.request("GET", "/api/Users")

    def computers(self, offset: int = 0, count: int = 1000) -> list:
        return self.request("GET", f"/api/Computers/{offset}/{count}")

    def licenses(self, available: Optional[bool] = None) -> list:
        path = "/api/Licenses"
        if available is not None:
            path += f"?isAvailable={'true' if available else 'false'}"
        return self.request("GET", path)

    def billing(self) -> Any:
        return self.request("GET", "/api/Billing")

    # --- writes (companies) ---------------------------------------------------
    def create_company(self, name: str, **extra: Any) -> Any:
        payload = {"Name": name, "StorageLimit": -1}
        payload.update(extra)
        return self.request("POST", "/api/Companies", body=payload)

    def delete_company(self, company_id: str) -> Any:
        return self.request("DELETE", f"/api/Companies/{company_id}")

    # --- writes (users) -------------------------------------------------------
    # Property edits (PUT /api/Users, /api/Companies) are reachable via the CLI's
    # `raw PUT ...` passthrough; not wrapped here until a typed edit command exists.
    def create_user(self, payload: dict) -> Any:
        return self.request("POST", "/api/Users", body=payload)

    def delete_user(self, user_id: str, keep_data: bool = False) -> Any:
        # /{id}/Account = drop metadata but KEEP backup data; /{id} = drop everything.
        suffix = f"/api/Users/{user_id}/Account" if keep_data else f"/api/Users/{user_id}"
        return self.request("DELETE", suffix)

    # --- writes (licenses) ----------------------------------------------------
    def grant_license(self, payload: dict) -> Any:
        return self.request("POST", "/api/Licenses/Grant", body=payload)

    def release_license(self, payload: dict) -> Any:
        return self.request("POST", "/api/Licenses/Release", body=payload)

    def revoke_license(self, payload: dict) -> Any:
        return self.request("POST", "/api/Licenses/Revoke", body=payload)
