import pandas as pd
import dataiku
import numpy as np
from copy import deepcopy
import logging
import control_charts_static

# Configure logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def get_control_chart_factors(chart_type):
    """
    Returns the appropriate control chart constants based on chart type.

    Parameters:
    - chart_type (str): One of 'X_Bar', 'XS_Bar', or 'I_MR'

    Returns:
    - dict: Control chart constants for the specified chart type

    Raises:
    - ValueError: If chart_type is invalid
    """
    if chart_type == "X_Bar":
        # X̅–R chart uses A2, D3, D4
        return control_charts_static.x_bar_r_chart_constants
    elif chart_type == "XS_Bar":
        # X̅–S chart uses A3, B3, B4
        return control_charts_static.x_bar_s_chart_constants
    elif chart_type == "I_MR":
        # I-MR chart uses E2, D3, D4
        return control_charts_static.i_mr_chart_constants
    else:
        raise ValueError(f"Invalid chart_type '{chart_type}'. Must be one of: 'X_Bar', 'XS_Bar', 'I_MR'")


def generate_capability_metrics_data(df, measurement_columns, chart_type=None):
    """
    Computes Cp, Cpk, Cpm, Cpkm, mean, and std for each column.
    Saves mean/std to Dataiku project variables.
    Returns a single-row DataFrame with flattened column names.

    Parameters:
    - df: Input DataFrame
    - measurement_columns: List of measurement column names
    - chart_type (str): One of 'X_Bar', 'XS_Bar', or 'I_MR'. If None, retrieved from project variables.
    """

    project = dataiku.api_client().get_default_project()
    variables = project.get_variables()
    project_vars = variables["standard"]

    # Get chart_type from project variables if not provided
    if chart_type is None:
        chart_type = project_vars.get("chart_type", "X_Bar")

    result_dict = {}

    for col in measurement_columns:
        df[col] = pd.to_numeric(df[col], errors="coerce")
        numeric_series = df[col].dropna()

        if numeric_series.empty:
            logger.warning(f"Column '{col}' has no valid data.")
            continue

        # Save mean and std dev to project variables
        mean = numeric_series.mean()
        std_dev = numeric_series.std()
        project_vars[f"mean_{col}"] = round(mean, 5)
        project_vars[f"std_{col}"] = round(std_dev, 5)

        # Compute subgroup stats and sigma based on chart_type
        if chart_type == "I_MR":
            # For I-MR, compute moving range
            df[f"MR_{col}"] = df[col].diff().abs()
            MR_bar = df[f"MR_{col}"].mean()

            if MR_bar <= 0:
                logger.warning(f"MR̄ = 0 for '{col}' → No variation. Skipping metrics.")
                continue

            # Use d2 for moving range (n=2)
            chart_constants = get_control_chart_factors(chart_type)
            factors = chart_constants.get(2)  # Moving range always uses n=2
            if not factors:
                logger.warning("No factors for I-MR chart.")
                continue

            d2 = factors["d2"]
            sigma = MR_bar / d2

        else:
            # X_Bar or XS_Bar: compute subgroup stats
            if "_subgroup" not in df.columns:
                logger.info(f"'_subgroup' column missing. Skipping capability indices for '{col}'.")
                continue

            agg_dict = {"range": (col, lambda x: x.max() - x.min()), "subgroup_size": (col, "count")}

            if chart_type == "XS_Bar":
                agg_dict["std"] = (col, "std")

            subgroup_stats = df.groupby("_subgroup").agg(**agg_dict).reset_index()

            subgroup_stats = subgroup_stats[subgroup_stats["subgroup_size"] > 0]
            if subgroup_stats.empty:
                logger.warning(f"No valid subgroups for '{col}'.")
                continue

            avg_subgroup_size = subgroup_stats["subgroup_size"].mean()

            if avg_subgroup_size < 2:
                logger.warning(f"Subgroup size < 2 for '{col}' → Can't compute sigma. Skipping metrics.")
                continue

            n = int(round(avg_subgroup_size))
            chart_constants = get_control_chart_factors(chart_type)
            available_sizes = np.array(list(chart_constants.keys()))
            closest_n = available_sizes[np.abs(available_sizes - n).argmin()]
            factors = chart_constants.get(closest_n)

            if not factors:
                logger.warning(f"No factors for size {closest_n}.")
                continue

            if chart_type == "X_Bar":
                # Use d2 for X-bar & R chart
                R_bar = subgroup_stats["range"].mean()
                logger.info(f"For variable '{col}', R̄ = {R_bar:.5f} and avg_subgroup_size = {avg_subgroup_size:.2f}")

                if R_bar <= 0:
                    logger.warning(f"R̄ = 0 for '{col}' → No within-subgroup variation. Skipping metrics.")
                    continue

                d2 = factors["d2"]
                sigma = R_bar / d2

            elif chart_type == "XS_Bar":
                # Use c4 for X-bar & S chart
                S_bar = subgroup_stats["std"].mean()
                logger.info(f"For variable '{col}', S̄ = {S_bar:.5f} and avg_subgroup_size = {avg_subgroup_size:.2f}")

                if S_bar <= 0:
                    logger.warning(f"S̄ = 0 for '{col}' → No within-subgroup variation. Skipping metrics.")
                    continue

                c4 = factors["c4"]
                sigma = S_bar / c4

        if sigma == 0:
            logger.warning(f"Sigma = 0 for '{col}'.")
            continue

        USL = variables["standard"].get(f"USL_{col}")
        LSL = variables["standard"].get(f"LSL_{col}")
        target = variables["standard"].get(f"TARGET_{col}")

        if USL is None or LSL is None:
            logger.warning(f"Missing USL or LSL for '{col}'. Skipping.")
            continue

        Cp = (float(USL) - float(LSL)) / (6 * sigma)
        Cpk = min((float(USL) - mean), (mean - float(LSL))) / (3 * sigma)

        result_dict[f"{col}_cp"] = Cp
        result_dict[f"{col}_cpk"] = Cpk

        if target is not None:
            dev_sq = ((mean - target) / sigma) ** 2
            result_dict[f"{col}_cpm"] = Cp / np.sqrt(1 + dev_sq)
            result_dict[f"{col}_cpkm"] = Cpk / np.sqrt(1 + dev_sq)
        else:
            logger.info(f"Target missing for '{col}'. Skipping Cpm/Cpkm.")

        logger.info(f"Metrics generated for '{col}': Cp = {Cp:.4f}, Cpk = {Cpk:.4f}")

    # Save updated variables
    variables["standard"] = project_vars
    project.set_variables(variables)

    # Return as single-row DataFrame
    return pd.DataFrame([result_dict])


