from __future__ import annotations

import logging
from copy import deepcopy
from typing import Any, Dict, List, Literal

import numpy as np
from shapely.geometry import Polygon, mapping

from pypilecore.results import SingleCPTBearingResults

_dft_optimize_result_by = [
    "minimum_pile_level",
    "number_of_cpts",
    "number_of_consecutive_pile_levels",
]


def create_grouper_payload(
    cpt_results_dict: Dict[str, SingleCPTBearingResults],
    pile_load_uls: float,
    building_polygon: Polygon | None = None,
    cpt_grid_rotation: float = 0.0,
    gamma_bottom: float = 1.2,
    gamma_shaft: float = 1.2,
    include_centre_to_centre_check: bool = False,
    stiff_construction: bool = False,
    optimize_result_by: List[
        Literal[
            "minimum_pile_level",
            "number_of_cpts",
            "number_of_consecutive_pile_levels",
            "centre_to_centre_check",
        ]
    ]
    | None = _dft_optimize_result_by,  # type: ignore
    resolution: float = 0.5,
    overrule_nan: float = 0.0,
    skip_nan: bool = False,
) -> dict:
    """
    Creates a dictionary with the payload content for the PileCore endpoint
    "/grouper/group_cpts"


    Note
    ------
    The grouper uses pile bearing capacity results calculated by PileCore or other software to
    form  subgroups of the total group of CPT’s belonging to this project.
    Valid subgroups have three characteristics:

        - a maximum variation coefficient of 12% at one or more pile-tip levels. (Variation check
          NEN9997-1 A.3.3.3)
        - a minimum design pile bearing capacity based on the given pile load ULS at one or more
          pile-tip levels. (Bearing check)
        - is spatially coherent, which means there are no other CPTs in between the members
          of the subgroup. (Spatial check)

    Additionally, centre to centre validation (include_centre_to_centre_check; NEN9997-1 3.2.3) can be
    added to the cluster method. This check adds restrictions to the maximum allowable R;c;cal outliers
    and makes sure that the suitable data density requirements for the subgroup are met, by checking the
    centre-to-centre (CTC) distance of the cpts in a regular square grid:

        - A maximum CTC distance of 25 m, if no R;c;cal outliers greater than 30% off the average;
        - A maximum CTC distance of 20 m, if no R;c;cal outliers greater than 40% off the average;
        - A maximum CTC distance of 15 m, if no R;c;cal outliers greater than 50% off the average;
        - All subgroups with R;c;cal outliers greater than 50% of the average are considered invalid.

    The CTC check is performed by drawing a square around each CPT with the CTC dimensions according to the
    outlier criterion and verifying that there is no empty space between squares. The rotation of the squares
    can be provided with the `cpt_grid_rotation` argument. Note that this rotation is assigned to all CPT's and
    therefore should represent the main orientation of the building.

    Parameters
    ----------
    cpt_results_dict:
        Dictionary with key as CPT name and value a SingleCPTBearingResults class.
        Should contain at least 2 entries.
    pile_load_uls
        ULS load in kN. Used to determine if a grouping configuration is valid.
    stiff_construction
        Default is False
        Attribute use to get the xi3 and xi4 value. True if it is a stiff construction
    optimize_result_by
        Default is "minimum_pile_level", "number_of_cpts", "number_of_consecutive_pile_levels"
        Attribute that states how to sort the result and find groups.
        Based on the filter method, a selection of valid subgroups are included in the report. The following
        filters are available:

            - Number_of_cpts: the grouper adds filters to make the group as big as possible to try and get a
              uniform pile tip level for most CPT’s.
            - Number_of_consecutive_pile_levels; the grouper adds filters to get groups that contain consecutive
              pile tip levels to ensure a consistent soil layer is used.
            - Minimum_pile_level; the grouper adds filters to return groups that optimize pile length to try and
              optimize or reduce material use.
            - Centre_to_centre_check; the grouper adds filters to favour groups that are valid according to the
              centre to centre rules of the NEN9997-1 3.2.3.

    gamma_shaft
        Default is 1.2
        Safety factor shaft design bearing capacity
    gamma_bottom
        Default is 1.2
        Safety factor bottom design bearing capacity
    include_centre_to_centre_check:
        Default is False
        Flag that indicates if the cluster algorithm performs a centre to centre validation of the CPT’s of the
        generated subgroups or not according to NEN9997-1 3.2.3. If the group doesn’t comply to the check, the
        result of the group is deleted from the result.
    building_polygon:
        Default is None
        Polygon of the contour of the building. If None the building Polygon in generated based on the
        convex_hull of the CPT points.
    cpt_grid_rotation:
        Default is 0.0.
        Rotation of the squares used in the centre to centre validation [degrees]
    resolution:
        Default is 0.5
        The resolution of clusters algorithm. If resolution is 1 the cluster boundary conditions can be met
        (number clusters is number CPTs). Depending on the number of CPTs this can take some time.
    overrule_nan:
        Default is 0.0
        The default behavior is to replace NaN with zero, for one of the following
        attributes ["R_b_cal", "F_nk_d", "R_s_cal"].
    skip_nan:
        Default is False
        If True the CPTs are skipped that have NaN values in one of the following
        attributes ["R_b_cal", "F_nk_d", "R_s_cal"], this means that they are not used in the grouper method.

    Raises
    ------
    ValueError:
        - if NaN values are present in negative friction, bottom or shaft bearing_capacity

        - if x or y coordinate is None

        - if pile tip levels don't macht for all SingleCPTBearingResults

        - if less than 2 valid CPTs are provided


    Returns
    -------
    payload:
        Dictionary with the payload content for the PileCore endpoint
        "/grouper/group_cpts"
    """
    # create default payload object
    payload: Dict[str, Any] = {
        "cpt_grid_rotation": cpt_grid_rotation,
        "gamma_bottom": gamma_bottom,
        "gamma_shaft": gamma_shaft,
        "include_centre_to_centre_check": include_centre_to_centre_check,
        "pile_load_uls": pile_load_uls,
        "stiff_construction": stiff_construction,
        "resolution": resolution,
        "optimize_result_by": optimize_result_by,
    }

    # set source building polygon in payload
    if building_polygon is not None:
        payload["building_polygon"] = mapping(building_polygon)

    # set bearing capacity in payload
    cpt_objects = []
    pile_tip_level_object = {}
    for name, cpt_result in cpt_results_dict.items():
        has_nan = False
        # check if coordinate are set
        if cpt_result.soil_properties.x is None or cpt_result.soil_properties.y is None:
            raise ValueError(
                f" CPT {name} does not have a x-coordinate or y-coordinate"
            )

        for item in ["R_b_cal", "F_nk_d", "R_s_cal"]:
            if np.isnan(cpt_result.table.__getattribute__(item)).any():
                if skip_nan:
                    has_nan = True
                    logging.warning(
                        f"CPT {name} has NaN values are present in column {item}. "
                        f"Not included in grouper payload."
                    )

                    break
                else:
                    logging.warning(
                        f"CPT {name} has NaN values are present in column {item}. "
                        f"Replace NaN with {overrule_nan}."
                    )
        # skip CPT that are not valid.
        if has_nan:
            continue

        # map pile tip levels to object
        pile_tip_level_object[name] = cpt_result.table.pile_tip_level_nap.tolist()

        # add bearing capacity result to object
        cpt_objects.append(
            {
                "bottom_bearing_capacity": np.nan_to_num(
                    cpt_result.table.R_b_cal, nan=overrule_nan
                ).tolist(),
                "negative_friction": np.nan_to_num(
                    cpt_result.table.F_nk_d, nan=overrule_nan
                ).tolist(),
                "shaft_bearing_capacity": np.nan_to_num(
                    cpt_result.table.R_s_cal, nan=overrule_nan
                ).tolist(),
                "name": name,
                "coordinates": {
                    "x": cpt_result.soil_properties.x,
                    "y": cpt_result.soil_properties.y,
                },
            }
        )

    if not len(cpt_objects) >= 2:
        raise ValueError(
            "The PileCore grouper requires at least 2 CPTs with valid bearing capacity."
        )

    payload["cpt_objects"] = cpt_objects

    # validate pile tip levels
    raw_lengths = [
        frozenset(np.round(values, 1)) for values in pile_tip_level_object.values()
    ]
    if len(list(set(raw_lengths))) > 1:
        msg = "The PileCore grouper requires all CPTs to have a valid bearing capacity for all pile tip levels. \n"
        for name, pile_tip_level in pile_tip_level_object.items():
            msg += (
                f"Pile tip levels are not similar for CPT {name} with length {len(pile_tip_level)}, "
                f"upper boundary: {max(pile_tip_level)}, lower boundary: {min(pile_tip_level)}. \n"
            )
        raise ValueError(msg)
    payload["pile_tip_level"] = sorted(list(raw_lengths[0]), reverse=True)

    return payload


def create_grouper_report_payload(
    grouper_payload: dict,
    grouper_response: dict,
    project_name: str,
    project_id: str,
    author: str,
) -> dict:
    """
    Creates a dictionary with the payload content for the PileCore endpoint
    "grouper/generate_grouper_report"

    Parameters
    ----------
    grouper_payload:
       The resulting payload of a call to `create_grouper_payload()`
    grouper_response:
       The resulting response of a call to `get_groups_api_result()`
    project_name:
        The name of the project.
    project_id:
        The identifier (code) of the project.
    author:
        The author of the report.

    Returns
    -------
    report_payload:
        Dictionary with the payload content for the PileCore endpoint
        "grouper/generate_grouper_report"
    """
    report_payload = deepcopy(grouper_payload)
    report_payload.update(
        dict(
            sub_groups=grouper_response["sub_groups"],
            author=author,
            project_number=project_id,
            project_name=project_name,
        ),
    )
    # remove not used attributes
    _ = report_payload.pop("pile_tip_level")
    _ = report_payload.pop("cpt_objects")
    return report_payload
