import json
import logging
from typing import Any, Optional, Union

import requests
from typeguard import typechecked

from psi_grafana import get_logger, log_it


@typechecked
class GrafanaClient:
    """GrafanaClient provides methods for interacting with the Grafana HTTP API.
    Note: this is seperate from the Grafana Cloud API, which allows you to manage resources at
    the organizational level, such as creating a stack and restarting a Grafana instance"""

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://pearson.grafana.net",
        http_session: Union[requests.Session, Any] = None,
        logger: Union[logging.Logger, None] = None,
    ):
        """Initializes a GrafanaClient

        Parameters
        ----------
        api_key : str
            The API key required to authenticate your requests.

        base_url : str
            (optional) The base URL used for API requests. If your server is configured to serve
            from a sub-path, be sure to include that as well.
            (default) https://pearson.grafana.net

        http_session : requests.Session
            (optional) The requests Session used by a GrafanaClient object. If unspecified,
            a new requests.Session will be created.

        logger : logging.Logger
            (optional) The logger used by a GrafanaClient object. If unspecified,
            a new logger will be created.
        """
        assert base_url, f"must include a base URL when initializing {self.__class__.__name__}"
        self.__api_key: str = api_key
        self.__base_url: str = base_url
        self.__http_session = http_session or requests.Session()
        self.__logger: logging.Logger = logger or get_logger()

        self.http_session.headers.update(
            {"Authorization": f"Bearer {self.__api_key}", "Content-Type": "application/json"}
        )

    @property
    def http_session(self) -> Union[requests.Session, Any]:
        return self.__http_session

    @property
    def logger(self) -> logging.Logger:
        return self.__logger

    @log_it
    def get_dashboard(self, uid: str, include_metadata: bool = False) -> dict[str, Any]:
        """https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#get-dashboard-by-uid

        Makes a GET request against Grafana's dashboard HTTP API and returns a dashboard by UID.
        By default, this method will only return the "dashboard" section of the API response
        containing the full dashboard definition JSON.

        If `include_metadata` is included, the full response is returned with the dashboard
        definition and metadata in their own respective objects. See the link for details.

        Parameters
        ----------
        uid : str
            The UID of the dashboard you want to fetch.
            This UID is the string of characters between /d/ and /dashboard-name in the URL.
            E.g. `Bm6lcC3nk` in https://pearson.grafana.net/d/Bm6lcC3nk/well-architected-practices.
            This is also the "uid" property of the dashboard JSON definition.

        include_metadata : bool
            (optional) Whether to include metadata about the dashboard in the request
        """
        self.logger.debug("Fetching dashboard: %s with metadata: %s", uid, include_metadata)
        json_response = self._make_request("get", f"api/dashboards/uid/{uid}")
        if include_metadata:
            return json_response
        return json_response.get("dashboard", {})

    @classmethod
    def get_dashboard_file(cls, file_path: str) -> Any:
        """Reads a dashboard JSON definition from a local file

        Parameters
        ----------
        file_path : str
            Full or relative path to a JSON file containing a dashboard definition
        """
        with open(file_path, "r") as file:
            return json.load(file)

    @log_it
    def create_dashboard(
        self,
        dashboard: dict[str, Any],
        folder_uid: Optional[str] = None,
        message: Optional[str] = None,
    ) -> dict[str, Any]:
        """https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#create--update-dashboard

        Makes a POST request against Grafana's dashboard HTTP API to create a new dashboard.

        If `folder_uid` is not included, the dashboard will be created in the "general" folder.

        Parameters
        ----------
        dashboard : dict
            A full dashboard definition JSON as a dictionary. If your JSON definition contains an
            `id` or `uid` attribute, they will be removed and Grafana will generate new IDs for the
            new dashboard.

        folder_uid : str
            The UID of the folder that you want this dashboard to be created in.
            This UID is the string of characters between /dashboards/f/ and /folder-name in the URL.
            E.g. `fAtzMx3Gk` in https://pearson.grafana.net/dashboards/f/fAtzMx3Gk/pearson-service-insights-workspace-draft-dashboards.

        message : str
            A message to include with the dashboard creation. This can be viewed in the "notes"
            section of the dashboard's version history in Grafana.

        Returns
        -------
        dict:
            {
                "id": 1,
                "uid": "cIBgcSjkk",
                "url": "/d/cIBgcSjkk/production-overview",
                "status": "success",
                "version": 1,
                "slug": "production-overview" //deprecated in Grafana v5.0
            }
        """
        if "id" in dashboard:
            del dashboard["id"]

        if "uid" in dashboard:
            del dashboard["uid"]

        payload = {
            "dashboard": dashboard,
            "folderUid": folder_uid,
            "message": message or "Created by POE Python Grafana client",
            "overwrite": False,
        }

        self.logger.debug("Creating dashboard in folder: %s", folder_uid)
        json_response = self._make_request("post", f"api/dashboards/db", data=payload)
        self.logger.info("New dashboard URL: %s", self.__base_url + json_response.get("url", ""))
        return json_response

    @log_it
    def update_dashboard(
        self,
        dashboard: dict[str, Any],
        folder_uid: Optional[str] = None,
        message: Optional[str] = None,
    ) -> dict[str, Any]:
        """https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#create--update-dashboard

        Makes a POST request against Grafana's dashboard HTTP API to update an new dashboard.

        If `folder_uid` is not included, the dashboard will be moved to the "general" folder.

        Parameters
        ----------
        dashboard : dict
            A full dashboard definition JSON as a dictionary.

        folder_uid : str
            The UID of the folder that you want this dashboard to be created in.
            This UID is the string of characters between /dashboards/f/ and /folder-name in the URL.
            E.g. `fAtzMx3Gk` in https://pearson.grafana.net/dashboards/f/fAtzMx3Gk/pearson-service-insights-workspace-draft-dashboards.

        message : str
            A message to include with the update. This can be viewed in the "notes"
            section of the dashboard's version history in Grafana.

        Returns
        -------
        dict:
            {
                "id": 1,
                "uid": "cIBgcSjkk",
                "url": "/d/cIBgcSjkk/production-overview",
                "status": "success",
                "version": 6,
                "slug": "production-overview" //deprecated in Grafana v5.0
            }
        """
        assert dashboard.get("id") or dashboard.get(
            "uid"
        ), "dashboard must include an id or uid to update"

        payload = {
            "dashboard": dashboard,
            "folderUid": folder_uid,
            "message": message or "Updated by POE Python Grafana client",
            "overwrite": True,
        }

        self.logger.debug("Updating dashboard in folder: %s", folder_uid)
        json_response = self._make_request("post", f"api/dashboards/db", data=payload)
        return json_response

    @log_it
    def get_contact_points(self):
        """https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#get-all-the-contact-points-routegetcontactpoints"""
        return self._make_request("get", "api/v1/provisioning/contact-points")

    @log_it
    def get_response_policies(self):
        """https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#span-idroute-get-policy-treespan-get-the-notification-policy-tree-_routegetpolicytree_"""
        return self._make_request("get", "api/v1/provisioning/policies")

    @log_it
    def _make_request(
        self,
        method: str,
        url: str,
        params: Optional[dict[str, Any]] = None,
        additional_headers: Optional[dict[str, Any]] = None,
        data: Optional[Any] = None,
    ):
        assert method.lower() in [
            "delete",
            "get",
            "head",
            "patch",
            "post",
            "put",
        ], "method must be a valid HTTP method"

        response: requests.Response = self.http_session.request(
            method,
            f"{self.__base_url}/{url}",
            params=params,
            headers=additional_headers,
            data=json.dumps(data),
        )
        response.raise_for_status()
        return response.json()

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}()"
