import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

ID_LIKE_COLUMNS = {
    "id",
    "device_id",
    "node_id",
    "sensor_id",
    "monitor_id",
    "junction_id",
}


def _normalize_time_frame(data: pd.DataFrame) -> pd.DataFrame:
    """返回按时间排序的副本，并尽量将 time 列解析为时间类型。"""
    data = data.copy()
    if "time" in data.columns:
        data["time"] = pd.to_datetime(data["time"], errors="coerce")
        data = data.sort_values(["time"]).reset_index(drop=True)
    return data


def _select_pressure_columns(data: pd.DataFrame) -> tuple[list[str], list[str]]:
    """区分需要清洗的数值列与需要原样保留的列。"""
    value_cols: list[str] = []
    keep_cols: list[str] = []
    for col in data.columns:
        if col == "time":
            continue
        col_key = col.lower()
        if col_key in ID_LIKE_COLUMNS or col_key.endswith("_id"):
            keep_cols.append(col)
            continue
        numeric = pd.to_numeric(data[col], errors="coerce")
        if numeric.notna().sum() == 0 or numeric.nunique(dropna=True) <= 1:
            keep_cols.append(col)
        else:
            value_cols.append(col)
    return value_cols, keep_cols


def _robust_scale(values: pd.Series) -> float:
    """基于 MAD 计算稳健尺度。"""
    series = pd.to_numeric(values, errors="coerce").dropna()
    if series.empty:
        return 1.0
    median = series.median()
    mad = (series - median).abs().median()
    if pd.notna(mad) and mad > 0:
        return float(1.4826 * mad)
    iqr = series.quantile(0.75) - series.quantile(0.25)
    if pd.notna(iqr) and iqr > 0:
        return float(iqr / 1.349)
    std = series.std()
    if pd.notna(std) and std > 0:
        return float(std)
    return 1.0


def _shrink_toward_baseline(observed: float, baseline: float, scale: float) -> float:
    """把观测值向基线值收缩，scale 越小，修复越强。"""
    if pd.isna(observed):
        return baseline
    if pd.isna(baseline):
        return observed
    diff = observed - baseline
    weight = scale / (abs(diff) + scale)
    return float(baseline + diff * weight)


def _infer_time_frequency(time_values: pd.Series | pd.Index) -> pd.Timedelta:
    """从时间序列中推断采样频率，失败时默认 15 分钟。"""
    parsed = pd.to_datetime(pd.Series(time_values), errors="coerce").dropna().sort_values()
    if len(parsed) < 2:
        return pd.Timedelta(minutes=15)

    diffs = parsed.diff().dropna()
    diffs = diffs[diffs > pd.Timedelta(0)]
    if diffs.empty:
        return pd.Timedelta(minutes=15)

    mode = diffs.mode()
    return mode.iloc[0] if not mode.empty else diffs.median()


def _build_local_pressure_baseline(series: pd.Series) -> pd.Series:
    """基于局部插值与中值滤波构造平滑基线。"""
    baseline = _safe_time_interpolate(series)
    baseline = baseline.rolling(window=5, center=True, min_periods=1).median()
    baseline = _safe_time_interpolate(baseline)
    return baseline.ffill().bfill()


def _build_seasonal_pressure_baseline(series: pd.Series) -> pd.Series:
    """按一天内的同一时刻构造季节性基线，适合日周期压力数据。"""
    if not isinstance(series.index, pd.DatetimeIndex):
        return pd.Series(np.nan, index=series.index, dtype=float)

    slot_labels = pd.Series(series.index.strftime("%H:%M:%S"), index=series.index)
    return series.groupby(slot_labels).transform("median")