def generate_process_capability_visualization_data(df_input, measurement_columns, chart_type=None):
    """
    Prepares data for process capability visualization (distribution charts).

    - Adds USL, LSL, and target values as columns.
    - Computes mean and std for each measurement column.
    - Handles chart_type to ensure compatible calculations:
        * X_Bar / XS_Bar: same behavior (grouped data possible)
        * I_MR: individual values only, no subgroup concept
    """
    df = df_input.copy()
    project = dataiku.api_client().get_default_project()
    all_vars = project.get_variables()
    project_vars = all_vars["standard"]

    # Normalize chart_type
    chart_type = project_vars.get("chart_type", "")

    columns_to_include = measurement_columns.copy()

    for col in measurement_columns:
        logger.info(f"[SPC] Processing column '{col}' for visualization (chart_type={chart_type})")

        USL = project_vars.get(f"USL_{col}")
        LSL = project_vars.get(f"LSL_{col}")
        target_value = project_vars.get(f"target_{col}")

        if USL is not None:
            df[f"USL_{col}"] = USL
            columns_to_include.append(f"USL_{col}")
        else:
            logger.warning(f"USL not found for column '{col}'. Skipping USL column for visualization.")

        if LSL is not None:
            df[f"LSL_{col}"] = LSL
            columns_to_include.append(f"LSL_{col}")
        else:
            logger.warning(f"LSL not found for column '{col}'. Skipping LSL column for visualization.")

        if target_value is not None:
            df[f"target_{col}"] = target_value
            columns_to_include.append(f"target_{col}")

        # ---- Compute mean / std according to chart type ----
        if chart_type == "I_MR":
            # No subgrouping, just direct mean/std on individual values
            col_mean = df[col].mean()
            col_std = df[col].std()
        else:
            # Subgroup-based data (X_Bar or XS_Bar) — safe default
            col_mean = df[col].mean()
            col_std = df[col].std()

        project_vars[f"mean_{col}"] = round(col_mean, 5)
        project_vars[f"std_{col}"] = round(col_std, 5)

        logger.info(f"[SPC] {col} → mean={col_mean:.4f}, std={col_std:.4f}")

    # Update project variables
    all_vars["standard"] = project_vars
    project.set_variables(all_vars)

    df_filtered = df[list(set(columns_to_include) & set(df.columns))]
    return df_filtered


