"""
:synopsis: Validate Geodatabase
:authors: Riley Baird (OK), Emma Baker (OK)
"""


import logging
from collections.abc import Sequence
from functools import lru_cache
from os.path import join

import arcpy

from ...lib.config_dataclasses import NG911FeatureClass
from ...lib.misc import unwrap, GPParameterValue, params_to_dict
from ...lib.session import config
from ...lib.validator import NG911Validator, ValidationRoutine

_logger = logging.getLogger(__name__)

@lru_cache(1)
def _gdb_has_rds(gdb: str) -> bool:
    """Returns whether *gdb* contains the required feature dataset (RDS). Uses
    ``@lru_cache`` so that ``arcpy.ListDatasets`` does not need to be called
    every time a parameter is updated. A cache size of 1 is used to ensure that
    *changing* the value of *gdb* will produce a fresh result."""
    with arcpy.EnvManager(workspace=gdb):
        return config.gdb_info.required_dataset_name in arcpy.ListDatasets()

def _routine_validate_selection(routine_parameter: arcpy.Parameter, fc_parameter: arcpy.Parameter) -> None:
    if not routine_parameter.enabled:
        # validation_logger.debug(f"{routine_parameter.name} disabled; nothing to validate.")
        return
    selected_feature_classes: set[NG911FeatureClass] = set(
        config.get_feature_class_by_name(name)
        for name, selected
        in get_feature_class_selections([fc_parameter]).items()
        if selected
    )
    # validation_logger.debug(f"Validating {routine_parameter.name}; selected feature classes: {', '.join(fc.name for fc in selected_feature_classes)}")
    messages: list[str] = []
    for routine in ValidationRoutine.get_selected_from_parameter(routine_parameter):
        missing_fcs: frozenset[NG911FeatureClass] = routine.required_feature_classes - selected_feature_classes
        if missing_fcs:
            fcs_str = ", ".join(fc.name for fc in missing_fcs)
            # validation_logger.debug(f"Routine '{routine.name}' requires additional feature class(es): {fcs_str}.")
            messages.append(f"Validation routine '{routine.name}' requires also selecting {fcs_str}.")
    if messages:
        routine_parameter.setErrorMessage(" ".join(messages))
    else:
        # validation_logger.debug(f"Parameter {routine_parameter.name} is valid.")
        routine_parameter.clearMessage()

def _param_set_enabled(parameter: arcpy.Parameter, enable: bool, *, populate: bool = False, select_all: bool = False) -> bool:
    """
    Sets the ``enabled`` attribute to *enable*. If *enable* is ``False``,
    clears the ``value`` and/or ``values`` attributes, if applicable.

    If *populate* is ``True`` and ``parameter.enabled`` is being changed from
    ``False`` to ``True``,
    :meth:`.ValidationRoutine.populate_parameter_value_table` is called.

    :param arcpy.Parameter parameter: The ``arcpy.Parameter`` instance
    :param bool enable: Whether to enable the parameter (if ``True``) or
        disable it (if ``False``); default True
    :param bool populate: Whether to call
        :meth:`.ValidationRoutine.populate_parameter_value_table` with
        *parameter*. Will only work if *parameter* is a routine selection
        parameter (based, in part, on its ``name`` attribute). Ignored if
        *enable* is ``False`` or ``parameter.enabled`` is already ``True``.
        Default False.
    :param bool select_all: Whether to select all routines in the parameter.
        Will only work if *parameter* is a routine selection
        parameter (based, in part, on its ``name`` attribute). Ignored if
        *enable* is ``False``. Default False.
    :return: Whether the parameter is enabled or not
    :rtype: bool
    """
    populate = populate and enable and not parameter.enabled
    if enable:
        parameter.enabled = True
        if populate or select_all:
            try:
                ValidationRoutine.populate_parameter_value_table(parameter, select_all)
            except ValueError as exc:
                to_raise = ValueError(f"Argument for 'populate' was True, but argument for 'parameter' (named '{parameter.name}' was not a validation parameter.")
                _logger.critical("Parameter-related error.", exc_info=to_raise)
                raise to_raise
    else:
        if hasattr(parameter, "value"):
            parameter.value = None
        if hasattr(parameter, "values"):
            parameter.values = []
        parameter.enabled = False
    return parameter.enabled