def _detect_pressure_spikes(series: pd.Series, local_baseline: pd.Series) -> pd.Series:
    """识别单点异常上升/下降尖峰，避免过度修正正常波动。"""
    residual = series - local_baseline
    neighbor_center = (series.shift(1) + series.shift(-1)) / 2
    curvature = series - neighbor_center

    residual_scale = max(_robust_scale(residual), 1e-6)
    curvature_scale = max(_robust_scale(curvature), 1e-6)
    direction_flip = ((series - series.shift(1)) * (series.shift(-1) - series) < 0).fillna(False)

    return (
        residual.abs() > 3.5 * residual_scale
    ) & (
        curvature.abs() > 3.0 * curvature_scale
    ) & direction_flip


def _fill_pressure_gaps(
    original: pd.Series,
    repaired: pd.Series,
    local_baseline: pd.Series,
    seasonal_baseline: pd.Series,
) -> pd.Series:
    """短缺口用局部插值，长缺口优先使用同一时刻的季节性轨迹。"""
    missing_mask = original.isna()
    if not missing_mask.any():
        return repaired

    gap_groups = (missing_mask != missing_mask.shift(fill_value=False)).cumsum()
    gap_lengths = missing_mask.groupby(gap_groups).transform("sum").where(missing_mask, 0)

    filled = repaired.copy()
    short_gap_mask = missing_mask & (gap_lengths < 4)
    long_gap_mask = missing_mask & ~short_gap_mask

    filled[short_gap_mask] = local_baseline[short_gap_mask]
    long_gap_fill = seasonal_baseline.where(seasonal_baseline.notna(), local_baseline)
    filled[long_gap_mask] = long_gap_fill[long_gap_mask]
    return filled


def _clean_pressure_series(series: pd.Series) -> pd.Series:
    """清洗单个压力时间序列。"""
    series = pd.to_numeric(series, errors="coerce").astype(float)
    local_baseline = _build_local_pressure_baseline(series)
    spike_mask = _detect_pressure_spikes(series, local_baseline)

    repaired = series.copy()
    repaired[spike_mask] = local_baseline[spike_mask]

    seasonal_baseline = _build_seasonal_pressure_baseline(repaired)
    repaired = _fill_pressure_gaps(series, repaired, local_baseline, seasonal_baseline)

    if repaired.isna().any():
        repaired = repaired.where(repaired.notna(), local_baseline)
    return repaired.ffill().bfill()


def _format_time_column(data: pd.DataFrame) -> pd.DataFrame:
    """统一输出时间格式，方便下游直接按 ISO 字符串解析。"""
    if "time" not in data.columns:
        return data

    formatted = data.copy()
    time_values = pd.to_datetime(formatted["time"], errors="coerce")
    if time_values.isna().all():
        return formatted

    if time_values.dt.tz is not None:
        time_strings = time_values.dt.strftime("%Y-%m-%dT%H:%M:%S%z")
        time_strings = time_strings.str.replace(
            r"([+-]\d{2})(\d{2})$",
            r"\1:\2",
            regex=True,
        )
    else:
        time_strings = time_values.dt.strftime("%Y-%m-%dT%H:%M:%S")

    formatted["time"] = time_strings.where(time_values.notna(), formatted["time"])
    return formatted


def _expand_snapshot_time_grid(data: pd.DataFrame, freq: pd.Timedelta) -> pd.DataFrame:
    """仅补齐时间轴，不提前填充值，避免长缺口丢失原始形状特征。"""
    expanded = data.copy()
    expanded["time"] = pd.to_datetime(expanded["time"], errors="coerce")
    expanded = expanded.dropna(subset=["time"]).sort_values("time")
    if expanded.empty:
        return data

    indexed = expanded.set_index("time")
    full_index = pd.date_range(indexed.index.min(), indexed.index.max(), freq=freq)
    indexed = indexed.reindex(full_index)
    indexed.index.name = "time"
    return indexed.reset_index()