def generate_selected_study_data_enhanced(df_input, measurement_columns, chart_type):
    """
    Enhances the study data with rank, subgroup, control chart limits, target values,
    and constant zone/range columns from project variables.

    Parameters:
    - df_input: Input DataFrame
    - measurement_columns: List of measurement column names
    - chart_type (str): One of 'X_Bar', 'XS_Bar', or 'I_MR'
    """
    df = df_input.copy()

    if "_rank" not in df.columns:
        logger.info("'_rank' column not found. Creating it based on index for study data enhancement.")
        df["_rank"] = range(1, len(df) + 1)

    project_vars = dataiku.get_custom_variables(typed=True)

    if chart_type == "I_MR":
        # For I_MR charts, use subgroup_size = 1 (no actual subgrouping needed)
        subgroup_size = 1
    else:
        # For X_Bar and XS_Bar charts, validate subgroup_size from project variables
        subgroup_size_str = project_vars.get("subgroup_size", None)

        if subgroup_size_str is None:
            raise ValueError("Project variable 'subgroup_size' is not defined. " "Cannot compute control chart inputs for subgroup charts.")

        try:
            subgroup_size = int(subgroup_size_str)
            if subgroup_size <= 0:
                raise ValueError("Subgroup size must be a positive integer.")
        except (ValueError, TypeError) as e:
            raise ValueError(f"Invalid 'subgroup_size' project variable: {subgroup_size_str}. Error: {e}")

    # Create _subgroup column for all chart types
    df["_subgroup"] = ((df["_rank"] - 1) // subgroup_size + 1).astype(str).str.zfill(3)

    date_time_column = None
    for col in df.columns:
        if "date" in col.lower() or "time" in col.lower() or "timestamp" in col.lower():
            try:
                pd.to_datetime(df[col], errors="raise")
                date_time_column = col
                logger.info(f"Identified '{col}' as a date/time column for study data enhancement.")
                break
            except (ValueError, TypeError):
                pass
    if not date_time_column and df.index.name is None:
        try:
            df.index = pd.to_datetime(df.index)
            date_time_column = df.index.name if df.index.name else "index_timestamp"
            df.index.name = date_time_column
            df = df.reset_index()
            logger.info("Identified index as a date/time for study data enhancement.")
        except (ValueError, TypeError):
            logger.warning("Could not identify index as date/time for study data enhancement.")

    new_columns_to_add = []

    for col in measurement_columns:
        df[col] = pd.to_numeric(df[col], errors="coerce")

        # Compute statistics based on chart type
        if chart_type == "I_MR":
            # I-MR chart: individual values and moving range
            # Calculate moving range (difference between consecutive values)
            df[f"MR_{col}"] = df[col].diff().abs()

            # For I-MR, we use all data (no subgrouping)
            X_bar = df[col].mean()
            MR_bar = df[f"MR_{col}"].mean()
            avg_subgroup_size = 2  # Moving range always uses n=2

            new_columns_to_add.append(f"MR_{col}")

        else:
            # X-bar & R or X-bar & S: compute subgroup statistics
            agg_dict = {"mean": (col, "mean"), "range": (col, lambda x: x.max() - x.min()), "subgroup_size": (col, "count")}

            if chart_type == "XS_Bar":
                # Add standard deviation for X-bar & S chart
                agg_dict["std"] = (col, "std")

            subgroup_stats = df.groupby("_subgroup").agg(**agg_dict).reset_index()
            subgroup_stats = subgroup_stats[subgroup_stats["subgroup_size"] > 0]

            if subgroup_stats.empty:
                logger.warning(f"No valid subgroups found for column '{col}'. Skipping control chart limits.")
                continue

            X_bar = subgroup_stats["mean"].mean()
            avg_subgroup_size = subgroup_stats["subgroup_size"].mean()

            if avg_subgroup_size < 2:
                logger.warning(f"Average subgroup size for column '{col}' is less than 2. Skipping control chart limits.")
                continue

        n = int(round(avg_subgroup_size))

        # Get appropriate control chart constants based on chart_type
        chart_constants = get_control_chart_factors(chart_type)
        available_sizes = np.array(list(chart_constants.keys()))
        closest_n = available_sizes[np.abs(available_sizes - n).argmin()]
        factors = chart_constants.get(closest_n)
        if not factors:
            logger.warning(f"Control chart factors not found for subgroup size {closest_n}.")
            continue

        # Extract factors and compute control limits based on chart type
        if chart_type == "X_Bar":
            # X̅–R chart uses A2, D3, D4
            A2, D3, D4 = factors["A2"], factors["D3"], factors["D4"]
            R_bar = subgroup_stats["range"].mean()

            UCL_Xbar = X_bar + A2 * R_bar
            LCL_Xbar = X_bar - A2 * R_bar
            UCL_R = D4 * R_bar
            LCL_R = D3 * R_bar

            # Add subgroup statistics to DataFrame
            subgroup_mean_col = f"mean_{col}"
            subgroup_range_col = f"range_{col}"
            subgroup_stats_renamed = subgroup_stats[["_subgroup", "mean", "range"]].rename(columns={"mean": subgroup_mean_col, "range": subgroup_range_col})
            df = pd.merge(df, subgroup_stats_renamed, on="_subgroup", how="left")
            new_columns_to_add.extend([subgroup_mean_col, subgroup_range_col])

            df[f"UCL_Xbar_{col}"] = UCL_Xbar
            df[f"LCL_Xbar_{col}"] = LCL_Xbar
            df[f"UCL_R_{col}"] = UCL_R
            df[f"LCL_R_{col}"] = LCL_R
            new_columns_to_add.extend([f"UCL_Xbar_{col}", f"LCL_Xbar_{col}", f"UCL_R_{col}", f"LCL_R_{col}"])

        elif chart_type == "XS_Bar":
            # X̅–S chart uses A3, B3, B4
            A3, B3, B4 = factors["A3"], factors["B3"], factors["B4"]
            S_bar = subgroup_stats["std"].mean()

            UCL_Xbar = X_bar + A3 * S_bar
            LCL_Xbar = X_bar - A3 * S_bar
            UCL_S = B4 * S_bar
            LCL_S = B3 * S_bar

            # Add subgroup statistics to DataFrame
            subgroup_mean_col = f"mean_{col}"
            subgroup_std_col = f"std_{col}"
            subgroup_stats_renamed = subgroup_stats[["_subgroup", "mean", "std"]].rename(columns={"mean": subgroup_mean_col, "std": subgroup_std_col})
            df = pd.merge(df, subgroup_stats_renamed, on="_subgroup", how="left")
            new_columns_to_add.extend([subgroup_mean_col, subgroup_std_col])

            df[f"UCL_Xbar_{col}"] = UCL_Xbar
            df[f"LCL_Xbar_{col}"] = LCL_Xbar
            df[f"UCL_S_{col}"] = UCL_S
            df[f"LCL_S_{col}"] = LCL_S
            new_columns_to_add.extend([f"UCL_Xbar_{col}", f"LCL_Xbar_{col}", f"UCL_S_{col}", f"LCL_S_{col}"])

        elif chart_type == "I_MR":
            # I-MR chart uses E2, D3, D4
            E2, D3, D4 = factors["E2"], factors["D3"], factors["D4"]

            UCL_I = X_bar + E2 * MR_bar
            LCL_I = X_bar - E2 * MR_bar
            UCL_MR = D4 * MR_bar
            LCL_MR = D3 * MR_bar

            df[f"UCL_I_{col}"] = UCL_I
            df[f"LCL_I_{col}"] = LCL_I
            df[f"UCL_MR_{col}"] = UCL_MR
            df[f"LCL_MR_{col}"] = LCL_MR
            df[f"centerline_{col}"] = X_bar
            df[f"MR_centerline_{col}"] = MR_bar
            new_columns_to_add.extend([f"UCL_I_{col}", f"LCL_I_{col}", f"UCL_MR_{col}", f"LCL_MR_{col}", f"centerline_{col}", f"MR_centerline_{col}"])

        target_value = project_vars.get(f"target_{col}")
        if target_value is not None:
            df[f"target_{col}"] = target_value
            new_columns_to_add.append(f"target_{col}")
        else:
            logger.info(f"Target variable 'target_{col}' not found for column '{col}'.")

    for key, value in project_vars.items():
        if key.endswith("_lower_zone") or key.endswith("_upper_zone"):
            df[key] = value
            new_columns_to_add.append(key)

    for key, value in project_vars.items():
        if key.endswith("_selected_range_min") or key.endswith("_selected_range_max"):
            df[key] = value
            new_columns_to_add.append(key)

    columns_to_keep = measurement_columns + ["_rank", "_subgroup"] + list(set(new_columns_to_add))
    if date_time_column and date_time_column not in columns_to_keep:
        columns_to_keep.append(date_time_column)

    final_df_columns = list(set(columns_to_keep).intersection(df.columns))
    return df[final_df_columns]


def insight_builder(project, base_info, variable_name, name_suffix, measures, reference_column=None, custom_colors=None, custom_formula=None, reference_lines=None, additional_full_measures=None):
    info = deepcopy(base_info)
    info["name"] = f"{name_suffix} - {variable_name}"
    chart_def = info["params"]["def"]
    chart_def["name"] = f"{name_suffix} - {variable_name}"

    # Fill in basic string-based column measures
    for idx, col in enumerate(measures):
        if isinstance(col, str):
            chart_def["genericMeasures"][idx]["column"] = col
            if col[:3] not in ("UCL", "LCL"):
                # legend update
                if name_suffix == "X-bar":
                    display_label = f"Mean of {variable_name}"
                elif name_suffix == "R":
                    display_label = f"Range of {variable_name}"
                elif name_suffix == "S":
                    display_label = f"Std. dev. of {variable_name}"
                elif name_suffix == "MR (Moving Range)":
                    display_label = f"Mean of {variable_name}"
                elif name_suffix == "I (Individual)":
                    display_label = f"{variable_name}"

                chart_def["genericMeasures"][idx]["displayLabel"] = display_label
        else:
            chart_def["genericMeasures"][idx] = col

    # Add additional full measure blocks (e.g., CPM, CPKM)
    if additional_full_measures:
        chart_def["genericMeasures"].extend(additional_full_measures)

    # ---- Y-axis title: set explicitly with the variable name ----
    y_title = None
    if name_suffix == "X-bar":
        y_title = f"Mean of {variable_name}"
    elif name_suffix == "R":
        y_title = f"Range of {variable_name}"
    elif name_suffix == "S":
        y_title = f"Std. dev. of {variable_name}"
    elif name_suffix == "MR (Moving Range)":
        y_title = f"Mean of {variable_name}"
    elif name_suffix == "I (Individual)":
        y_title = f"{variable_name}"

    if y_title:
        for ax in chart_def.get("yAxesFormatting", []):
            if ax.get("id") == "y_left_0":
                ax["axisTitle"] = y_title

    # Reference line (aggregated column)
    if reference_column:
        chart_def["referenceLines"][0]["aggregatedColumn"]["column"] = reference_column

    # Merge custom colors (don’t overwrite existing mappings)
    if custom_colors:
        chart_def.setdefault("colorOptions", {}).setdefault("customColors", {}).update(custom_colors)

    # For I charts, changing variable and renaming axis
    if name_suffix in ("I (Individual)", "MR (Moving Range)"):
        chart_def["genericDimension0"][0]["column"] = "_rank"
        chart_def["genericDimension0"][0]["sort"]["type"] = "AGGREGATION"
        chart_def["genericDimension0"][0]["sort"]["label"] = "UCL"
        chart_def["xAxisFormatting"]["axisTitle"] = "Observation"

    # Optional custom formula for second measure
    if custom_formula and len(chart_def["genericMeasures"]) > 1:
        chart_def["genericMeasures"][1]["customFunction"] = custom_formula

    # Dataset column reference lines (e.g., USL/LSL)
    if reference_lines:
        for idx, col in enumerate(reference_lines):
            chart_def["referenceLines"][idx]["datasetColumn"]["column"] = col

    return project.create_insight(info).insight_id


def make_metric_block(column_name, label):
    return {
        "column": column_name,
        "function": "AVG",
        "type": "NUMERICAL",
        "displayed": True,
        "isA": "measure",
        "displayAxis": "axis1",
        "displayType": "column",
        "computeMode": "NORMAL",
        "computeModeDim": 0,
        "multiplier": "Auto",
        "decimalPlaces": 2,
        "digitGrouping": "DEFAULT",
        "useParenthesesForNegativeValues": False,
        "shouldFormatInPercentage": False,
        "hideTrailingZeros": False,
        "prefix": "",
        "suffix": "",
        "showValue": True,
        "displayLabel": label,
        "showDisplayLabel": True,
        "labelPosition": "BOTTOM",
        "labelFontSize": 16,
        "percentile": 50.0,
        "isCustomPercentile": False,
        "kpiTextAlign": "CENTER",
        "kpiValueFontSizeMode": "RESPONSIVE",
        "kpiValueFontSize": 32,
        "responsiveTextAreaFill": 100,
        "valueTextFormatting": {"fontSize": 11, "fontColor": "#333", "hasBackground": False},
        "labelTextFormatting": {"fontSize": 11, "fontColor": "#333", "hasBackground": False},
        "valuesInChartDisplayOptions": {
            "displayValues": True,
            "textFormatting": {"fontSize": 11, "fontColor": "AUTO", "hasBackground": False, "backgroundColor": "#D9D9D9BF"},
            "addDetails": False,
            "additionalMeasures": [],
        },
        "colorRules": [],
    }


def is_under_control(df, parameter, ucl_location, lcl_location, ucl_variability, lcl_variability, chart_type="X_Bar"):
    """
    Determine if a parameter is statistically under control using:
    - Rule 1: Any location statistic outside UCL/LCL (Xbar, I)
    - Rule 2: Any variability statistic outside UCL/LCL (R, S, or MR)

    Parameters:
    - df: DataFrame with aggregated values
    - parameter: base column name of the parameter (e.g. "pressure")
    - ucl_location/lcl_location: numeric limits for location chart (Xbar for X_Bar/XS_Bar, I for I_MR)
    - ucl_variability/lcl_variability: numeric limits for variability chart (R for X_Bar, S for XS_Bar, MR for I_MR)
    - chart_type (str): One of 'X_Bar', 'XS_Bar', or 'I_MR'

    Returns:
    - is_controlled (bool): True if under control
    - violations (dict): {"rule1_location": bool, "rule1_variability": bool}
    """
    # Determine sort column and data columns based on chart type
    if chart_type == "X_Bar":
        sort_column = "_subgroup"
        location_col = f"mean_{parameter}"
        variability_col = f"range_{parameter}"
    elif chart_type == "XS_Bar":
        sort_column = "_subgroup"
        location_col = f"mean_{parameter}"
        variability_col = f"std_{parameter}"
    elif chart_type == "I_MR":
        sort_column = "_rank"
        location_col = parameter  # Individual values
        variability_col = f"MR_{parameter}"
    else:
        logger.warning(f"Invalid chart_type '{chart_type}'. Defaulting to X_Bar.")
        sort_column = "_subgroup"
        location_col = f"mean_{parameter}"
        variability_col = f"range_{parameter}"

    df = df.sort_values(by=sort_column).reset_index(drop=True)

    # Check if columns exist
    if location_col not in df.columns:
        logger.warning(f"Column '{location_col}' not found in dataframe for chart_type '{chart_type}'.")
        return False, {"rule1_location": True, "rule1_variability": True}

    if variability_col not in df.columns:
        logger.warning(f"Column '{variability_col}' not found in dataframe for chart_type '{chart_type}'.")
        return False, {"rule1_location": True, "rule1_variability": True}

    # Rule 1 (Location): values outside control limits
    out_of_bounds_location = (df[location_col] > ucl_location) | (df[location_col] < lcl_location)
    rule1_location_violation = out_of_bounds_location.any()

    # Rule 2 (Variability): values outside control limits
    out_of_bounds_variability = (df[variability_col] > ucl_variability) | (df[variability_col] < lcl_variability)
    rule1_variability_violation = out_of_bounds_variability.any()

    violations = {"rule1_location": rule1_location_violation, "rule1_variability": rule1_variability_violation}

    is_controlled = not any(violations.values())
    return is_controlled, violations
