from __future__ import annotations

from typing import Any, Dict, Iterable, List, Optional

import pandas as pd

from mnemosynecore.data_bases.vertica import (
    vertica_insert_dataframe,
    vertica_insert_dataframe_test,
    vertica_select,
    vertica_select_test,
    vertica_upsert,
    vertica_upsert_test,
)
from mnemosynecore.mattermost import (
    post_message,
    post_message_test,
    send_dataframe_as_csv,
    send_dataframe_as_csv_test,
    send_dataframe_preview,
    send_dataframe_preview_test,
)
from mnemosynecore.sharepoint import sharepoint_read_dataframe, sharepoint_read_dataframe_test


def _format_number(value: float) -> str:
    return f"{value:,.2f}".replace(",", " ")


def _build_root_context(
    *,
    channel_id: str,
    bot_id: str,
    root_id: Optional[str],
    header_text: str,
    threaded: bool,
    test_mode: bool,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict[str, Optional[str]]:
    if not header_text.strip():
        return {
            "header_post_id": None,
            "target_root_id": root_id,
        }

    if test_mode:
        header = post_message_test(
            channel_id=channel_id,
            bot_id=bot_id,
            text=header_text,
            root_id=root_id,
            dir_path=dir_path,
            creds_path=creds_path,
            silent=True,
        )
    else:
        header = post_message(
            channel_id=channel_id,
            bot_id=bot_id,
            text=header_text,
            root_id=root_id,
            silent=True,
        )

    header_post_id = header.get("post_id")
    if root_id:
        return {"header_post_id": header_post_id, "target_root_id": root_id}
    if threaded and header_post_id:
        return {"header_post_id": header_post_id, "target_root_id": header_post_id}
    return {"header_post_id": header_post_id, "target_root_id": None}


def send_query_report(
    *,
    channel_id: str,
    bot_id: str,
    conn_id: str,
    sql: str,
    params: Any = None,
    header_text: str = "",
    root_id: Optional[str] = None,
    threaded: bool = True,
    send_preview: bool = True,
    preview_title: str = "SQL report preview",
    preview_max_rows: int = 10,
    preview_max_cols: int = 10,
    send_csv: bool = True,
    csv_file_name: str = "query_report.csv",
    csv_text: str = "",
) -> Dict[str, Any]:
    df = vertica_select(conn_id=conn_id, sql=sql, params=params)
    root_ctx = _build_root_context(
        channel_id=channel_id,
        bot_id=bot_id,
        root_id=root_id,
        header_text=header_text,
        threaded=threaded,
        test_mode=False,
    )
    target_root_id = root_ctx["target_root_id"]

    if send_preview:
        send_dataframe_preview(
            channel_id=channel_id,
            bot_id=bot_id,
            df=df,
            title=preview_title,
            max_rows=preview_max_rows,
            max_cols=preview_max_cols,
            root_id=target_root_id,
            silent=True,
        )

    csv_result = None
    if send_csv:
        csv_result = send_dataframe_as_csv(
            channel_id=channel_id,
            bot_id=bot_id,
            df=df,
            file_name=csv_file_name,
            text=csv_text,
            root_id=target_root_id,
            silent=True,
        )

    return {
        "rows": int(len(df)),
        "cols": int(len(df.columns)),
        "header_post_id": root_ctx["header_post_id"],
        "target_root_id": target_root_id,
        "csv_result": csv_result,
        "dataframe": df,
    }


def send_query_report_test(
    *,
    channel_id: str,
    bot_id: str,
    conn_id: str,
    sql: str,
    params: Any = None,
    header_text: str = "",
    root_id: Optional[str] = None,
    threaded: bool = True,
    send_preview: bool = True,
    preview_title: str = "SQL report preview",
    preview_max_rows: int = 10,
    preview_max_cols: int = 10,
    send_csv: bool = True,
    csv_file_name: str = "query_report.csv",
    csv_text: str = "",
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict[str, Any]:
    df = vertica_select_test(
        conn_id=conn_id,
        sql=sql,
        params=params,
        dir_path=dir_path,
        creds_path=creds_path,
    )
    root_ctx = _build_root_context(
        channel_id=channel_id,
        bot_id=bot_id,
        root_id=root_id,
        header_text=header_text,
        threaded=threaded,
        test_mode=True,
        dir_path=dir_path,
        creds_path=creds_path,
    )
    target_root_id = root_ctx["target_root_id"]

    if send_preview:
        send_dataframe_preview_test(
            channel_id=channel_id,
            bot_id=bot_id,
            df=df,
            title=preview_title,
            max_rows=preview_max_rows,
            max_cols=preview_max_cols,
            root_id=target_root_id,
            dir_path=dir_path,
            creds_path=creds_path,
            silent=True,
        )

    csv_result = None
    if send_csv:
        csv_result = send_dataframe_as_csv_test(
            channel_id=channel_id,
            bot_id=bot_id,
            df=df,
            file_name=csv_file_name,
            text=csv_text,
            root_id=target_root_id,
            dir_path=dir_path,
            creds_path=creds_path,
            silent=True,
        )

    return {
        "rows": int(len(df)),
        "cols": int(len(df.columns)),
        "header_post_id": root_ctx["header_post_id"],
        "target_root_id": target_root_id,
        "csv_result": csv_result,
        "dataframe": df,
    }


def _prepare_kpi_frame(
    *,
    current_df: pd.DataFrame,
    previous_df: pd.DataFrame,
    metric_col: str,
    value_col: str,
) -> pd.DataFrame:
    if metric_col not in current_df.columns or value_col not in current_df.columns:
        raise ValueError(
            f"current_df должен содержать колонки '{metric_col}' и '{value_col}'"
        )
    if metric_col not in previous_df.columns or value_col not in previous_df.columns:
        raise ValueError(
            f"previous_df должен содержать колонки '{metric_col}' и '{value_col}'"
        )

    cur = current_df[[metric_col, value_col]].copy()
    prev = previous_df[[metric_col, value_col]].copy()
    cur.columns = [metric_col, "current"]
    prev.columns = [metric_col, "previous"]

    merged = cur.merge(prev, on=metric_col, how="outer")
    merged["current"] = pd.to_numeric(merged["current"], errors="coerce").fillna(0.0)
    merged["previous"] = pd.to_numeric(merged["previous"], errors="coerce").fillna(0.0)
    merged["delta_abs"] = merged["current"] - merged["previous"]
    merged["delta_pct"] = (
        (merged["delta_abs"] / merged["previous"].replace(0, pd.NA)) * 100.0
    )
    merged = merged.sort_values(by="delta_abs", key=lambda x: x.abs(), ascending=False)
    merged = merged.reset_index(drop=True)
    return merged


def send_kpi_report(
    *,
    channel_id: str,
    bot_id: str,
    conn_id: str,
    current_sql: str,
    previous_sql: str,
    metric_col: str = "metric",
    value_col: str = "value",
    current_params: Any = None,
    previous_params: Any = None,
    header_text: str = "",
    root_id: Optional[str] = None,
    threaded: bool = True,
    send_preview: bool = True,
    send_csv: bool = True,
    csv_file_name: str = "kpi_report.csv",
) -> Dict[str, Any]:
    current_df = vertica_select(conn_id=conn_id, sql=current_sql, params=current_params)
    previous_df = vertica_select(
        conn_id=conn_id,
        sql=previous_sql,
        params=previous_params,
    )
    kpi_df = _prepare_kpi_frame(
        current_df=current_df,
        previous_df=previous_df,
        metric_col=metric_col,
        value_col=value_col,
    )

    auto_header = (
        f"KPI report: metrics={len(kpi_df)}, "
        f"current_total={_format_number(float(kpi_df['current'].sum()))}, "
        f"previous_total={_format_number(float(kpi_df['previous'].sum()))}, "
        f"delta={_format_number(float(kpi_df['delta_abs'].sum()))}"
    )
    root_ctx = _build_root_context(
        channel_id=channel_id,
        bot_id=bot_id,
        root_id=root_id,
        header_text=header_text or auto_header,
        threaded=threaded,
        test_mode=False,
    )
    target_root_id = root_ctx["target_root_id"]

    if send_preview:
        send_dataframe_preview(
            channel_id=channel_id,
            bot_id=bot_id,
            df=kpi_df,
            title="KPI delta preview",
            max_rows=10,
            max_cols=6,
            root_id=target_root_id,
            silent=True,
        )

    csv_result = None
    if send_csv:
        csv_result = send_dataframe_as_csv(
            channel_id=channel_id,
            bot_id=bot_id,
            df=kpi_df,
            file_name=csv_file_name,
            text="KPI full report",
            root_id=target_root_id,
            silent=True,
        )

    return {
        "header_post_id": root_ctx["header_post_id"],
        "target_root_id": target_root_id,
        "csv_result": csv_result,
        "dataframe": kpi_df,
    }


def send_kpi_report_test(
    *,
    channel_id: str,
    bot_id: str,
    conn_id: str,
    current_sql: str,
    previous_sql: str,
    metric_col: str = "metric",
    value_col: str = "value",
    current_params: Any = None,
    previous_params: Any = None,
    header_text: str = "",
    root_id: Optional[str] = None,
    threaded: bool = True,
    send_preview: bool = True,
    send_csv: bool = True,
    csv_file_name: str = "kpi_report.csv",
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict[str, Any]:
    current_df = vertica_select_test(
        conn_id=conn_id,
        sql=current_sql,
        params=current_params,
        dir_path=dir_path,
        creds_path=creds_path,
    )
    previous_df = vertica_select_test(
        conn_id=conn_id,
        sql=previous_sql,
        params=previous_params,
        dir_path=dir_path,
        creds_path=creds_path,
    )
    kpi_df = _prepare_kpi_frame(
        current_df=current_df,
        previous_df=previous_df,
        metric_col=metric_col,
        value_col=value_col,
    )

    auto_header = (
        f"KPI report: metrics={len(kpi_df)}, "
        f"current_total={_format_number(float(kpi_df['current'].sum()))}, "
        f"previous_total={_format_number(float(kpi_df['previous'].sum()))}, "
        f"delta={_format_number(float(kpi_df['delta_abs'].sum()))}"
    )
    root_ctx = _build_root_context(
        channel_id=channel_id,
        bot_id=bot_id,
        root_id=root_id,
        header_text=header_text or auto_header,
        threaded=threaded,
        test_mode=True,
        dir_path=dir_path,
        creds_path=creds_path,
    )
    target_root_id = root_ctx["target_root_id"]

    if send_preview:
        send_dataframe_preview_test(
            channel_id=channel_id,
            bot_id=bot_id,
            df=kpi_df,
            title="KPI delta preview",
            max_rows=10,
            max_cols=6,
            root_id=target_root_id,
            dir_path=dir_path,
            creds_path=creds_path,
            silent=True,
        )

    csv_result = None
    if send_csv:
        csv_result = send_dataframe_as_csv_test(
            channel_id=channel_id,
            bot_id=bot_id,
            df=kpi_df,
            file_name=csv_file_name,
            text="KPI full report",
            root_id=target_root_id,
            dir_path=dir_path,
            creds_path=creds_path,
            silent=True,
        )

    return {
        "header_post_id": root_ctx["header_post_id"],
        "target_root_id": target_root_id,
        "csv_result": csv_result,
        "dataframe": kpi_df,
    }


def data_quality_report(
    *,
    df: pd.DataFrame,
    freshness_col: Optional[str] = None,
    top_n: int = 10,
) -> Dict[str, Any]:
    if not isinstance(df, pd.DataFrame):
        raise TypeError("df должен быть pandas.DataFrame")
    if top_n < 1:
        raise ValueError("top_n должен быть >= 1")

    rows = int(len(df))
    cols = int(len(df.columns))
    duplicate_count = int(df.duplicated().sum()) if rows > 0 else 0
    duplicate_pct = float((duplicate_count / rows) * 100.0) if rows > 0 else 0.0

    null_counts = df.isna().sum()
    nulls_df = (
        pd.DataFrame(
            {
                "column": null_counts.index,
                "null_count": null_counts.values,
                "null_pct": (null_counts.values / rows * 100.0) if rows > 0 else 0.0,
            }
        )
        .sort_values(["null_count", "column"], ascending=[False, True])
        .reset_index(drop=True)
    )

    numeric_df = df.select_dtypes(include=["number"])
    numeric_rows: List[Dict[str, Any]] = []
    outlier_rows: List[Dict[str, Any]] = []
    for column in numeric_df.columns:
        series = pd.to_numeric(numeric_df[column], errors="coerce").dropna()
        if series.empty:
            continue
        q1 = float(series.quantile(0.25))
        q3 = float(series.quantile(0.75))
        iqr = q3 - q1
        lower = q1 - 1.5 * iqr
        upper = q3 + 1.5 * iqr
        outlier_count = int(((series < lower) | (series > upper)).sum()) if iqr > 0 else 0

        numeric_rows.append(
            {
                "column": column,
                "min": float(series.min()),
                "max": float(series.max()),
                "mean": float(series.mean()),
                "median": float(series.median()),
            }
        )
        outlier_rows.append(
            {
                "column": column,
                "outlier_count": outlier_count,
                "outlier_pct": float(outlier_count / len(series) * 100.0),
            }
        )

    numeric_ranges_df = pd.DataFrame(numeric_rows).sort_values(
        ["column"], ascending=[True]
    ) if numeric_rows else pd.DataFrame(columns=["column", "min", "max", "mean", "median"])
    outliers_df = pd.DataFrame(outlier_rows).sort_values(
        ["outlier_count", "column"], ascending=[False, True]
    ) if outlier_rows else pd.DataFrame(columns=["column", "outlier_count", "outlier_pct"])

    freshness: Optional[Dict[str, Any]] = None
    if freshness_col:
        if freshness_col not in df.columns:
            raise ValueError(f"Колонка freshness_col='{freshness_col}' не найдена")
        parsed = pd.to_datetime(df[freshness_col], errors="coerce")
        max_ts = parsed.max()
        min_ts = parsed.min()
        freshness = {
            "column": freshness_col,
            "max": None if pd.isna(max_ts) else str(max_ts),
            "min": None if pd.isna(min_ts) else str(min_ts),
            "null_count": int(parsed.isna().sum()),
        }

    result = {
        "rows": rows,
        "cols": cols,
        "duplicate_count": duplicate_count,
        "duplicate_pct": duplicate_pct,
        "nulls": nulls_df,
        "numeric_ranges": numeric_ranges_df,
        "outliers": outliers_df,
        "freshness": freshness,
        "top_nulls": nulls_df.head(top_n).copy(),
        "top_outliers": outliers_df.head(top_n).copy(),
    }
    return result


def format_data_quality_report(
    report: Dict[str, Any],
    *,
    title: str = "Data Quality Report",
) -> str:
    lines = [
        title,
        f"rows={report['rows']}, cols={report['cols']}",
        f"duplicates={report['duplicate_count']} ({report['duplicate_pct']:.2f}%)",
    ]

    top_nulls = report.get("top_nulls")
    if isinstance(top_nulls, pd.DataFrame) and not top_nulls.empty:
        top_row = top_nulls.iloc[0]
        lines.append(
            f"top_null_column={top_row['column']} ({float(top_row['null_pct']):.2f}% null)"
        )

    freshness = report.get("freshness")
    if isinstance(freshness, dict):
        lines.append(
            f"freshness[{freshness['column']}]: max={freshness['max']}, min={freshness['min']}, "
            f"null_count={freshness['null_count']}"
        )

    top_outliers = report.get("top_outliers")
    if isinstance(top_outliers, pd.DataFrame) and not top_outliers.empty:
        top_row = top_outliers.iloc[0]
        lines.append(
            f"top_outlier_column={top_row['column']} ({float(top_row['outlier_pct']):.2f}% outliers)"
        )

    return "\n".join(lines)


def send_data_quality_report(
    *,
    channel_id: str,
    bot_id: str,
    df: pd.DataFrame,
    freshness_col: Optional[str] = None,
    top_n: int = 10,
    title: str = "Data Quality Report",
    root_id: Optional[str] = None,
    include_details: bool = True,
) -> Dict[str, Any]:
    report = data_quality_report(df=df, freshness_col=freshness_col, top_n=top_n)
    message_text = format_data_quality_report(report, title=title)
    post = post_message(
        channel_id=channel_id,
        bot_id=bot_id,
        text=message_text,
        root_id=root_id,
        silent=True,
    )

    details_root_id = root_id or post.get("post_id")
    if include_details:
        send_dataframe_preview(
            channel_id=channel_id,
            bot_id=bot_id,
            df=report["top_nulls"],
            title="Top null columns",
            max_rows=top_n,
            max_cols=3,
            root_id=details_root_id,
            silent=True,
        )
        send_dataframe_preview(
            channel_id=channel_id,
            bot_id=bot_id,
            df=report["top_outliers"],
            title="Top outlier columns",
            max_rows=top_n,
            max_cols=3,
            root_id=details_root_id,
            silent=True,
        )

    return {"post": post, "report": report}


def send_data_quality_report_test(
    *,
    channel_id: str,
    bot_id: str,
    df: pd.DataFrame,
    freshness_col: Optional[str] = None,
    top_n: int = 10,
    title: str = "Data Quality Report",
    root_id: Optional[str] = None,
    include_details: bool = True,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict[str, Any]:
    report = data_quality_report(df=df, freshness_col=freshness_col, top_n=top_n)
    message_text = format_data_quality_report(report, title=title)
    post = post_message_test(
        channel_id=channel_id,
        bot_id=bot_id,
        text=message_text,
        root_id=root_id,
        dir_path=dir_path,
        creds_path=creds_path,
        silent=True,
    )

    details_root_id = root_id or post.get("post_id")
    if include_details:
        send_dataframe_preview_test(
            channel_id=channel_id,
            bot_id=bot_id,
            df=report["top_nulls"],
            title="Top null columns",
            max_rows=top_n,
            max_cols=3,
            root_id=details_root_id,
            dir_path=dir_path,
            creds_path=creds_path,
            silent=True,
        )
        send_dataframe_preview_test(
            channel_id=channel_id,
            bot_id=bot_id,
            df=report["top_outliers"],
            title="Top outlier columns",
            max_rows=top_n,
            max_cols=3,
            root_id=details_root_id,
            dir_path=dir_path,
            creds_path=creds_path,
            silent=True,
        )

    return {"post": post, "report": report}


def sharepoint_to_vertica(
    *,
    sharepoint_conn: str | Dict[str, Any],
    file_url: str,
    table_name: str,
    vertica_conn_id: Optional[str] = None,
    vertica_conn: Any = None,
    file_format: str = "auto",
    read_kwargs: Optional[Dict[str, Any]] = None,
    mode: str = "upsert",
    unique_keys: Optional[Iterable[str]] = None,
    date_col: Optional[str] = None,
    days_back: Optional[int] = None,
    dir_path: Optional[str] = None,
    timeout: int = 60,
    verify: bool = True,
    headers: Optional[Dict[str, str]] = None,
    auth_type: str = "ntlm",
    allow_redirects: bool = True,
) -> Dict[str, Any]:
    df = sharepoint_read_dataframe(
        sharepoint_conn=sharepoint_conn,
        file_url=file_url,
        file_format=file_format,
        read_kwargs=read_kwargs,
        dir_path=dir_path,
        timeout=timeout,
        verify=verify,
        headers=headers,
        auth_type=auth_type,
        allow_redirects=allow_redirects,
    )

    mode_norm = mode.strip().lower()
    if mode_norm == "upsert":
        keys = list(unique_keys or [])
        if not keys:
            raise ValueError("Для mode='upsert' нужно указать unique_keys")
        vertica_upsert(
            df=df,
            table_name=table_name,
            unique_keys=keys,
            conn=vertica_conn,
            conn_id=vertica_conn_id,
            date_col=date_col,
            days_back=days_back,
        )
    elif mode_norm == "insert":
        vertica_insert_dataframe(
            df=df,
            table_name=table_name,
            conn=vertica_conn,
            conn_id=vertica_conn_id,
        )
    else:
        raise ValueError("mode должен быть 'upsert' или 'insert'")

    return {
        "rows": int(len(df)),
        "cols": int(len(df.columns)),
        "table_name": table_name,
        "mode": mode_norm,
        "dataframe": df,
    }


def sharepoint_to_vertica_test(
    *,
    sharepoint_conn: str | Dict[str, Any],
    file_url: str,
    table_name: str,
    vertica_conn_id: Optional[str] = None,
    vertica_conn: Any = None,
    file_format: str = "auto",
    read_kwargs: Optional[Dict[str, Any]] = None,
    mode: str = "upsert",
    unique_keys: Optional[Iterable[str]] = None,
    date_col: Optional[str] = None,
    days_back: Optional[int] = None,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
    timeout: int = 60,
    verify: bool = True,
    headers: Optional[Dict[str, str]] = None,
    auth_type: str = "ntlm",
    allow_redirects: bool = True,
) -> Dict[str, Any]:
    df = sharepoint_read_dataframe_test(
        sharepoint_conn=sharepoint_conn,
        file_url=file_url,
        file_format=file_format,
        read_kwargs=read_kwargs,
        dir_path=dir_path,
        creds_path=creds_path,
        timeout=timeout,
        verify=verify,
        headers=headers,
        auth_type=auth_type,
        allow_redirects=allow_redirects,
    )

    mode_norm = mode.strip().lower()
    if mode_norm == "upsert":
        keys = list(unique_keys or [])
        if not keys:
            raise ValueError("Для mode='upsert' нужно указать unique_keys")
        vertica_upsert_test(
            df=df,
            table_name=table_name,
            unique_keys=keys,
            conn=vertica_conn,
            conn_id=vertica_conn_id,
            date_col=date_col,
            days_back=days_back,
            dir_path=dir_path,
            creds_path=creds_path,
        )
    elif mode_norm == "insert":
        vertica_insert_dataframe_test(
            df=df,
            table_name=table_name,
            conn=vertica_conn,
            conn_id=vertica_conn_id,
            dir_path=dir_path,
            creds_path=creds_path,
        )
    else:
        raise ValueError("mode должен быть 'upsert' или 'insert'")

    return {
        "rows": int(len(df)),
        "cols": int(len(df.columns)),
        "table_name": table_name,
        "mode": mode_norm,
        "dataframe": df,
    }