def _safe_datetime_index(values: pd.Series | pd.Index | list[object]) -> pd.DatetimeIndex | None:
    """尽量把时间值标准化为 DatetimeIndex；失败则返回 None。"""
    parsed = pd.to_datetime(values, errors="coerce")
    try:
        datetime_index = pd.DatetimeIndex(parsed)
    except (TypeError, ValueError):
        return None

    if datetime_index.isna().all():
        return None
    return datetime_index


def _safe_time_interpolate(series: pd.Series) -> pd.Series:
    """仅在索引确实是 DatetimeIndex 时使用 time interpolation。"""
    if isinstance(series.index, pd.DatetimeIndex):
        return series.interpolate(method="time", limit_direction="both")
    return series.interpolate(limit_direction="both")


def _detect_long_form_identifier(data: pd.DataFrame, value_cols: list[str], keep_cols: list[str]) -> str | None:
    """识别 time/id/value 长表结构。"""
    if "time" not in data.columns or len(value_cols) != 1:
        return None

    identifier_candidates = [
        col
        for col in keep_cols
        if col.lower() in ID_LIKE_COLUMNS or col.lower().endswith("_id")
    ]
    if len(identifier_candidates) != 1:
        return None
    if not data["time"].duplicated().any():
        return None
    return identifier_candidates[0]


def _clean_long_form_pressure(
    data: pd.DataFrame,
    value_col: str,
    identifier_col: str,
    keep_cols: list[str],
    fill_gaps: bool,
) -> pd.DataFrame:
    """按测点拆分 long-form 压力数据，再逐列清洗后恢复原结构。"""
    data = _normalize_time_frame(data)
    wide_df = (
        data[[identifier_col, "time", value_col]]
        .pivot(index="time", columns=identifier_col, values=value_col)
        .reset_index()
    )

    sensor_cols = [col for col in wide_df.columns if col != "time"]
    cleaned_wide = _clean_snapshot_pressure(wide_df, sensor_cols, keep_cols=[], fill_gaps=fill_gaps)

    cleaned_long = cleaned_wide.melt(
        id_vars="time",
        var_name=identifier_col,
        value_name=value_col,
    )

    passthrough_cols = [col for col in keep_cols if col != identifier_col]
    if passthrough_cols:
        metadata = data[[identifier_col] + passthrough_cols].drop_duplicates(subset=[identifier_col])
        cleaned_long = cleaned_long.merge(metadata, on=identifier_col, how="left")

    try:
        cleaned_long[identifier_col] = cleaned_long[identifier_col].astype(data[identifier_col].dtype)
    except (TypeError, ValueError):
        pass

    cleaned_long = cleaned_long.sort_values(["time", identifier_col]).reset_index(drop=True)
    ordered_cols = ["time", identifier_col] + passthrough_cols + [value_col]
    cleaned_long = cleaned_long[[col for col in ordered_cols if col in cleaned_long.columns]]
    return cleaned_long


def _build_time_slot_frame(
    data: pd.DataFrame, value_col: str, expected_slots: int
) -> pd.DataFrame:
    """把重复时间点整理成 time x slot 的矩阵。"""
    grouped = data.groupby("time", sort=True)
    times = list(grouped.groups.keys())
    slot_frame = pd.DataFrame(index=pd.Index(times, name="time"), columns=range(expected_slots), dtype=float)

    for time_value, group in grouped:
        values = pd.to_numeric(group[value_col], errors="coerce").tolist()
        for slot_idx, value in enumerate(values[:expected_slots]):
            slot_frame.loc[time_value, slot_idx] = value
    return slot_frame


def _slot_baseline(slot_frame: pd.DataFrame) -> pd.DataFrame:
    """对每个槽位做时间插值和平滑，得到基线轨迹。"""
    baseline = pd.DataFrame(index=slot_frame.index, columns=slot_frame.columns, dtype=float)
    for col in slot_frame.columns:
        series = slot_frame[col].astype(float)
        series = _safe_time_interpolate(series)
        series = series.rolling(window=5, center=True, min_periods=1).median()
        series = _safe_time_interpolate(series).ffill().bfill()
        baseline[col] = series
    return baseline


