#  Copyright (c) 2022 Mira Geoscience Ltd.
#
#  This file is part of geoapps.
#
#  geoapps is distributed under the terms and conditions of the MIT License
#  (see LICENSE file at the root of this source code package).

from __future__ import annotations

import json
import os
import warnings
from copy import deepcopy
from typing import Any
from uuid import UUID

from geoh5py.io.utils import (
    as_str_if_uuid,
    dict_mapper,
    entity2uuid,
    str2uuid,
    uuid2entity,
)
from geoh5py.shared import Entity
from geoh5py.shared.exceptions import BaseValidationError, JSONParameterValidationError
from geoh5py.shared.validators import AssociationValidator
from geoh5py.workspace import Workspace

from .constants import base_validations, ui_validations
from .utils import (
    container_group2name,
    flatten,
    inf2str,
    list2str,
    none2str,
    path2workspace,
    set_enabled,
    str2inf,
    str2list,
    str2none,
    workspace2path,
)
from .validation import InputValidation


class InputFile:
    """
    Handles loading ui.json input files.

    Attributes
    ----------
    data : Input file content parsed to flat dictionary of key:value.
    ui_json: User interface serializable as ui.json format
    workspace: Target :obj:`geoh5py.workspace.Workspace`
    validations: Dictionary of validations for parameters in the input file

    Methods
    -------
    write_ui_json()
        Writes a ui.json formatted file from 'data' attribute contents.
    read_ui_json()
        Reads a ui.json formatted file into 'data' attribute dictionary.  Optionally filters
        ui.json fields other than 'value'.
    """

    _path: str | None = None
    _name: str | None = None
    _ui_validators: InputValidation = InputValidation(
        validations=ui_validations,
        validation_options={"ignore_list": ("value",)},
    )
    _validators = None
    association_validator = AssociationValidator()

    def __init__(
        self,
        data: dict[str, Any] = None,
        ui_json: dict[str, Any] = None,
        validations: dict = None,
        validation_options: dict = None,
        workspace: Workspace = None,
    ):
        self._validation_options = validation_options
        self.validations = validations
        self.ui_json = ui_json
        self.data = data
        self.workspace = workspace

    @property
    def data(self):
        if (
            getattr(self, "_data", None) is None
            and getattr(self, "_ui_json", None) is not None
        ):
            self.data = flatten(self.ui_json)

        return self._data

    @data.setter
    def data(self, value: dict[str, Any]):
        if value is not None:
            if not isinstance(value, dict):
                raise ValueError("Input 'data' must be of type dict or None.")

            if self._ui_json is None:
                raise AttributeError("'ui_json' must be set before setting data.")

            if len(value) != len(self._ui_json):
                raise ValueError(
                    "The number of input values for 'data' must "
                    "equal the number of parameters in 'ui_json'."
                )

            if "geoh5" in value:
                self.workspace = value["geoh5"]

            value = self._promote(value)

            if self.validators is not None and not self.validation_options.get(
                "disabled", False
            ):
                self.validators.validate_data(value)

        self._data = value

    @property
    def name(self) -> str | None:
        """
        Name of ui.json file.
        """
        if getattr(self, "_name", None) is None and self.ui_json is not None:
            self.name = self.ui_json["title"]

        return self._name

    @name.setter
    def name(self, name: str):
        if ".ui.json" not in name:
            name += ".ui.json"

        self._name = name

    def load(self, input_dict: dict[str, Any]):
        """Load data from dictionary and validate."""
        self.ui_json = input_dict
        self.data = flatten(input_dict)

    @property
    def path(self) -> str | None:
        """
        Directory for the input/output ui.json file.
        """
        if getattr(self, "_path", None) is None and self.workspace is not None:
            self.path = os.path.dirname(self.workspace.h5file)

        return self._path

    @path.setter
    def path(self, path: str):
        if not os.path.isdir(path):
            raise ValueError(f"The specified path: '{path}' does not exist.")

        self._path = path

    @property
    def path_name(self) -> str | None:
        if self.path is not None and self.name is not None:
            return os.path.join(self.path, self.name)

        return None

    @staticmethod
    def read_ui_json(json_file: str, **kwargs):
        """
        Read and create an InputFile from *.ui.json
        """
        input_file = InputFile(**kwargs)

        if "ui.json" not in json_file:
            raise ValueError("Input file should have the extension *.ui.json")

        with open(json_file, encoding="utf-8") as file:
            input_file.load(json.load(file))

        return input_file

    @property
    def ui_json(self) -> dict | None:
        """
        Dictionary representing the ui.json file with promoted values.
        """
        return self._ui_json

    @ui_json.setter
    def ui_json(self, value: dict[str, Any]):
        if value is not None:

            if not isinstance(value, dict):
                raise ValueError("Input 'ui_json' must be of type dict or None.")

            self._ui_json = self.numify(value)
            default_validations = InputValidation.infer_validations(self._ui_json)
            for key, validations in default_validations.items():
                if key in self.validations:
                    validations = {**validations, **self.validations[key]}
                self._validations[key] = validations
        else:
            self._ui_json = None
        self._validators = None

    @classmethod
    def ui_validation(cls, ui_json: dict[str, Any]):
        """Validation of the ui_json forms"""
        cls._ui_validators(ui_json)

    def update_ui_values(self, data: dict, none_map=None):
        """
        Update the ui.json values and enabled status from input data.

        :param data: Key and value pairs expected by the ui_json.
        :param none_map : Map parameter 'None' values to non-null numeric types.
            The parameters in the dictionary are mapped to optional and disabled.

        :raises ValueError : If attempting to set None value to non-optional
            parameter.
        """
        if self.ui_json is None:
            raise UserWarning("InputFile requires a 'ui_json' to be defined.")

        if none_map is None:
            none_map = {}

        for key, value in data.items():
            if isinstance(self.ui_json[key], dict):
                if value is None:
                    value = none_map.get(key, None)
                    enabled = False
                else:
                    enabled = True

                was_group_enabled = set_enabled(self.ui_json, key, enabled)
                if was_group_enabled:
                    warnings.warn(
                        f"Setting all member of group: {self.ui_json[key]['group']} "
                        f"to enabled: {enabled}."
                    )

                member = "value"
                if "isValue" in self.ui_json[key]:
                    if isinstance(value, (Entity, UUID)):
                        self.ui_json[key]["isValue"] = False
                        member = "property"
                    else:
                        self.ui_json[key]["isValue"] = True

                if (value is None) and (not self.ui_json[key].get("enabled", False)):
                    continue

                self.ui_json[key][member] = value

            else:
                self.ui_json[key] = value

    @property
    def validation_options(self):
        """Pass validation options to the validators."""
        if self._validation_options is None:
            return {}
        return self._validation_options

    @property
    def validations(self):
        if getattr(self, "_validations", None) is None:
            self._validations = deepcopy(base_validations)

        return self._validations

    @validations.setter
    def validations(self, valid_dict: dict | None):
        if not isinstance(valid_dict, (dict, type(None))):
            raise TypeError(
                "Input validations must be of type 'dict' or None. "
                f"Value type {type(valid_dict)} provided"
            )

        if valid_dict is not None:
            valid_dict = {**valid_dict, **deepcopy(base_validations)}

        self._validations = valid_dict

    @property
    def validators(self):
        if getattr(self, "_validators", None) is None:
            self._validators = InputValidation(
                ui_json=self.ui_json,
                validations=self.validations,
                validation_options=self.validation_options,
            )

        return self._validators

    @property
    def workspace(self):
        return self._workspace

    @workspace.setter
    def workspace(self, workspace: Workspace | None):
        if workspace is not None and not isinstance(workspace, Workspace):
            raise ValueError(
                "Input 'workspace' must be a valid :obj:`geoh5py.workspace.Workspace`"
            )

        self._workspace = workspace

        if self.validators is not None:
            self.validators.workspace = workspace

    def write_ui_json(
        self,
        name: str = None,
        none_map: dict[str, Any] = None,
        path: str = None,
    ):
        """
        Writes a formatted ui.json file from InputFile data

        :param name: Name of the file
        :param none_map : Map parameter None values to non-null numeric types.
        :param path: Directory to write the ui.json to.
        """
        if name is not None:
            self.name = name

        if path is not None:
            self.path = path

        if self.path_name is None:
            raise AttributeError(
                "The input file requires 'path' and 'name' to be set before writing out."
            )

        if self.ui_json is None:
            raise AttributeError(
                "The input file requires 'ui_json' and 'data' to be set before writing out."
            )

        if self.data is not None:
            self.update_ui_values(self.data, none_map=none_map)

        with open(self.path_name, "w", encoding="utf-8") as file:
            json.dump(self._stringify(self._demote(self.ui_json)), file, indent=4)

        return self.path_name

    @staticmethod
    def _stringify(var: dict[str, Any]) -> dict[str, Any]:
        """
        Convert inf, none, and list types to strings within a dictionary

        :param var: Dictionary containing ui.json keys, values, fields

        :return: Dictionary with inf, none and list types converted to string
            representations in json format.
        """
        for key, value in var.items():
            exclude = ["choiceList", "meshType", "dataType", "association"]
            mappers = (
                [list2str, inf2str, as_str_if_uuid, none2str]
                if key not in exclude
                else [inf2str, as_str_if_uuid, none2str]
            )
            var[key] = dict_mapper(
                value, mappers, omit={ex: [list2str] for ex in exclude}
            )

        return var

    @classmethod
    def numify(cls, ui_json: dict[str, Any]) -> dict[str, Any]:
        """
        Convert inf, none and list strings to numerical types within a dictionary

        Parameters
        ----------

        ui_json :
            dictionary containing ui.json keys, values, fields

        Returns
        -------
        Dictionary with inf, none and list string representations converted numerical types.

        """
        if not isinstance(ui_json, dict):
            raise ValueError("Input value for 'numify' must be a ui_json dictionary.")

        for key, value in ui_json.items():
            if isinstance(value, dict):
                try:
                    cls.ui_validation(value)
                except tuple(BaseValidationError.__subclasses__()) as error:
                    raise JSONParameterValidationError(key, error.args[0]) from error

                value = cls.numify(value)

            mappers = (
                [str2none, str2inf, str2uuid, path2workspace]
                if key == "ignore_values"
                else [str2list, str2none, str2inf, str2uuid, path2workspace]
            )
            ui_json[key] = dict_mapper(value, mappers)

        return ui_json

    def _demote(self, var: dict[str, Any]) -> dict[str, str]:
        """Converts promoted parameter values to their string representations."""
        mappers = [entity2uuid, as_str_if_uuid, workspace2path, container_group2name]
        for key, value in var.items():

            if isinstance(value, dict):
                var[key] = self._demote(value)
            elif isinstance(value, (list, tuple)):
                var[key] = [dict_mapper(val, mappers) for val in value]
            else:
                var[key] = dict_mapper(value, mappers)

        return var

    def _promote(self, var: dict[str, Any]) -> dict[str, Any]:
        """Convert uuids to entities from the workspace."""
        for key, value in var.items():

            if isinstance(value, dict):
                var[key] = self._promote(value)
            elif isinstance(value, UUID):
                self.association_validator(key, value, self.workspace)
                var[key] = uuid2entity(value, self.workspace)

        return var
