#!/usr/bin/env python3

from __future__ import annotations

import argparse
import json
import os
import sys
from datetime import UTC, date, datetime, timedelta
from pathlib import Path
from typing import Any
from urllib.parse import quote, urlparse, urlunparse

import cmk.utils.password_store
import requests
import urllib3
from urllib3.exceptions import InsecureRequestWarning
from cmk.utils.paths import tmp_dir

urllib3.disable_warnings(InsecureRequestWarning)

SECTIONS = [
    "cluster_system_status",
    "cluster_compliance_status",
    "bandwidth",
    "node_status",
    "node_disk_status",
    "node_hardware_health",
    "storage",
    "cluster_event_logwatch",
]

LIST_CLUSTERS_QUERY = """
query ListClusters($first: Int!, $after: String, $filter: ClusterFilterInput) {
  clusterConnection(first: $first, after: $after, filter: $filter) {
    count
    nodes {
      id
      name
      defaultAddress
      version
      status
      systemStatus
      isHealthy
      estimatedRunway
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
"""

CLUSTER_SUMMARY_QUERY = """
query ClusterSummary($clusterUuid: UUID!) {
  cluster(clusterUuid: $clusterUuid) {
    id
    name
    status
    systemStatus
    systemStatusMessage
    systemStatusAffectedNodes {
      id
      hostname
    }
  }
}
"""

CLUSTER_NODES_STATUS_QUERY = """
query ClusterNodesStatus($clusterUuid: String!) {
  clusterNodes(input: {clusterUuid: $clusterUuid}) {
    data {
      id
      hostname
      ipAddress
      brikId
      status
      hasUnavailableDisks
    }
  }
}
"""

CLUSTER_NODE_HEALTH_QUERY = """
query ClusterNodeHealth($clusterUuid: UUID!, $first: Int!, $after: String) {
  cluster(clusterUuid: $clusterUuid) {
    clusterNodeConnection(first: $first, after: $after) {
      nodes {
        id
        hostname
        hardwareHealth {
          policyName
          isHealthy
          message
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
"""

CLUSTER_DISKS_QUERY = """
query ClusterDisks($clusterUuid: UUID!, $first: Int!, $after: String) {
  cluster(clusterUuid: $clusterUuid) {
    clusterDiskConnection(first: $first, after: $after) {
      nodes {
        diskId
        nodeId
        path
        capacityBytes
        unallocatedBytes
        isEncrypted
        status
        raidStatus
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
"""

CLUSTER_COMPLIANCE_QUERY = """
query ClusterCompliance($clusterUuid: UUID!, $first: Int!, $after: String) {
  snappableConnection(
    first: $first
    after: $after
    filter: {
      cluster: {id: [$clusterUuid]}
      isLocal: true
    }
  ) {
    count
    nodes {
      complianceStatus
      awaitingFirstFull
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
"""

CLUSTER_BANDWIDTH_QUERY = """
query ClusterBandwidth($clusterUuid: String!, $range: String!) {
  replicationOutgoingStats(input: {clusterUuid: $clusterUuid, range: $range}) {
    items {
      stat
      time
    }
  }
}
"""

CLUSTER_EVENT_QUERY = """query EventSeriesListQuery($after: String, $filters: ActivitySeriesFilter, $first: Int, $sortBy: ActivitySeriesSortField, $sortOrder: SortOrder) {
  activitySeriesConnection(
    after: $after
    first: $first
    filters: $filters
    sortBy: $sortBy
    sortOrder: $sortOrder
  ) {
    edges {
      cursor
      node {
        ...EventSeriesFragment
        cluster {
          id
          name
          timezone
          __typename
        }
        activityConnection(first: 1) {
          nodes {
            id
            message
            __typename
          }
          __typename
        }
        __typename
      }
      __typename
    }
    pageInfo {
      endCursor
      hasNextPage
      hasPreviousPage
      __typename
    }
    __typename
  }
}

fragment EventSeriesFragment on ActivitySeries {
  id
  fid
  activitySeriesId
  lastUpdated
  lastActivityType
  lastActivityStatus
  objectId
  objectName
  objectType
  severity
  progress
  isCancelable
  isPolarisEventSeries
  location
  effectiveThroughput
  dataTransferred
  logicalSize
  slaDomainName
  organizations {
    id
    name
    __typename
  }
  clusterUuid
  clusterName
  username
  urlMetadata
  __typename
}
"""