def _choose_insertion_position(
    observed: list[float], baseline_row: pd.Series, expected_slots: int
) -> int:
    """为少一个观测值的时间组选择最合理的插入位置。"""
    missing_count = expected_slots - len(observed)
    if missing_count <= 0:
        return 0

    best_pos = 0
    best_cost = float("inf")
    for insert_pos in range(expected_slots):
        cost = 0.0
        obs_idx = 0
        for slot_idx in range(expected_slots):
            if slot_idx == insert_pos:
                continue
            obs_value = observed[obs_idx]
            base_value = float(baseline_row.iloc[slot_idx])
            if pd.notna(obs_value) and pd.notna(base_value):
                cost += abs(obs_value - base_value)
            obs_idx += 1
        if cost < best_cost:
            best_cost = cost
            best_pos = insert_pos
    return best_pos


def _clean_repeated_timestamp_pressure(
    data: pd.DataFrame, value_col: str, keep_cols: list[str]
) -> pd.DataFrame:
    """针对同一时间点重复采样的压力数据进行修复。"""
    data = _normalize_time_frame(data)
    grouped_sizes = data.groupby("time").size()
    if grouped_sizes.empty:
        return data

    expected_slots = int(grouped_sizes.mode().iloc[0]) if not grouped_sizes.mode().empty else int(grouped_sizes.max())
    expected_slots = max(expected_slots, int(grouped_sizes.max()))
    slot_frame = _build_time_slot_frame(data, value_col, expected_slots)
    baseline_frame = _slot_baseline(slot_frame)

    residuals = slot_frame - baseline_frame
    slot_scales = {
        col: max(_robust_scale(residuals[col]), 1e-6) for col in residuals.columns
    }

    cleaned_rows: list[dict[str, object]] = []
    grouped = data.groupby("time", sort=True)
    for time_value, group in grouped:
        observed_values = pd.to_numeric(group[value_col], errors="coerce").tolist()
        baseline_row = baseline_frame.loc[time_value]
        insert_pos = _choose_insertion_position(observed_values, baseline_row, expected_slots)

        cleaned_values: list[float] = []
        obs_idx = 0
        for slot_idx in range(expected_slots):
            if slot_idx == insert_pos and len(observed_values) < expected_slots:
                cleaned_values.append(float(baseline_row.iloc[slot_idx]))
                continue

            if obs_idx >= len(observed_values):
                cleaned_values.append(float(baseline_row.iloc[slot_idx]))
                continue

            observed = observed_values[obs_idx]
            baseline = float(baseline_row.iloc[slot_idx])
            cleaned_values.append(
                _shrink_toward_baseline(observed, baseline, slot_scales.get(slot_idx, 1.0))
            )
            obs_idx += 1

        # 其余字段原样保留；常量列（如 id）直接复制第一条记录即可
        template_row = group.iloc[0].to_dict()
        for slot_idx, cleaned_value in enumerate(cleaned_values):
            row = dict(template_row)
            row["time"] = time_value
            row[value_col] = cleaned_value
            cleaned_rows.append(row)

    cleaned_df = pd.DataFrame(cleaned_rows)
    cleaned_df = cleaned_df.sort_values(["time"]).reset_index(drop=True)
    ordered_cols = ["time"] + keep_cols + [value_col]
    ordered_cols = [col for col in ordered_cols if col in cleaned_df.columns]
    remaining_cols = [col for col in cleaned_df.columns if col not in ordered_cols]
    cleaned_df = cleaned_df[ordered_cols + remaining_cols]
    return _format_time_column(cleaned_df)