def get_feature_class_selections(parameters: Sequence[arcpy.Parameter]) -> dict[str, bool]:
    """Returns a ``dict`` of feature class :attr:`~NG911FeatureClass.name`\ s
    and whether each is selected. Only includes the feature classes listed in
    the ``feature_classes_to_check`` parameter, which is automatically found
    among *parameters*."""
    fc_param = params_to_dict(parameters)["feature_classes_to_check"]
    return {fc_name: selected for fc_name, selected in unwrap(fc_param) or []}

def get_routine_selections(parameters: Sequence[arcpy.Parameter], enabled_only: bool = False) -> dict[str, bool]:
    """Returns a ``dict`` of routine names and whether each is selected."""
    return {
        routine_name: bool(selected)
        for routine_parameter in _get_routine_parameters(parameters)
            if (routine_parameter.enabled if enabled_only else True)
        for routine_name, selected in (unwrap(routine_parameter) or [])
    }

def _all_available_validations_selected(parameters: Sequence[arcpy.Parameter]) -> bool:
    """Returns whether all *available* routines AND all *available* feature
    classes have been selected. This may be ``True`` even if required feature
    classes are missing. Does **not** consider the ``run_all`` parameter."""
    fc_selections: dict[str, bool] = get_feature_class_selections(parameters)
    routine_selections: dict[str, bool] = get_routine_selections(parameters, True)
    _logger.debug(f"fc_selections ({type(fc_selections)}: {fc_selections}")
    _logger.debug(f"routine_selections ({type(routine_selections)}: {routine_selections}")
    return all(fc_selections.values()) and all(routine_selections.values())

    # fc_param = params_to_dict(parameters)["feature_classes_to_check"]
    # selected_feature_class_names: list[str] = unwrap(fc_param)
    # return (
    #     set(selected_feature_class_names or [])
    #         .issuperset(set(fc_param.filter.list))
    #     and all(map(all_values_selected, _get_routine_parameters(parameters)))
    # )

def _all_required_validations_selected(parameters: Sequence[arcpy.Parameter]) -> bool:
    """Returns whether all routines AND all *required* feature classes have
    been selected. If any required feature classes are missing, this will
    return ``False``, even though :func:`_all_available_validations_selected`
    may return ``True``. Does **not** consider the `run_all` parameter."""
    selected_fc_names: set[str] = set(get_feature_class_selections(parameters).keys())
    required_fc_names: set[str] = set(config.required_feature_class_names)
    if required_fc_names - selected_fc_names:
        # Then at least one is missing from selected_fc_names
        return False
    return all(get_routine_selections(parameters, False).values())
    # selected_feature_class_names = unwrap_to_dict(parameters)["feature_classes_to_check"]
    # return (
    #     set(selected_feature_class_names or [])
    #         .issuperset(set(config.required_feature_class_names))
    #     and all(map(all_values_selected, _get_routine_parameters(parameters)))
    # )

def _get_parameter_index(parameters: Sequence[arcpy.Parameter], name: str) -> int:
    """Given a list of ``arcpy.Parameter`` instances, returns the index of the
    ``Parameter`` in the list with a ``name`` attribute equal to *name*."""
    try:
        return parameters.index(next(filter(lambda p: p.name == name, parameters)))
    except StopIteration:
        raise ValueError(f"No Parameter found with name '{name}'.")

def _get_routine_parameters(parameters: Sequence[arcpy.Parameter]) -> list[arcpy.Parameter]:
    """Given a list of ``arcpy.Parameter`` instances, returns only those which
    are for the selection of validation routines."""
    return [p for p in parameters if ValidationRoutine.is_routine_parameter_name(p.name)]