class RubrikApiError(RuntimeError):
    pass


def _resolve_secret_reference(secret: str) -> str:
    if ":/" not in secret:
        return secret

    password_id, password_file = secret.split(":", 1)
    password_store_file = Path(password_file)
    if not password_id or not password_store_file.is_absolute() or not password_store_file.exists():
        return secret

    try:
        return cmk.utils.password_store.lookup(password_store_file, password_id)
    except ValueError as exc:
        raise RubrikApiError(f"Unable to resolve password store reference {secret}: {exc}") from exc


class RubrikRscApi:
    def __init__(
        self,
        hostname: str | None,
        client_id: str,
        client_secret: str,
        access_token_uri: str,
        *,
        cluster_name: str | None = None,
        cluster_uuid: str | None = None,
        verify: bool = False,
        timeout: int = 30,
        page_size: int = 200,
    ) -> None:
        self.hostname = hostname
        self.cluster_name = cluster_name
        self.cluster_uuid = cluster_uuid
        self.access_token_uri = access_token_uri.rstrip("/")
        self.rsc_base_url = self._derive_rsc_base_url(self.access_token_uri)
        self.graphql_url = f"{self.rsc_base_url}/api/graphql"
        self.verify = verify
        self.client_id = client_id
        self.client_secret = _resolve_secret_reference(client_secret)
        self.token: str | None = None
        self.session = requests.Session()
        self.session.headers.update({"accept": "application/json", "User-Agent": "CheckmkRubrikAgent/1.5.8"})
        self.selected_cluster: dict[str, Any] | None = None
        self.timeout = timeout
        self.page_size = page_size
        self._pending_event_states: dict[Path, tuple[str, dict[str, Any]]] = {}

        if not self.verify:
            urllib3.disable_warnings(InsecureRequestWarning)
    
    def overwrite_cluster(self, cluster_name: str, cluster_uuid: str) -> None:
        self.cluster_name = cluster_name
        self.cluster_uuid = cluster_uuid

    @property
    def _event_state_directory(self) -> Path:
        """Separate event state by querying host and currently resolved cluster."""
        host = quote(self.hostname or "standalone", safe="")
        cluster = quote(self.selected_cluster_uuid, safe="")
        return tmp_dir / "agents" / "agent_rubrik" / f"host-{host}" / f"cluster-{cluster}"

    @property
    def last_checked_file(self) -> Path:
        """Return the event record path for the currently resolved cluster."""
        return self._event_state_directory / "last_checked.txt"

    @property
    def last_time_file(self) -> Path:
        """Return the timestamp path for the currently resolved cluster."""
        return self._event_state_directory / "last_time.txt"

    @staticmethod
    def _derive_rsc_base_url(access_token_uri: str) -> str:
        parsed = urlparse(access_token_uri)
        if not parsed.scheme or not parsed.netloc:
            raise RubrikApiError(f"Invalid access token URI: {access_token_uri}")

        return urlunparse((parsed.scheme, parsed.netloc, "", "", "", ""))

    def login(self) -> None:
        try:
            response = self.session.post(
                url=self.access_token_uri,
                json={
                    "grant_type": "client_credentials",
                    "client_id": self.client_id,
                    "client_secret": self.client_secret,
                },
                headers={
                    "accept": "application/json",
                    "content-type": "application/json",
                },
                verify=self.verify,
                timeout=self.timeout,
            )
            response.raise_for_status()
        except requests.RequestException as exc:
            raise RubrikApiError(f"RSC token request failed: {exc}") from exc

        try:
            response_json = response.json()
        except ValueError as exc:
            raise RubrikApiError("RSC token request returned invalid JSON") from exc

        if not (access_token := response_json.get("access_token")):
            details = response_json.get("message") or response_json.get("error_description") or response_json
            raise RubrikApiError(f"RSC token request did not return an access token: {details}")

        self.token = access_token
        self.session.headers.update({"Authorization": f"Bearer {access_token}"})

    def delete_token(self) -> int:
        if not self.token:
            self.session.close()
            return 0

        try:
            response = self.session.delete(
                url=f"{self.rsc_base_url}/api/session",
                verify=self.verify,
                timeout=self.timeout,
            )
            if response.status_code not in (200, 204):
                sys.stderr.write(
                    f"Token delete error: unexpected status code {response.status_code} from {self.rsc_base_url}/api/session\n"
                )
                return 1
        except requests.RequestException as exc:
            sys.stderr.write(f"Token delete error: {exc}\n")
            return 1
        finally:
            self.token = None
            self.session.close()

        return 0

    def graphql(self, query: str, variables: dict[str, Any] | None = None) -> dict[str, Any]:
        try:
            response = self.session.post(
                url=self.graphql_url,
                json={"query": query, "variables": variables or {}},
                headers={
                    "accept": "application/json",
                    "content-type": "application/json",
                },
                verify=self.verify,
                timeout=self.timeout,
            )
            response.raise_for_status()
        except requests.RequestException as exc:
            raise RubrikApiError(f"GraphQL request failed: {exc}") from exc

        try:
            payload = response.json()
        except ValueError as exc:
            raise RubrikApiError("GraphQL request returned invalid JSON") from exc

        if errors := payload.get("errors"):
            raise RubrikApiError(f"GraphQL request returned errors: {errors}")

        return payload.get("data", {})

    def list_clusters(self, filter_input: dict[str, Any] | None = None) -> list[dict[str, Any]]:
        clusters: list[dict[str, Any]] = []
        after: str | None = None

        while True:
            variables: dict[str, Any] = {"first": self.page_size, "after": after}
            if filter_input:
                variables["filter"] = filter_input

            connection = self.graphql(LIST_CLUSTERS_QUERY, variables).get("clusterConnection") or {}
            clusters.extend(connection.get("nodes", []))

            page_info = connection.get("pageInfo") or {}
            if not page_info.get("hasNextPage"):
                break
            after = page_info.get("endCursor")
            if not after:
                break

        return clusters

    def resolve_cluster(self) -> dict[str, Any]:
        if self.cluster_uuid:
            matches = self.list_clusters({"id": [self.cluster_uuid]})
        elif self.cluster_name:
            matches = self.list_clusters({"name": [self.cluster_name]})
        elif self.hostname:
            short_name = self.hostname.split(".", 1)[0]
            candidates = {self.hostname, short_name, short_name.upper(), short_name.lower()}
            matches = [
                cluster
                for cluster in self.list_clusters()
                if cluster.get("name") in candidates
            ]
        else:
            matches = []

        if not matches:
            selector = self.cluster_uuid or self.cluster_name or self.hostname or "<none>"
            raise RubrikApiError(f"No cluster found in RSC matching selector {selector}")
        if len(matches) > 1:
            details = ", ".join(f"{item.get('name')} ({item.get('id')})" for item in matches)
            raise RubrikApiError(f"Multiple clusters matched selector {self.cluster_uuid or self.cluster_name or self.hostname}: {details}")

        self.selected_cluster = matches[0]
        return matches[0]

    @property
    def selected_cluster_uuid(self) -> str:
        if not self.selected_cluster:
            raise RubrikApiError("No Rubrik cluster has been resolved yet")
        return str(self.selected_cluster["id"])

    def get_cluster_system_status(self) -> dict[str, Any]:
        cluster = self.graphql(CLUSTER_SUMMARY_QUERY, {"clusterUuid": self.selected_cluster_uuid}).get("cluster") or {}
        return {
            "status": cluster.get("systemStatus") or "UNKNOWN",
            "message": cluster.get("systemStatusMessage") or "",
            "affectedNodeIds": [
                node.get("hostname") or node.get("id")
                for node in cluster.get("systemStatusAffectedNodes") or []
                if node.get("hostname") or node.get("id")
            ],
        }

    def get_cluster_compliance_summary(self) -> dict[str, Any]:
        total_protected = 0
        awaiting_first_full = 0
        in_compliance = 0
        out_of_compliance = 0
        after: str | None = None

        while True:
            payload = self.graphql(
                CLUSTER_COMPLIANCE_QUERY,
                {"clusterUuid": self.selected_cluster_uuid, "first": self.page_size, "after": after},
            ).get("snappableConnection") or {}

            total_protected = payload.get("count", total_protected)
            for snappable in payload.get("nodes") or []:
                if snappable.get("awaitingFirstFull"):
                    awaiting_first_full += 1
                status = (snappable.get("complianceStatus") or "").upper()
                if status == "IN_COMPLIANCE":
                    in_compliance += 1
                elif status == "OUT_OF_COMPLIANCE":
                    out_of_compliance += 1

            page_info = payload.get("pageInfo") or {}
            if not page_info.get("hasNextPage"):
                break
            after = page_info.get("endCursor")
            if not after:
                break

        safe_total = total_protected or 1
        percent_in = round((in_compliance / safe_total) * 100, 2) if total_protected else 0.0
        percent_out = round((out_of_compliance / safe_total) * 100, 2) if total_protected else 0.0

        return {
            "totalProtected": total_protected,
            "numberAwaitingFirstFull": awaiting_first_full,
            "numberOfInComplianceSnapshots": in_compliance,
            "percentInCompliance": percent_in,
            "numberOfOutOfComplianceSnapshots": out_of_compliance,
            "percentOutOfCompliance": percent_out,
        }

    def get_cluster_bandwidth(self) -> dict[str, Any]:
        payload = self.graphql(
            CLUSTER_BANDWIDTH_QUERY,
            {"clusterUuid": self.selected_cluster_uuid, "range": "-1h"},
        ).get("replicationOutgoingStats") or {}
        samples = payload.get("items") or []
        values = []
        for sample in samples:
            try:
                values.append(int(sample.get("stat") or 0))
            except (TypeError, ValueError):
                continue
        average = int(sum(values) / len(values)) if values else 0
        return {"archiveAvgBytesPerSecondLastHour": average}

    def get_cluster_storage(self) -> dict[str, Any]:
        cluster = self.selected_cluster
        if cluster is None:
            raise RubrikApiError("No Rubrik cluster has been resolved yet")

        runway = cluster.get("estimatedRunway")
        return {
            "runwayRemaining": int(runway) if runway is not None else None,
        }

    def get_cluster_nodes_status(self) -> list[dict[str, Any]]:
        data = self.graphql(CLUSTER_NODES_STATUS_QUERY, {"clusterUuid": self.selected_cluster_uuid}).get("clusterNodes") or {}
        return list(data.get("data") or [])

    def get_cluster_disk_status(self) -> list[dict[str, Any]]:
        disks: list[dict[str, Any]] = []
        after: str | None = None

        while True:
            cluster = self.graphql(
                CLUSTER_DISKS_QUERY,
                {"clusterUuid": self.selected_cluster_uuid, "first": self.page_size, "after": after},
            ).get("cluster") or {}
            connection = cluster.get("clusterDiskConnection") or {}
            for disk in connection.get("nodes") or []:
                raid_status = (disk.get("raidStatus") or "NONE").upper()
                disks.append(
                    {
                        "id": disk.get("diskId"),
                        "nodeId": disk.get("nodeId"),
                        "path": disk.get("path"),
                        "capacityBytes": disk.get("capacityBytes") or 0,
                        "unallocatedBytes": disk.get("unallocatedBytes") or 0,
                        "isEncrypted": disk.get("isEncrypted", False),
                        "status": disk.get("status") or "UNKNOWN",
                        "raidStatus": raid_status,
                        "isDegraded": raid_status in {"DEGRADED", "OFFLINE", "READY_TO_REBUILD", "REBUILDING"},
                    }
                )

            page_info = connection.get("pageInfo") or {}
            if not page_info.get("hasNextPage"):
                break
            after = page_info.get("endCursor")
            if not after:
                break

        return disks

    def get_cluster_node_hardware_health(self) -> list[dict[str, Any]]:
        nodes: list[dict[str, Any]] = []
        after: str | None = None

        while True:
            cluster = self.graphql(
                CLUSTER_NODE_HEALTH_QUERY,
                {"clusterUuid": self.selected_cluster_uuid, "first": self.page_size, "after": after},
            ).get("cluster") or {}
            connection = cluster.get("clusterNodeConnection") or {}
            for node in connection.get("nodes") or []:
                nodes.append(
                    {
                        "id": node.get("id"),
                        "hostname": node.get("hostname"),
                        "policies": [
                            {
                                "policyName": policy.get("policyName"),
                                "isHealthy": policy.get("isHealthy", True),
                                "message": policy.get("message") or "",
                            }
                            for policy in node.get("hardwareHealth") or []
                        ],
                    }
                )

            page_info = connection.get("pageInfo") or {}
            if not page_info.get("hasNextPage"):
                break
            after = page_info.get("endCursor")
            if not after:
                break

        return nodes

    def save_last_check(
        self,
        timestamp: str,
        event: dict[str, Any],
        *,
        directory: Path | None = None,
    ) -> None:
        """Persist an event cursor in its host and cluster namespace."""
        try:
            state_directory = directory if directory is not None else self._event_state_directory
            state_directory.mkdir(parents=True, exist_ok=True)
            with open(state_directory / "last_time.txt", "w", encoding="utf-8") as f:
                f.write(timestamp)
            with open(state_directory / "last_checked.txt", "w", encoding="utf-8") as f:
                json.dump(event, f, ensure_ascii=False)
        except Exception:
            pass

    def commit_event_states(self) -> None:
        """Persist fetched cursors only after the complete output has been flushed."""
        for directory, (timestamp, event) in self._pending_event_states.items():
            self.save_last_check(timestamp, event, directory=directory)
        self._pending_event_states.clear()
    
    def get_last_check(self) -> tuple[Any, str]:

        data_obj = {}
        try:
            with open(self.last_checked_file, "r", encoding="utf-8") as f:
                try:
                    data_obj = json.load(f)
                except Exception:
                    pass
            with open(self.last_time_file, "r", encoding="utf-8") as f:
                date_str = f.readline()
                date_obj = _parse_event_timestamp(date_str).date()

                if date_obj == date.today() or date_obj == date.today() - timedelta(days=1):
                    return (data_obj, date_str)
                else:
                    return (data_obj, (date.today() - timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z")
        except Exception:
            return ({}, (date.today() - timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z")
    
    def get_cluster_events(self, last_activity_types: list | None, severity: list | None, lastactivitystatus: list | None, objecttype: list | None) -> list[dict[str, Any]]:
        events: list[dict[str, Any]] = []
        after: str | None = None
        
        last_data, last_updated = self.get_last_check()
        filters: dict[str, Any] = {
            "clusterId": self.selected_cluster_uuid,
            "lastUpdatedTimeGt": last_updated,
        }
        last_entry = {}
        newest_timestamp = _parse_event_timestamp(last_updated)

        if last_activity_types:
            filters["lastActivityType"] = last_activity_types
        if lastactivitystatus:
            filters["lastActivityStatus"] = lastactivitystatus
        if objecttype:
            filters["objectType"] = objecttype
        if severity:
            filters["severity"] = severity


        while True:
            result = self.graphql(
                CLUSTER_EVENT_QUERY,
                {
                    "filters": filters,
                    "first": self.page_size,
                    "after": after,
                },
            )
            connection = result.get("activitySeriesConnection") or {}
            for node in connection.get("edges") or []:
                entry = (node or {}).get("node")
                if not entry:
                    continue
                events.append(
                    {
                        "id": entry.get("activitySeriesId"),
                        "last_updated": entry.get("lastUpdated"),
                        "type": entry.get("lastActivityType"),
                        "status": entry.get("lastActivityStatus"),
                        "object_name": entry.get("objectName"),
                        "severity": entry.get("severity"),
                        "object_type": entry.get("objectType"),
                        "message": entry.get("activityConnection"),
                    }
                )
                if entry.get("lastUpdated"):
                    lastUpdated = _parse_event_timestamp(entry["lastUpdated"])
                    if lastUpdated > newest_timestamp:
                        newest_timestamp = lastUpdated
                        last_entry = {
                        "id": entry.get("activitySeriesId"),
                        "last_updated": entry.get("lastUpdated"),
                        "type": entry.get("lastActivityType"),
                        "status": entry.get("lastActivityStatus"),
                        "object_name": entry.get("objectName"),
                        "severity": entry.get("severity"),
                        "object_type": entry.get("objectType"),
                        "message": entry.get("activityConnection"),
                    }
            page_info = connection.get("pageInfo") or {}
            if not page_info.get("hasNextPage"):
                break
            after = page_info.get("endCursor")
            if not after:
                break
        if newest_timestamp != _parse_event_timestamp(last_updated):
            self._pending_event_states[self._event_state_directory] = (
                newest_timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
                last_entry,
            )
        return [] if len(events) == 1 and events[0] == last_data else events

def _parse_event_timestamp(timestamp: str) -> datetime:
    """Accept ISO-8601 fractions and offsets, treating naive timestamps as UTC."""
    parsed = datetime.fromisoformat(timestamp.strip())
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=UTC)
    return parsed.astimezone(UTC)


def _node_piggyback_name(node_id: object) -> str:
    """Use the same local node ID for status, disk and hardware health data."""
    name = str(node_id or "").rsplit(":::", 1)[-1]
    if not name:
        message = "Rubrik node data is missing its node ID"
        raise RubrikApiError(message)
    return name


def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--client-id", help="RSC client ID used for token generation", required=True)
    parser.add_argument("--client-secret", help="RSC client secret used for token generation", required=True)
    parser.add_argument(
        "--access-token-uri",
        help="RSC access token URI, for example https://example.my.rubrik.com/api/client_token",
        required=True,
    )
    parser.add_argument("--cluster-name", help="Optional cluster name in RSC used to resolve the monitored cluster")
    parser.add_argument("--cluster-uuid", help="Optional cluster UUID in RSC used to resolve the monitored cluster")
    parser.add_argument("--hostname", help="Optional Checkmk hostname fallback used to infer the Rubrik cluster name")
    parser.add_argument(
        "--page-size",
        type=int,
        default=200,
        help="Number of items to fetch per page for paginated queries (default: %(default)s)",
    )
    parser.add_argument(
        "--saas",
        help="If SaaS use all clusters as piggyback",
        action="store_true",
    )
    parser.add_argument(
        "--event-lastactivitytype",
        type=lambda s: s.split(","),
        help="Event filter to query. This is a comma separated list.",
    )
    parser.add_argument(
        "--event-severity",
        type=lambda s: s.split(","),
        help="Event filter to query. This is a comma separated list.",
    )
    parser.add_argument(
        "--event-lastactivitystatus",
        type=lambda s: s.split(","),
        help="Event filter to query. This is a comma separated list.",
    )
    parser.add_argument(
        "--event-objecttype",
        type=lambda s: s.split(","),
        help="Event filter to query. This is a comma separated list.",
    )
    parser.add_argument(
        "--timeout",
        default=30,
        type=int,
        help="Timeout value for each query (default: %(default)s)",
    )
    parser.add_argument(
        "--list-clusters",
        help="List all clusters visible in RSC and exit",
        action="store_true",
    )
    parser.set_defaults(verify_ssl=True)
    verify_ssl_group = parser.add_mutually_exclusive_group()
    verify_ssl_group.add_argument(
        "--verify_ssl",
        dest="verify_ssl",
        help="Enable SSL certificate verification (default)",
        action="store_true",
    )
    verify_ssl_group.add_argument(
        "--no-verify-ssl",
        dest="verify_ssl",
        help="Disable SSL certificate verification",
        action="store_false",
    )
    parser.add_argument(
        "--sections",
        type=lambda s: s.split(","),
        default=SECTIONS,
        help=f"Sections to query. This is a comma separated list of {', '.join(SECTIONS)}. Default is to query all sections.",
    )
    return parser.parse_args(argv)


def _print_clusters(clusters: list[dict[str, Any]]) -> None:
    for cluster in clusters:
        print(cluster)

def resolve_sections(args: argparse.Namespace, api: RubrikRscApi) -> None:
    if "cluster_system_status" in args.sections:
        print("<<<rubrik_cluster_system_status:sep(0)>>>")
        print(api.get_cluster_system_status())

    if "cluster_compliance_status" in args.sections:
        print("<<<rubrik_cluster_compliance_24_hours:sep(0)>>>")
        print(api.get_cluster_compliance_summary())
    
    if "bandwidth" in args.sections:
        print("<<<rubrik_bandwidth:sep(0)>>>")
        print(api.get_cluster_bandwidth())

    if "storage" in args.sections:
        print("<<<rubrik_storage:sep(0)>>>")
        print(api.get_cluster_storage())

    if "node_status" in args.sections:
        for node in api.get_cluster_nodes_status():
            print(f"<<<<{_node_piggyback_name(node.get('id'))}>>>>")
            print("<<<rubrik_node:sep(0)>>>")
            print(node)
            print("<<<<>>>>")

    if "node_disk_status" in args.sections:
        for disk in api.get_cluster_disk_status():
            print(f"<<<<{_node_piggyback_name(disk.get('nodeId'))}>>>>")
            print("<<<rubrik_node_disk:sep(0)>>>")
            print(disk)
            print("<<<<>>>>")

    if "node_hardware_health" in args.sections:
        for node in api.get_cluster_node_hardware_health():
            print(f"<<<<{_node_piggyback_name(node.get('id'))}>>>>")
            print("<<<rubrik_node_hardware_health:sep(0)>>>")
            print(node)
            print("<<<<>>>>")

def resolve_cluster_section(args: argparse.Namespace, api: RubrikRscApi) -> None:
    if "cluster_event_logwatch" in args.sections:
        status_map = {
            "info": "O",
            "warning": "W",
            "critical": "C",
            "fallback": "u",
        }
        print("<<<logwatch>>>")
        print(f"[[[Rubrik Events - {api.cluster_name}]]]")
        for data in api.get_cluster_events(args.event_lastactivitytype, args.event_severity, args.event_lastactivitystatus, args.event_objecttype):
            if len(data) != 0:
                for message in (data.get('message') or {}).get('nodes') or []:
                    if message is not None:
                        severity = (data.get('severity') or '').lower()
                        print(f"{status_map.get(severity, status_map['fallback'])} {message.get('message') or ''} ")
        print("<<<<>>>>")

def main(argv: list[str] | None = None) -> int:
    args = parse_arguments(argv)

    if not args.event_lastactivitytype and not args.event_severity and not args.event_lastactivitystatus and not args.event_objecttype and "cluster_event_logwatch" in args.sections:
        args.sections.remove("cluster_event_logwatch")

    try:
        api = RubrikRscApi(
            hostname=args.hostname,
            client_id=args.client_id,
            client_secret=args.client_secret,
            access_token_uri=args.access_token_uri,
            cluster_name=args.cluster_name,
            cluster_uuid=args.cluster_uuid,
            verify=args.verify_ssl,
            timeout=args.timeout,
            page_size=args.page_size,
        )
    except RubrikApiError as exc:
        sys.stderr.write(f"{exc}\n")
        return 1

    exit_code = 0
    try:
        api.login()

        if args.list_clusters:
            clusters = api.list_clusters()
            if not clusters:
                raise RubrikApiError("No clusters are visible in RSC for this service account")
            _print_clusters(clusters)
            return 0

        if args.saas:
            clusters = api.list_clusters()
            for cluster in clusters:
                api.overwrite_cluster(cluster.get('name', ''), cluster.get('id', ''))
                api.resolve_cluster()
                print(f"<<<<{api.cluster_name}>>>>")
                resolve_sections(args, api)
                print("<<<<>>>>")
                resolve_cluster_section(args, api)
        else:
            api.resolve_cluster()
            resolve_sections(args, api)
            resolve_cluster_section(args, api)

        sys.stdout.flush()

    except RubrikApiError as exc:
        sys.stderr.write(f"{exc}\n")
        exit_code = 1
    except Exception as exc:
        sys.stderr.write(f"Unexpected error: {exc}\n")
        exit_code = 1
    finally:
        logout_rc = api.delete_token()

    if not (exit_code or logout_rc):
        api.commit_event_states()

    return exit_code or logout_rc


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