def _clean_snapshot_pressure(
    data: pd.DataFrame, value_cols: list[str], keep_cols: list[str], fill_gaps: bool
) -> pd.DataFrame:
    """针对单条时间序列或多列快照数据进行稳健修复。"""
    data = _normalize_time_frame(data)
    if fill_gaps and "time" in data.columns:
        freq = _infer_time_frequency(data["time"])
        data = _expand_snapshot_time_grid(data, freq)
        data["time"] = pd.to_datetime(data["time"], errors="coerce")
        data = data.sort_values(["time"]).reset_index(drop=True)

    cleaned_df = data.copy()
    time_index = (
        _safe_datetime_index(cleaned_df["time"])
        if "time" in cleaned_df.columns
        else None
    )
    if time_index is None:
        time_index = pd.RangeIndex(start=0, stop=len(cleaned_df))
    for col in value_cols:
        series = pd.Series(
            pd.to_numeric(cleaned_df[col], errors="coerce").to_numpy(),
            index=time_index,
            dtype=float,
        )
        cleaned_df[col] = _clean_pressure_series(series).to_numpy()

    ordered_cols = ["time"] + keep_cols + value_cols
    ordered_cols = [col for col in ordered_cols if col in cleaned_df.columns]
    remaining_cols = [col for col in cleaned_df.columns if col not in ordered_cols]
    cleaned_df = cleaned_df[ordered_cols + remaining_cols]
    return _format_time_column(cleaned_df)


def clean_pressure_data_km(
    input_csv_path: str, show_plot: bool = False, fill_gaps: bool = True
) -> str:
    """
    读取输入 CSV，基于时间结构进行稳健修复。输出为 <input_basename>_cleaned.xlsx（同目录）。
    原始数据在 sheet 'raw_pressure_data'，处理后数据在 sheet 'cleaned_pressusre_data'。
    返回输出文件的绝对路径。

    Args:
        input_csv_path: CSV 文件路径
        show_plot: 是否显示可视化
        fill_gaps: 是否先补齐时间缺口（默认 True）
    """
    # 读取 CSV
    input_csv_path = os.path.abspath(input_csv_path)
    data = pd.read_csv(input_csv_path, header=0, index_col=None, encoding="utf-8")
    data = _normalize_time_frame(data)
    value_cols, keep_cols = _select_pressure_columns(data)
    has_repeated_time = "time" in data.columns and data["time"].duplicated().any()
    identifier_col = _detect_long_form_identifier(data, value_cols, keep_cols)

    if identifier_col is not None:
        data_repaired = _clean_long_form_pressure(
            data,
            value_cols[0],
            identifier_col,
            keep_cols,
            fill_gaps,
        )
    elif has_repeated_time and len(value_cols) == 1:
        data_repaired = _clean_repeated_timestamp_pressure(data, value_cols[0], keep_cols)
    else:
        data_repaired = _clean_snapshot_pressure(data, value_cols, keep_cols, fill_gaps)

    # 可选可视化（只展示首个数值列）
    plt.rcParams["font.sans-serif"] = ["SimHei"]
    plt.rcParams["axes.unicode_minus"] = False
    if show_plot and value_cols:
        plot_col = value_cols[0]
        if "time" in data_repaired.columns:
            x = pd.to_datetime(data_repaired["time"], errors="coerce")
        else:
            x = np.arange(len(data_repaired))
        plt.figure(figsize=(12, 6))
        plt.plot(x, pd.to_numeric(data_repaired[plot_col], errors="coerce"), label="cleaned")
        plt.xlabel("时间" if "time" in data_repaired.columns else "序号")
        plt.ylabel("压力监测值")
        plt.title(f"{plot_col} 清洗结果")
        plt.legend()
        plt.show()

    # 保存到 Excel：两个 sheet
    input_dir = os.path.dirname(os.path.abspath(input_csv_path))
    input_base = os.path.splitext(os.path.basename(input_csv_path))[0]
    output_filename = f"{input_base}_cleaned.xlsx"
    output_path = os.path.join(input_dir, output_filename)

    # 如果原始数据包含时间列，将其添加回结果
    data_for_save = data.copy()
    data_repaired_for_save = data_repaired.copy()

    if os.path.exists(output_path):
        os.remove(output_path)  # 覆盖同名文件
    with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
        data_for_save.to_excel(writer, sheet_name="raw_pressure_data", index=False)
        data_repaired_for_save.to_excel(
            writer, sheet_name="cleaned_pressusre_data", index=False
        )

    # 返回输出文件的绝对路径
    return os.path.abspath(output_path)