class ValidateGeodatabase:
    """
    Class describing an ArcGIS Python Toolbox tool: "Validate Geodatabase".
    """
    # TODO: Idea - Generate feature layers queried down to include only those features with errors?
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = "Validate Geodatabase"
        self.description = ""
        self.canRunInBackground = False
        self.category = "2 - Validation"

    def getParameterInfo(self):
        """Define parameter definitions"""

        params = [
            arcpy.Parameter(
                displayName="Input Geodatabase",
                name="input_geodatabase",
                datatype="DEWorkspace",
                parameterType="Required",
                direction="Input"
            ),
            overwrite_error_table := arcpy.Parameter(
                displayName="Overwrite Error Table",
                name="overwrite_error_table",
                datatype="GPBoolean",
                parameterType="Optional",
                direction="Input"
            ),
            respect_submit := arcpy.Parameter(
                displayName=f"Respect {config.fields.submit:n}",
                name="respect_submit",
                datatype="GPBoolean",
                parameterType="Optional",
                direction="Input"
            ),
            run_all := arcpy.Parameter(
                displayName=f"Run All Available Validations",
                name="run_all",
                datatype="GPBoolean",
                parameterType="Optional",
                direction="Input",
                enabled=False
            ),
            # feature_classes_to_check := arcpy.Parameter(
            #     displayName="Feature Classes to Check",
            #     name="feature_classes_to_check",
            #     datatype="GPString",
            #     parameterType="Required",
            #     direction="Input",
            #     enabled=False,
            #     multiValue=True
            # ),
            feature_classes_to_check := arcpy.Parameter(
                displayName="Feature Classes to Check",
                name="feature_classes_to_check",
                datatype="GPValueTable",
                parameterType="Optional",
                direction="Input"
            ),
            *ValidationRoutine.parameters_for_all_categories(),
            # OUTPUT
            arcpy.Parameter(
                displayName="Feature Attribute Error Table",
                name="feature_attribute_error_table",
                datatype="DETable",
                parameterType="Derived",
                direction="Output"
            ),
            arcpy.Parameter(
                displayName="GDB Error Table",
                name="gdb_error_table",
                datatype="DETable",
                parameterType="Derived",
                direction="Output"
            )
        ]
        run_all.value = False
        feature_classes_to_check.columns = [['GPString', 'Feature Class', 'ReadOnly'], ['GPBoolean', 'Validate?']]
        feature_classes_to_check.enabled = False
        overwrite_error_table.value = False
        respect_submit.value = True
        # feature_classes_to_check.controlCLSID = "{38C34610-C7F7-11D5-A693-0008C711C8C1}"
        return params

    def isLicensed(self):
        """Set whether tool is licensed to execute."""
        return True

    def updateParameters(self, parameters: list[arcpy.Parameter]):
        """Modify the values and properties of parameters before internal
        validation is performed.  This method is called whenever a parameter
        has been changed."""
        required_dataset_name = config.gdb_info.required_dataset_name
        param_dict: dict[str, arcpy.Parameter] = {p.name: p for p in parameters}
        gdb = unwrap(param_dict["input_geodatabase"])
        gdb_data_type = arcpy.Describe(gdb).dataType if gdb else None
        if (
            gdb
            and not param_dict["input_geodatabase"].hasBeenValidated
            and gdb_data_type == "Workspace"
            and arcpy.Exists(gdb)
        ):  # ...then gdb input is some workspace; don't know yet if it's an NG911 workspace
            # Ensure required dataset exists
            with arcpy.EnvManager(workspace=gdb):
                if required_dataset_name in arcpy.ListDatasets():
                    # Input GDB is an NG911 workspace
                    _param_set_enabled(param_dict["run_all"], True)
                    self._enable_feature_class_parameter(gdb, param_dict)
                else:
                    # Input GDB is NOT an NG911 workspace
                    _param_set_enabled(param_dict["run_all"], False)
                    _param_set_enabled(param_dict["feature_classes_to_check"], False)
                    param_dict["input_geodatabase"].setErrorMessage(f"Geodatabase does not contain a feature dataset named '{required_dataset_name}'.")
                    return



        def __update_routine_parameters_enabled(*, select_all: bool = False):
            feature_class_selection_status: list[list[str | bool]] = unwrap(param_dict["feature_classes_to_check"]) or []
            selected_feature_class_names = [fc_name for fc_name, selected in feature_class_selection_status if selected]
            _param_set_enabled(param_dict["geodatabase_validations"], gdb_data_type == "Workspace", populate=True, select_all=select_all)
            _param_set_enabled(param_dict["general_feature_class_validations"], bool(selected_feature_class_names), populate=True, select_all=select_all)
            _param_set_enabled(param_dict["address_point_validations"], config.feature_classes.address_point.name in selected_feature_class_names, populate=True, select_all=select_all)
            _param_set_enabled(param_dict["road_centerline_validations"], config.feature_classes.road_centerline.name in selected_feature_class_names, populate=True, select_all=select_all)
        __update_routine_parameters_enabled()

        run_all = param_dict["run_all"]
        if run_all.enabled:  # Implies that the input GDB is an NG911 workspace
            if not run_all.hasBeenValidated:
                # If run_all was just changed, select all validations
                param_dict["feature_classes_to_check"].values = [[fc_name, True] for fc_name, _ in param_dict["feature_classes_to_check"].values]
                __update_routine_parameters_enabled(select_all=True)
                # for param in [p for p in _get_routine_parameters(parameters) if p.enabled]:
                #     param.values = param.filter.list
            # Keep run_all in sync with the validation routine parameters
            # TODO: FIX AND RE-ENABLE BELOW
            all_selected: bool = _all_available_validations_selected(parameters)
            if bool(unwrap(run_all)) != all_selected:
                run_all.value = all_selected


        # _update_validation_group_param(param_dict["geodatabase_validations"], gdb_data_type == "Workspace")
        # _update_validation_group_param(param_dict["general_feature_class_validations"], bool(selected_feature_class_names))
        # _update_validation_group_param(param_dict["address_point_validations"], config.feature_classes.address_point.name in selected_feature_class_names)
        # _update_validation_group_param(param_dict["road_centerline_validations"], config.feature_classes.road_centerline.name in selected_feature_class_names)

    @staticmethod
    def _enable_feature_class_parameter(gdb: str, param_dict: dict[str, arcpy.Parameter]):
        with arcpy.EnvManager(workspace=join(gdb, config.gdb_info.required_dataset_name)):
            param_dict["input_geodatabase"].clearMessage()
            required_fc_set = set(config.required_feature_class_names)
            optional_fc_set = set(config.optional_feature_class_names)
            available_fc_set = set(arcpy.ListFeatureClasses())
            param_dict["feature_classes_to_check"].enabled = True
            param_dict["feature_classes_to_check"].values = [[fc, False] for fc in sorted(list(required_fc_set & available_fc_set))]
            if available_fc_set - required_fc_set - optional_fc_set:
                param_dict["input_geodatabase"].setWarningMessage("Workspace contains feature classes with non-standard names. Those feature classes will not be validated, and their presence may be noted in the output error table.")

    def updateMessages(self, parameters: list[arcpy.Parameter]):
        """Modify the messages created by internal validation for each tool
        parameter.  This method is called after internal validation."""
        required_dataset_name = config.gdb_info.required_dataset_name
        param_dict: dict[str, arcpy.Parameter] = {p.name: p for p in parameters}
        gdb = unwrap(param_dict["input_geodatabase"])

        if not _gdb_has_rds(gdb):
            param_dict["input_geodatabase"].setErrorMessage(f"Geodatabase does not contain a feature dataset named '{required_dataset_name}'.")
        else:
            param_dict["input_geodatabase"].clearMessage()

        for routine_parameter_name in ["geodatabase_validations", "general_feature_class_validations", "address_point_validations", "road_centerline_validations"]:
            _routine_validate_selection(param_dict[routine_parameter_name], param_dict["feature_classes_to_check"])

    def execute(self, parameters, messages):
        """The source code of the tool."""
        _gdb_has_rds.cache_clear()
        param_dict: dict[str, arcpy.Parameter] = {p.name: p for p in parameters}
        value_dict: dict[str, GPParameterValue | list[GPParameterValue]] = {p.name: unwrap(p) for p in parameters}
        gdb: str = value_dict["input_geodatabase"]
        overwrite: bool | None = value_dict["overwrite_error_table"]
        selected_feature_class_names: list[str] = [x[0] for x in value_dict["feature_classes_to_check"] if x[1]]
        respect_submit: bool = value_dict["respect_submit"]

        with NG911Validator(gdb, respect_submit, messenger=messages, export=True, overwrite_error_tables=overwrite) as validator:
            selected_routines: list[ValidationRoutine] =  [
                r
                for p in _get_routine_parameters(parameters)
                for r in ValidationRoutine.get_selected_from_parameter(p)
            ]
            arcpy.SetProgressor("step", "Running validations...", 0, len(selected_routines) + 2, 1)
            arcpy.SetProgressorPosition()

            routine_parameter_slice = slice(_get_parameter_index(parameters, _get_routine_parameters(parameters)[0].name), None)
            """Slice starting with the index of the first routine selection
            parameter and ending after the last parameter in ``parameters``."""

            # all_validations_selected: bool = (
            #     set(selected_feature_class_names or [])
            #         .issuperset(set(config.required_feature_class_names))
            #     and all(map(all_values_selected, parameters[routine_parameter_slice]))
            # )
            # """True if all required feature classes are selected and all
            # validation routines in all validation groups are selected."""

            for i, routine in enumerate(selected_routines):
                arcpy.SetProgressorLabel(f"Running {routine.name} ({i+1}/{len(selected_routines)})...")
                if routine.takes_feature_class_argument:
                    for fc_name in selected_feature_class_names:
                        routine(validator, fc_name)
                else:
                    routine(validator)
                arcpy.SetProgressorPosition()

            arcpy.SetProgressor("default", "Setting output parameters...")
            param_dict["feature_attribute_error_table"].value = str(validator.fa_error_table) if len(validator.feature_attribute_issues) > 0 else None
            param_dict["gdb_error_table"].value = str(validator.gdb_error_table) if len(validator.gdb_issues) > 0 else None

            if validator.has_errors:
                messages.addWarningMessage(f"Validation completed. There are {validator.error_count} error(s) that must be resolved before submission. See output table(s) for details.")
            elif validator.has_issues:
                messages.addWarningMessage(f"Validation completed without errors! See output table(s) for {validator.issue_count} warning(s)/notice(s).")
            else:
                messages.addMessage(f"Validation completed without errors, warnings, or notices!")


