# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
"""Module for Log Analytics-related configuration."""

import os
import json
from typing import Dict, Any, Optional
from pathlib import Path

from IPython.display import display
import ipywidgets as widgets

from .exceptions import MsticpyUserConfigError
from .utility import export, is_valid_uuid, md, md_warn
from . import pkg_config
from .._version import VERSION

__version__ = VERSION
__author__ = "Ian Hellen"


_RESOURCES = [
    (
        "https://github.com/Azure/Azure-Sentinel-Notebooks/blob/"
        "master/ConfiguringNotebookEnvironment.ipynb"
    ),
    (
        "https://github.com/Azure/Azure-Sentinel-Notebooks/blob/"
        "master/A%20Getting%20Started%20Guide%20For%20Azure%20"
        "Sentinel%20ML%20Notebooks.ipynb"
    ),
]

_NO_CONFIG_WARN = [
    "Could not find Microsoft Sentinel settings in msticpyconfig.yaml or a config.json ",
    "(in the current directory or via a MSTICPYCONFIG variable.)",
    "We found the file '{config_file}' and will use this.",
    "We recommend using an explicit msticpyconfig.yaml specified using the",
    "MSTICPYCONFIG environment variable. See:",
    (
        "https://msticpy.readthedocs.io/en/latest/data_acquisition/DataProviders.html#"
        "connecting-to-an-azure-sentinel-workspace"
    ),
    "and",
    "https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html",
    "for more details.",
    "Also see the notebooks:",
    *_RESOURCES,
]

_NO_CONFIG_ERR = [
    "Could not find msticpyconfig.yaml or config.json.",
    "The 'config.json' file is created when you launch notebooks from "
    "Microsoft Sentinel. If you have copied the notebook to another location "
    "or folder you will need to copy this configuration file.",
    "Alternatively, we recommend using an explicit msticpyconfig.yaml"
    "and adding your Workspace and Tenant IDs to that file.",
    "",
    "You can create a settings file using the following commands:",
    ">>> from msticpy.config import MpConfigEdit",
    ">>> MpConfigEdit()",
]

WIDGET_DEFAULTS = {
    "layout": widgets.Layout(width="95%"),
    "style": {"description_width": "150px"},
}