def clean_pressure_data_df_km(data: pd.DataFrame, show_plot: bool = False) -> pd.DataFrame:
    """
    接收一个 DataFrame 数据结构，使用时间感知的稳健修复方法清洗压力数据。
    返回清洗后的 DataFrame。

    Args:
        data: 输入 DataFrame（可包含 time 列）
        show_plot: 是否显示可视化
    """
    # 使用传入的 DataFrame
    data = data.copy()
    data = _normalize_time_frame(data)
    value_cols, keep_cols = _select_pressure_columns(data)
    has_repeated_time = "time" in data.columns and data["time"].duplicated().any()
    identifier_col = _detect_long_form_identifier(data, value_cols, keep_cols)

    if identifier_col is not None:
        data_repaired = _clean_long_form_pressure(
            data,
            value_cols[0],
            identifier_col,
            keep_cols,
            fill_gaps=True,
        )
    elif has_repeated_time and len(value_cols) == 1:
        data_repaired = _clean_repeated_timestamp_pressure(data, value_cols[0], keep_cols)
    else:
        data_repaired = _clean_snapshot_pressure(data, value_cols, keep_cols, fill_gaps=True)

    if show_plot and value_cols:
        plt.rcParams["font.sans-serif"] = ["SimHei"]
        plt.rcParams["axes.unicode_minus"] = False
        plot_col = value_cols[0]
        x = pd.to_datetime(data_repaired["time"], errors="coerce") if "time" in data_repaired.columns else np.arange(len(data_repaired))
        plt.figure(figsize=(12, 6))
        plt.plot(x, pd.to_numeric(data_repaired[plot_col], errors="coerce"), label="cleaned")
        plt.xlabel("时间" if "time" in data_repaired.columns else "序号")
        plt.ylabel("压力监测值")
        plt.title(f"{plot_col} 清洗结果")
        plt.legend()
        plt.show()

    return data_repaired


# 测试
# if __name__ == "__main__":
#     # 默认使用脚本目录下的 pressure_raw_data.csv
#     script_dir = os.path.dirname(os.path.abspath(__file__))
#     default_csv = os.path.join(script_dir, "pressure_raw_data.csv")
#     out_path = clean_pressure_data_km(default_csv, show_plot=False)
#     print("保存路径：", out_path)

# 测试 clean_pressure_data_dict_km 函数
if __name__ == "__main__":
    import random

    # 读取 szh_pressure_scada.csv 文件
    script_dir = os.path.dirname(os.path.abspath(__file__))
    csv_path = os.path.join(script_dir, "szh_pressure_scada.csv")
    data = pd.read_csv(csv_path, header=0, index_col=None, encoding="utf-8")

    # 排除 Time 列，随机选择 5 列
    columns_to_exclude = ["Time"]
    available_columns = [col for col in data.columns if col not in columns_to_exclude]
    selected_columns = random.sample(available_columns, 5)

    # 将选中的列转换为字典
    data_dict = {col: data[col].tolist() for col in selected_columns}

    print("选中的列:", selected_columns)
    print("原始数据长度:", len(data_dict[selected_columns[0]]))

    # 调用函数进行清洗
    cleaned_dict = clean_pressure_data_df_km(data_dict, show_plot=True)

    print("清洗后的字典键:", list(cleaned_dict.keys()))
    print("清洗后的数据长度:", len(cleaned_dict[selected_columns[0]]))
    print("测试完成：函数运行正常")