__all__ = ["ValidateGeodatabase"]

            # for routine_parameter_name in ["geodatabase_validations", "general_feature_class_validations", "address_point_validations", "road_centerline_validations"]:
            # for routine_parameter in _get_routine_parameters(parameters):
            #     # for routine in _routine_get_selected(param_dict[routine_parameter_name]):
            #     for routine in ValidationRoutine.get_selected_from_parameter(routine_parameter):
            #         if routine.takes_feature_class_argument:
            #             map(partial(routine, validator), selected_feature_class_names)
            #         else:
            #             routine(validator)


            # for routine in param_dict["geodatabase_validations"].selected_routines:
            #     routine(validator)
            # for routine in param_dict["general_feature_class_validations"].selected_routines:
            #     routine(validator)
            # for routine in param_dict["address_point_validations"].selected_routines:
            #     routine(validator)
            # for routine in param_dict["road_centerline_validations"].selected_routines:
            #     routine(validator)

            # selected_gdb_validations: list[str] = unwrap(param_dict["geodatabase_validations"])
            # if "Check Feature Class List" in selected_gdb_validations:
            #     validator.check_feature_class_list_routine()
            # if "Check Feature Class Configuration" in selected_gdb_validations:
            #     validator.check_feature_class_configuration_routine()
            # if "Check Geodatabase Domains" in selected_gdb_validations:
            #     validator.check_gdb_domains_routine()
            # if "Check Spatial Reference" in selected_gdb_validations:
            #     validator.check_spatial_reference_routine()

            # selected_general_validations: list[str] = unwrap(param_dict["general_feature_class_validations"])
            # for fc_name in param_dict["feature_classes_to_check"].values:
            #     ...
            #     if "Check Unique ID Format" in selected_general_validations:
            #         validator.check_unique_id_format_routine(fc_name)
            #     if "Check Unique ID Frequency" in selected_general_validations:
            #         validator.check_unique_id_frequency_routine(fc_name)
            #     if "Check Required Field Attributes" in selected_general_validations:
            #         validator.check_required_field_attributes_routine(fc_name)
            #     if "Check Attributes Against Domains" in selected_general_validations:
            #         validator.check_attributes_against_domains_routine(fc_name)
            #     if "Check Submission Counts" in selected_general_validations:
            #         validator.check_submission_counts_routine(fc_name)
            #     if "Find Invalid Geometry" in selected_general_validations:
            #         validator.find_invalid_geometry_routine(fc_name)
            #     if "Check Feature Locations" in selected_general_validations:
            #         validator.check_feature_locations_routine(fc_name)

            # selected_ap_validations: list[str] = unwrap(param_dict["address_point_validations"])
            # if "Check ESN and City Attributes (Address Point)" in selected_ap_validations:
            #     validator.check_esn_attribute_address_point_routine()
            # if "Check Next-Gen Against Legacy Fields (Address Point)" in selected_ap_validations:
            #     validator.check_next_gen_against_legacy_fields_address_point_routine()
            # if "Check RCLMatch" in selected_ap_validations:
            #     validator.check_rclmatch_routine()

            # selected_rcl_validations: list[str] = unwrap(param_dict["road_centerline_validations"])
            # if "Check Parities" in selected_rcl_validations:
            #     validator.check_parities_routine()
            # if "Check for Cutbacks" in selected_rcl_validations:
            #     validator.check_for_cutbacks_routine()
            # if "Check Address Range Directionality" in selected_rcl_validations:
            #     validator.check_address_range_directionality_routine()
            # if "Check for Address Range Overlaps" in selected_rcl_validations:
            #     validator.check_for_address_range_overlaps_routine()
            # if "Check ESN and City Attributes (Road Centerline)" in selected_ap_validations:
            #     validator.check_esn_attribute_road_centerline_routine()
            # if "Check Next-Gen Against Legacy Fields (Road Centerline)" in selected_rcl_validations:
            #     validator.check_next_gen_against_legacy_fields_road_centerline_routine()