@export
class WorkspaceConfig:
    """Workspace configuration class."""

    # Constants
    TENANT_ID = "{{cookiecutter.tenant_id}}"
    SUBSCRIPTION_ID = "{{cookiecutter.subscription_id}}"
    RESOURCE_GROUP = "{{cookiecutter.resource_group}}"
    WORKSPACE_ID = "{{cookiecutter.workspace_id}}"
    WORKSPACE_NAME = "{{cookiecutter.workspace_name}}"

    PKG_CONF_TENANT_KEY = "TenantId"
    PKG_CONF_WS_KEY = "WorkspaceId"

    CONF_WS_ID_KEY = "workspace_id"
    CONF_TENANT_ID_KEY = "tenant_id"
    CONF_SUB_ID_KEY = "subscription_id"
    CONF_RES_GROUP_KEY = "resource_group"
    CONF_WS_NAME_KEY = "workspace_name"

    def __init__(
        self,
        workspace: Optional[str] = None,
        config_file: Optional[str] = None,
        interactive: bool = True,
    ):
        """
        Load current Azure Notebooks configuration for Log Analytics.

        Parameters
        ----------
        config_file : Optional[str], optional
            path to a configuration file,
            If not specified, the defaults is to use a configured msticpyconfig.yaml
            If this isn't configured, it will search for (first) a config.json
            and (second) a msticpyconfig.yaml in (first) the current directory
            and (second) the parent directory and subfolders.
        workspace : str, Optional[str]
            Workspace name (where multiple workspaces are configured),
            by default the Default workspace will be used.
        interactive : bool, optional
            If this is False, initializing the class will not raise an
            exception if no configuration is found. By default, True.

        """
        self._config: Dict[str, str] = {}
        self._interactive = interactive
        self._config_file = config_file
        # If config file specified, use that
        if config_file:
            self._config.update(self._read_config_values(config_file))
        else:
            self._determine_config_source(workspace)

    def _determine_config_source(self, workspace):
        # First, try default MSTICPy config
        self._read_pkg_config_values(workspace_name=workspace)
        if self.config_loaded:
            return
        # Next, search for a config.json in the current director
        if Path("./config.json").exists():
            self._config_file = "./config.json"
        else:
            self._config_file = self._search_for_file("**/config.json")
        if self._config_file:
            self._config.update(self._read_config_values(self._config_file))
            return

        # Finally, search for a msticpyconfig.yaml
        if (
            os.environ.get("MSTICPYCONFIG")
            and Path(os.environ.get("MSTICPYCONFIG")).exists()
        ):
            self._config_file = os.environ.get("MSTICPYCONFIG")
        elif Path("./msticpyconfig.yaml").exists():
            self._config_file = "./msticpyconfig.yaml"
        else:
            self._config_file = self._search_for_file("**/msticpyconfig.yaml")
        if self._config_file:
            os.environ["MSTICPYCONFIG"] = self._config_file
            pkg_config.refresh_config()
            self._read_pkg_config_values(workspace_name=workspace)
            return
        # Finally, throw an exception (unless non-interactive)
        if self._interactive:
            # If we've arrived here after searching current folder and parent
            # then we give up. (We create but don't raise an actual exception)
            display(
                MsticpyUserConfigError(
                    *_NO_CONFIG_ERR,
                    title="Workspace configuration missing.",
                    **{f"nb_{idx}_uri": res for idx, res in enumerate(_RESOURCES)},
                )
            )

    def __getitem__(self, key: str):
        """Allow property get using dictionary key syntax."""
        if key in self._config:
            return self._config[key]
        raise KeyError

    def __setitem__(self, key: str, value: Any):
        """Allow property set using dictionary key syntax."""
        self._config[key] = value

    def __contains__(self, key: str):
        """Allow property in test."""
        # In operator overload
        return key == "Type" or key in self._config or key in self.__dict__

    def __repr__(self):
        """Return contents of current config."""
        return self._config.__repr__()

    @property
    def config_loaded(self) -> bool:
        """
        Return True if workspace id and tenant id have values.

        Returns
        -------
        bool
            True if configuration loaded.

        """
        ws_value = self._config.get(self.CONF_WS_ID_KEY, None)
        ten_value = self._config.get(self.CONF_TENANT_ID_KEY, None)
        return is_valid_uuid(ws_value) and is_valid_uuid(ten_value)  # type: ignore

    @property
    def code_connect_str(self) -> str:
        """
        Return the Log Analytics connection string for dev code auth.

        Returns
        -------
        str
            Connection string

        """
        ten_id = self._config.get(self.CONF_TENANT_ID_KEY, None)
        ws_id = self._config.get(self.CONF_WS_ID_KEY, None)
        if not ten_id:
            raise KeyError(
                f"Configuration setting for {self.CONF_TENANT_ID_KEY} "
                + "could not be found."
            )
        if not ws_id:
            raise KeyError(
                f"Configuration setting for {self.CONF_WS_ID_KEY} "
                + "could not be found."
            )
        return f"loganalytics://code().tenant('{ten_id}').workspace('{ws_id}')"

    @staticmethod
    def _read_config_values(file_path: str) -> Dict[str, str]:
        """Read configuration file."""
        if not file_path:
            return {}
        try:
            with open(file_path, "r", encoding="utf-8") as json_file:
                if json_file:
                    return json.load(json_file)
        except json.JSONDecodeError:
            pass
        return {}

    @classmethod
    def list_workspaces(cls) -> Dict:
        """
        Return list of available workspaces.

        Returns
        -------
        Dict
            Dictionary of workspaces with workspace and tenantIds.

        """
        ws_settings = pkg_config.settings.get("AzureSentinel", {}).get("Workspaces")
        if not ws_settings:
            return {}
        return {
            ws_name: {
                cls.PKG_CONF_WS_KEY: ws.get(cls.PKG_CONF_WS_KEY),
                cls.PKG_CONF_TENANT_KEY: ws.get(cls.PKG_CONF_TENANT_KEY),
            }
            for ws_name, ws in ws_settings.items()
        }

    def prompt_for_ws(self):
        """Display an interactive prompt for Workspace details."""
        md_warn("No Microsoft Sentinel configuration found.")
        md(
            "Please enter the workspace ID and tenant Id"
            + " to allow connection to the Microsoft Sentinel workspace."
        )
        ws_id_wgt = widgets.Text(description="Workspace Id:", **WIDGET_DEFAULTS)
        ten_id_wgt = widgets.Text(description="Tenant Id:", **WIDGET_DEFAULTS)

        def update_ws(chg):
            self["workspace_id"] = chg.get("new")

        def update_tnt(chg):
            self["tenant_id"] = chg.get("new")

        ws_id_wgt.observe(update_ws, names="value")
        ten_id_wgt.observe(update_tnt, names="value")
        display(widgets.VBox([ws_id_wgt, ten_id_wgt]))
        md(
            (
                "You can avoid this prompt in future by following the"
                " guidance in the"
                " <a href='https://msticpy.readthedocs.io/en/latest/"
                "data_acquisition/DataProviders.html#"
                "connecting-to-an-azure-sentinel-workspace' "
                "target='_blank' rel='noopener noreferrer'>"
                "Connecting to a Microsoft Sentinel Workspace</a>"
            ),
        )

    def _read_pkg_config_values(self, workspace_name: str = None):
        as_settings = pkg_config.settings.get("AzureSentinel")
        if not as_settings:
            return {}
        ws_settings = as_settings.get("Workspaces")  # type: ignore
        if not ws_settings:
            return {}
        if workspace_name and workspace_name in ws_settings:
            selected_workspace = ws_settings[workspace_name]
        elif "Default" in ws_settings:
            selected_workspace = ws_settings["Default"]
        else:
            return {}
        if (
            selected_workspace
            and self.PKG_CONF_WS_KEY in selected_workspace
            and self.PKG_CONF_TENANT_KEY in selected_workspace
        ):
            self._config[self.CONF_WS_ID_KEY] = selected_workspace.get(
                self.PKG_CONF_WS_KEY
            )
            self._config[self.CONF_TENANT_ID_KEY] = selected_workspace.get(
                self.PKG_CONF_TENANT_KEY
            )
        return {}

    def _search_for_file(self, pattern: str) -> Optional[str]:
        config_file = None
        for start_path in (".", ".."):
            searched_configs = list(Path(start_path).glob(pattern))
            for found_file in searched_configs:
                if found_file.name == "msticpyconfig.yaml":
                    config_file = str(found_file)
                    break
                test_content = self._read_config_values(str(found_file))
                if "workspace_id" in test_content:
                    config_file = str(found_file)
                    break
            if config_file:
                break
        if config_file:
            # Warn that we're using a "found" file, not one in the current directory
            print("Warning")
            print("\n".join(_NO_CONFIG_WARN).format(config_file=config_file))
        return config_file
