import requests
import logging
import os

from poe_new_relic import get_logger, log_it
from typing import Any, Dict, List

NERDGRAPH_MAX_TIMEOUT_MS = 30 * 60 * 1000  # 30 mins
NEWRELIC_GRAPHQL_URL = os.environ.get("NERDGRAPH_URL", "https://api.newrelic.com/graphql")


class NewRelic:
    def __init__(
        self,
        api_key: str,
        base_url: str = None,
        http_session: requests.Session = requests.Session(),
        logger: logging.Logger = None,
    ):
        self._http_session = http_session
        self.__logger = logger or get_logger()
        self._base_url = base_url or NEWRELIC_GRAPHQL_URL
        self._headers = {
            "API-Key": api_key,
        }

    @log_it
    def execute_nerdgraph_query(
        self,
        query: str,
        variables: str = None,
        timeout_ms: int = None,
    ) -> Dict[str, Any]:
        """Execute a NerdGraph query.

        Parameters
        ----------
        query : str
            The NerdGraph query to execute
        variables : str
            (optional) Variables to augment the NerdGraph query
        timeout_ms : int
            (optional) How long (in milliseconds) the query should run before NerdGraph gives up.
            If unset, NerdGraph uses the default of 5 seconds.

        Returns
        -------
        Dict[str, Any]
            The NerdGraph response as a dictionary. 
        """
        if timeout_ms is not None:
            assert (
                0 < timeout_ms <= NERDGRAPH_MAX_TIMEOUT_MS
            ), f"NerdGraph query timeout ms must be in the range [0, {NERDGRAPH_MAX_TIMEOUT_MS})"

        try:
            full_query = {"query": query}
            if variables:
                full_query["variables"] = variables
            if timeout_ms:
                full_query["timeout"] = timeout_ms

            resp = self._http_session.post(
                url=self._base_url, headers=self._headers, json=full_query
            )
            resp.raise_for_status()

            resp_json = resp.json() or {}
            if resp_json.get("errors"):
                err_messages = [err["message"] for err in resp_json["errors"] if err.get("message")]
                raise Exception("\n".join(err_messages))
            return resp_json
        except Exception as e:
            self.__logger.error(f"Failed to execute NerdGraph query with error: {e}")
            raise e

    def nerdgraph_nrql_search(
        self,
        account_id: int,
        nrql: str = None,
        label: str = "entities",
        nrqls: Dict[str, str] = None,
        timeout_ms: int = 10,  # Defaults to 10 seconds
    ) -> Dict[str, dict]:
        """Run a NerdGraph NRQL search by either specifying a single NRQL query to run or a map of
        label -> NRQL query to run (allows running multiple NRQL queries at once).

        Parameters
        ----------
        account_id : int
            The NewRelic account to run the NRQL query in.
        nrql : str
            (conditional) The NRQL query string to run. Must be set if the 'nrqls' parameter isn't.
        label : str
            (optional) The label given to the NRQL query specified by the 'nrql' parameter.
            Does not apply to the 'nrqls' parameter.
        nrqls : Dict[str, str]
            (conditional) A mapping of label -> NRQL query allowing for running multiple NRQL
            queries at once. E.g.

            {
                "apmAppNames": "SELECT uniques(appName) FROM Transaction SINCE 1 hour ago",
                "browserAppNames": "SELECT uniques(appName) FROM BrowserInteraction SINCE 1 hour ago"
            }
        timeout_ms : int
            (optional) How long (in milliseconds) the query should run before NerdGraph gives up.
            If unset, NerdGraph uses the default of 5 seconds.

        Returns
        -------
        Dict[str, dict]
            The NerdGraph response as a dictionary with the form:

            {
                "exampleLabel1": {
                    "results": [<list of result dicts>]
                },
                "exampleLabel2": {
                    "results": [<list of result dicts>]
                }
            }
        """
        assert bool(nrql) != bool(
            nrqls
        ), "To run a NerdGraph NRQL search, must specify exactly one of nrql or nrqls"
        if nrql:
            nrqls = {label: nrql}

        self.__logger.info(f"Running NerdGraph NRQL search with queries: {nrqls}")
        try:
            nrqls = ",".join(
                [f'{k.strip()}: nrql(query: "{v} timeout: {timeout_ms}") {{results}}' for k, v in nrqls.items()]
            )
            query = f"{{actor {{account(id: {account_id}) {{{nrqls}}}}}}}"
            resp_json = self.execute_nerdgraph_query(query, timeout_ms=timeout_ms)
            return resp_json.get("data", {}).get("actor", {}).get("account", {})
        except Exception as e:
            self.__logger.error(f"Failed to run NRQL query with error: {e}")
            raise e

    def nerdgraph_entitysearch(
        self,
        query: str,
        output_keys: str,
        on: str = "",
        timeout_ms: int = None,
    ) -> Dict:
        """_summary_
            query New Relic's Nerdgraph api, https://api.newrelic.com/graphiql
        Args:
            query (str): the SELECT section of the NRQL query
            on (str): appears to be a sub filtering of what to return and contains proprietary return values for what you pick
            output_keys (str): the keys to return values for

        Returns:
            Dict: dynamic results defined by the output_keys arg
        """
        self.__logger.info(f"Running entity search with query: {query}")
        entities, next_cursor = [], ""
        while True:
            try:
                if next_cursor:
                    next_cursor = f'(cursor: "{next_cursor}")'
                # TODO: allow for multiple 'on' values to be used and built out in the query string like in the nerdgraph explorer
                resp_json = self.execute_nerdgraph_query(
                    query=f"{{actor {{{query} {{results{next_cursor} {{entities {{{on} {output_keys}}}nextCursor}}}}}}}}",
                    timeout_ms=timeout_ms,
                )
                results = resp_json["data"]["actor"]["entitySearch"]["results"]
                entities.extend(results.get("entities"))
                self.__logger.debug(f"Total entities: {len(entities)}")
                next_cursor = results.get("nextCursor", None)
                if not next_cursor:
                    self.__logger.info(
                        f"Completed entity search, found {len(entities)} entity/entities"
                    )
                    break
            except Exception as e:
                self.__logger.error(f"Failed to run entity search with error: {e}")
                raise e
        return entities

    def get_synths(self) -> List:
        """_summary_
            cursors over Nerdgraph's entitySearch query results to return every synthetic
        Raises:
            e: logs the response back from new relic regarding the query attempt and raises the actual error
        Returns:
            List: the combined cursored results of all of the synthetics
        """
        entity_search = "entitySearch(query: \"domain = 'SYNTH' AND type = 'MONITOR'\")"
        on = "... on SyntheticMonitorEntityOutline"
        output_keys = "{guid name tags {key values} monitorId}"
        resp = self.nerdgraph_entitysearch(entity_search, output_keys, on)
        return resp

    def get_monitorid_and_guids_synths_by_tag(self, tag: str) -> Dict:
        """_summary_
            groups monitorIds and guids by tag in an array
        Args:
            tag (str): tag of an entity

        Returns:
            Dict: containing per tag grouping of an array of monitorIds and array of guids
        """
        wb_tagged = []
        all_synths = self.get_synths()
        # get only synthetics having said tag
        wb_tagged.extend(
            [
                i
                for e in all_synths
                for i in [",".join(t["values"]) for t in e.get("tags") if t["key"] == tag]
            ]
        )
        self.__logger.info(f"Found {len(wb_tagged)} entities with tag {tag}")
        # geting distinct set of tags
        set_tags = set(wb_tagged)
        # reducing the  keys down grabbing monitorIds and guids per tag
        panel_entities = [
            {t: {"monitorId": e.get("monitorId"), "guid": e.get("guid")}}
            for e in all_synths
            for t in set_tags
            for l in [w for w in e.get("tags") if t in w["values"]]
        ]
        # grouping tags to array of dicts, {tag:[{monitorId:value, guid:value}]}
        panel_entities = {
            k: [d.get(k) for d in panel_entities if k in d] for k in set().union(*panel_entities)
        }
        # creating arrays per monitorId and per guid per tag, {tag:[{monitorId:[values]},{guid:[values]}]} TODO change array of dicts for tags to single dict with the keys
        panel_entities = {
            g: [
                {
                    k: [t.get(k) for t in panel_entities.get(g) if k in t]
                    for x in panel_entities.get(g)
                }
                for k in set().union(*panel_entities.get(g))
            ]
            for g in set_tags
            if panel_entities.get(g)
        }
        return panel_entities

    def get_scorecard_performance_dashboards(self) -> List:
        """_summary_
            returns the raw array of entities of dashboards filtered on name like Performance and Scorecard
        Raises:
            e: logs the response back from new relic regarding the query attempt and raises the actual error

        Returns:
            List: array of the entities built out from the results of the cursor/paginator
        """
        entity_search = "entitySearch(query: \"((name LIKE 'Scorecard -%') OR (name LIKE 'Performance -%')) AND (domain = 'VIZ' AND type = 'DASHBOARD')\")"
        on = "... on DashboardEntityOutline"
        output_keys = "{guid name updatedAt accountId permalink createdAt}"
        resp = self.nerdgraph_entitysearch(entity_search, output_keys, on)
        main_dashboards = [
            x
            for x in resp
            if x
            for k, v in x.items()
            if k == "name" and v.split()[-1] == "Highered"
        ]
        self.__logger.info(
            f"Found {len(main_dashboards)} Scorecard and Performance dashboards found"
        )
        return main_dashboards
