import json

from typeguard import typechecked
from typing import Any, Dict, List, Union
from wallboard.utils.constants import AUTO_DISCOVERY_TYPES, DEFAULT_AUTO_DISCOVERY_TYPE


@typechecked
class ProductAccountConfig:
    """Model class for a singular account config for a product.

    Enforces attribute schemas (types/allowed values) and defaults.
    Implements to_dict() and from_dict() to allow easy conversion to and from dictionaries.
    """

    def __init__(
        self,
        product_id: str = None,
        account_id: int = None,
        auto_discover_synthetic_guids: str = DEFAULT_AUTO_DISCOVERY_TYPE,
        auto_discover_apm_apps: str = DEFAULT_AUTO_DISCOVERY_TYPE,
        auto_discover_browser_apps: str = DEFAULT_AUTO_DISCOVERY_TYPE,
        monitors: List[str] = [],
        synthetic_guids: List[str] = [],
        exclude_synthetic_guids: List[str] = [],
        browser_apps: List[str] = [],
        exclude_browser_apps: List[str] = [],
        apm_apps: List[str] = [],
        exclude_apm_apps: List[str] = [],
        notification_channel_ids: List[int] = [],
        pagerduty_channel_ids: List[int] = [],
    ):
        self.product_id = product_id
        self.account_id = account_id
        self.auto_discover_synthetic_guids = auto_discover_synthetic_guids
        self.auto_discover_apm_apps = auto_discover_apm_apps
        self.auto_discover_browser_apps = auto_discover_browser_apps
        self.monitors = monitors
        self.synthetic_guids = synthetic_guids
        self.exclude_synthetic_guids = exclude_synthetic_guids
        self.browser_apps = browser_apps
        self.exclude_browser_apps = exclude_browser_apps
        self.apm_apps = apm_apps
        self.exclude_apm_apps = exclude_apm_apps
        self.notification_channel_ids = notification_channel_ids
        self.pagerduty_channel_ids = pagerduty_channel_ids

    @property
    def product_id(self) -> Union[str, None]:
        return self.__product_id

    @product_id.setter
    def product_id(self, product_id: Union[str, None]):
        if product_id is not None:
            assert product_id, "Product ID cannot be an empty string"
        self.__product_id = product_id

    @property
    def account_id(self) -> Union[int, None]:
        return self.__account_id

    @account_id.setter
    def account_id(self, account_id: Union[int, None]):
        if account_id is not None:
            assert account_id, "Account ID cannot be 0"
        self.__account_id = account_id

    @property
    def auto_discover_synthetic_guids(self) -> str:
        return self.__auto_discover_synthetic_guids

    @auto_discover_synthetic_guids.setter
    def auto_discover_synthetic_guids(self, auto_discover_synthetic_guids: str):
        assert (
            auto_discover_synthetic_guids in AUTO_DISCOVERY_TYPES
        ), f"Unexpected auto discovery type: {auto_discover_synthetic_guids}"
        self.__auto_discover_synthetic_guids = auto_discover_synthetic_guids

    @property
    def auto_discover_apm_apps(self) -> str:
        return self.__auto_discover_apm_apps

    @auto_discover_apm_apps.setter
    def auto_discover_apm_apps(self, auto_discover_apm_apps: str):
        assert (
            auto_discover_apm_apps in AUTO_DISCOVERY_TYPES
        ), f"Unexpected auto discovery type: {auto_discover_apm_apps}"
        self.__auto_discover_apm_apps = auto_discover_apm_apps

    @property
    def auto_discover_browser_apps(self) -> str:
        return self.__auto_discover_browser_apps

    @auto_discover_browser_apps.setter
    def auto_discover_browser_apps(self, auto_discover_browser_apps: str):
        assert (
            auto_discover_browser_apps in AUTO_DISCOVERY_TYPES
        ), f"Unexpected auto discovery type: {auto_discover_browser_apps}"
        self.__auto_discover_browser_apps = auto_discover_browser_apps

    @property
    def monitors(self) -> List[str]:
        return self.__monitors

    @monitors.setter
    def monitors(self, monitors: List[str]):
        self.__monitors = list(sorted(set(monitors)))

    @property
    def synthetic_guids(self) -> List[str]:
        return [guid for guid in self.__synthetic_guids if guid not in self.exclude_synthetic_guids]

    @synthetic_guids.setter
    def synthetic_guids(self, synthetic_guids: List[str]):
        self.__synthetic_guids = list(sorted(set(synthetic_guids)))

    @property
    def exclude_synthetic_guids(self) -> List[str]:
        return self.__exclude_synthetic_guids

    @exclude_synthetic_guids.setter
    def exclude_synthetic_guids(self, exclude_synthetic_guids: List[str]):
        self.__exclude_synthetic_guids = list(sorted(set(exclude_synthetic_guids)))

    @property
    def browser_apps(self) -> List[str]:
        return [app for app in self.__browser_apps if app not in self.exclude_browser_apps]

    @browser_apps.setter
    def browser_apps(self, browser_apps: List[str]):
        self.__browser_apps = list(sorted(set(browser_apps)))

    @property
    def exclude_browser_apps(self) -> List[str]:
        return self.__exclude_browser_apps

    @exclude_browser_apps.setter
    def exclude_browser_apps(self, exclude_browser_apps: List[str]):
        self.__exclude_browser_apps = list(sorted(set(exclude_browser_apps)))

    @property
    def apm_apps(self) -> List[str]:
        return [app for app in self.__apm_apps if app not in self.exclude_apm_apps]

    @apm_apps.setter
    def apm_apps(self, apm_apps: List[str]):
        self.__apm_apps = list(sorted(set(apm_apps)))

    @property
    def exclude_apm_apps(self) -> List[str]:
        return self.__exclude_apm_apps

    @exclude_apm_apps.setter
    def exclude_apm_apps(self, exclude_apm_apps: List[str]):
        self.__exclude_apm_apps = list(sorted(set(exclude_apm_apps)))

    @property
    def notification_channel_ids(self) -> List[int]:
        return self.__notification_channel_ids

    @notification_channel_ids.setter
    def notification_channel_ids(self, notification_channel_ids: List[int]):
        self.__notification_channel_ids = list(sorted(set(notification_channel_ids)))

    @property
    def pagerduty_channel_ids(self) -> List[int]:
        return self.__pagerduty_channel_ids

    @pagerduty_channel_ids.setter
    def pagerduty_channel_ids(self, pagerduty_channel_ids: List[int]):
        self.__pagerduty_channel_ids = list(sorted(set(pagerduty_channel_ids)))

    def get_terraform_key(self) -> str:
        """Returns the key for this ProductAccountConfig used by Terraform."""
        return f"{self.product_id}:{self.account_id}"

    def get_terraform_value(
        self, product_title: str, alert_cfgs: List[AlertConfig]
    ) -> Dict[str, Any]:
        """Format this ProductAccountConfig as a dictionary that can be used by Terraform.

        Parameters
        ----------
        product_title : str
            The title of the product that this ProductAccountConfig belongs to.
        alert_cfgs : List[AlertConfig]
            The AlertConfigs of the product that this ProductAccountConfig belongs to.

        Returns
        -------
        terraform_input : Dict[str, Any]
            This ProductAccountConfig as a dictionary that can be used by Terraform.
        """
        tf_input = {
            "title": product_title,
            "alerts": {a.label: a.to_dict() for a in alert_cfgs},
        }
        tf_input = {**tf_input, **self.to_dict()}
        return tf_input

    def from_dict(self, cfg: Dict[str, Any]):
        """Load this object's attributes from a dictionary

        Parameters
        ----------
        cfg : Dict[str, Any]
            The dictionary to load this object's attributes from. Requires structure:

            {
                "productId": str|None,
                "accountId": int|None,
                "autoDiscoverSyntheticGuids": str|None,
                "autoDiscoverApmApps": str|None,
                "autoDiscoverBrowserApps": str|None,
                "monitors": List[str]|None,
                "syntheticGuids": List[str]|None,
                "excludeSyntheticGuids": List[str]|None,
                "browserApps": List[str]|None,
                "excludeBrowserApps": List[str]|None,
                "apmApps": List[str]|None,
                "excludeApmApps": List[str]|None,
                "notificationChannelIds": List[int]|None,
                "pagerDutyChannelIds": List[int]|None
            }
        """
        self.product_id = cfg.get("productId")
        self.account_id = cfg.get("accountId")
        self.auto_discover_synthetic_guids = (
            cfg.get("autoDiscoverSyntheticGuids") or DEFAULT_AUTO_DISCOVERY_TYPE
        )
        self.auto_discover_apm_apps = cfg.get("autoDiscoverApmApps") or DEFAULT_AUTO_DISCOVERY_TYPE
        self.auto_discover_browser_apps = (
            cfg.get("autoDiscoverBrowserApps") or DEFAULT_AUTO_DISCOVERY_TYPE
        )
        self.monitors = cfg.get("monitors") or []
        self.synthetic_guids = cfg.get("syntheticGuids") or []
        self.exclude_synthetic_guids = cfg.get("excludeSyntheticGuids") or []
        self.browser_apps = cfg.get("browserApps") or []
        self.exclude_browser_apps = cfg.get("excludeBrowserApps") or []
        self.apm_apps = cfg.get("apmApps") or []
        self.exclude_apm_apps = cfg.get("excludeApmApps") or []
        self.notification_channel_ids = cfg.get("notificationChannelIds") or []
        self.pagerduty_channel_ids = cfg.get("pagerDutyChannelIds") or []

    def to_dict(self, sparse: bool = False) -> Dict[str, Any]:
        """Convert this object to a dictionary

        Parameters
        ----------
        sparse : bool
            (optional) If true, does not include empty or defaulted fields on the returned dictionary.

        Returns
        -------
        product_account_dict : Dict[str, Any]
            This object in dictionary form with the structure:

            {
                "productId": str|None,
                "accountId": int|None,
                "autoDiscoverSyntheticGuids": str|None,
                "autoDiscoverApmApps": str|None,
                "autoDiscoverBrowserApps": str|None,
                "monitors": List[str]|None,
                "syntheticGuids": List[str]|None,
                "excludeSyntheticGuids": List[str]|None,
                "browserApps": List[str]|None,
                "excludeBrowserApps": List[str]|None,
                "apmApps": List[str]|None,
                "excludeApmApps": List[str]|None,
                "notificationChannelIds": List[int]|None,
                "pagerDutyChannelIds": List[int]|None
            }
        """
        d = {}
        if not sparse:
            d["productId"] = self.product_id
        if self.account_id or not sparse:
            d["accountId"] = self.account_id
        if self.auto_discover_synthetic_guids != DEFAULT_AUTO_DISCOVERY_TYPE or not sparse:
            d["autoDiscoverSyntheticGuids"] = self.auto_discover_synthetic_guids
        if self.auto_discover_apm_apps != DEFAULT_AUTO_DISCOVERY_TYPE or not sparse:
            d["autoDiscoverApmApps"] = self.auto_discover_apm_apps
        if self.auto_discover_browser_apps != DEFAULT_AUTO_DISCOVERY_TYPE or not sparse:
            d["autoDiscoverBrowserApps"] = self.auto_discover_browser_apps
        if self.monitors or not sparse:
            d["monitors"] = self.monitors
        if self.synthetic_guids or not sparse:
            d["syntheticGuids"] = self.synthetic_guids
        if self.exclude_synthetic_guids or not sparse:
            d["excludeSyntheticGuids"] = self.exclude_synthetic_guids
        if self.browser_apps or not sparse:
            d["browserApps"] = self.browser_apps
        if self.exclude_browser_apps or not sparse:
            d["excludeBrowserApps"] = self.exclude_browser_apps
        if self.apm_apps or not sparse:
            d["apmApps"] = self.apm_apps
        if self.exclude_apm_apps or not sparse:
            d["excludeApmApps"] = self.exclude_apm_apps
        if self.notification_channel_ids or not sparse:
            d["notificationChannelIds"] = self.notification_channel_ids
        if self.pagerduty_channel_ids or not sparse:
            d["pagerDutyChannelIds"] = self.pagerduty_channel_ids
        return d

    def __repr__(self) -> str:
        return json.dumps(self.to_dict(sparse=True))