# Function boilerplate generation code

# GENERAL FEATURE CLASS VALIDATIONS
# import re
# get_name = lambda check: re.sub(r"^_|_$", "", re.sub(r"\W+", "_", check)).lower()
# checks: list[str] = []
# print("\n\n".join(f"""    @routine("{check}", takes_feature_class_argument=True)
#     def {get_name(check)}_routine(self, feature_class_name: str) -> list[ValidationErrorMessage]:
#         \"""Validation routine *{check}*\"""
#         fc_obj: NG911FeatureClass = config.get_feature_class_by_name(feature_class_name)
#         self._precheck(self.check_feature_class_exists(fc_obj.role))
#         errors: list[ValidationErrorMessage] = []
#         raise NotImplementedError
#         return errors"""
#     for check in checks
# ))

# OTHER VALIDATIONS
# import re
# get_name = lambda check: re.sub(r"^_|_$", "", re.sub(r"\W+", "_", check)).lower()
# checks: list[str] = []
# print("\n\n".join(f"""    @routine("{check}")
#     def {get_name(check)}_routine(self) -> list[ValidationErrorMessage]:
#         \"""Validation routine *{check}*\"""
#         errors: list[ValidationErrorMessage] = []
#         raise NotImplementedError
#         return errors"""
#     for check in checks
# ))


# if __name__ == "__main__":
#     raise Exception("This module is a dependency of an ArcGIS Python