from __future__ import annotations

import pandas as pd

DATE_PARSE_HINTS = ("date", "time", "posted", "report", "service", "admit", "discharge")
_STORAI_DATE_COL = "_storai_source_date"
_SKIP_DRIVER_COLS = {"year", "month", "day"}
METRIC_ALIASES = {
    "charges": ("charges", "charge", "billed_amount", "billing_amount", "billed"),
    "payment": ("payment", "payment_amount", "collections", "paid_amount"),
    "visitcount": ("visit_count", "visits", "visit"),
    "encountercount": ("encounter_count", "encounters", "encounter"),
    "payerwithheld": ("payer_withheld", "withheld"),
    "writeoffadjustment": ("writeoff_adjustment", "writeoff", "write_off"),
    "contractualadjustment": ("contractual_adjustment", "contractual"),
    "insurancear": ("insurance_ar", "insurancear"),
    "patientar": ("patient_ar", "patientar"),
    "cptcount": ("cpt_count", "cpt"),
}


def analyze_root_causes(insights, source_df):
    """
    Build dynamic, data-specific reasons for each detected insight.
    """
    if source_df is None or source_df.empty:
        return insights

    prepared = _prepare_source_dates(source_df)
    if prepared is None:
        return insights

    monthly = _monthly_numeric(prepared)
    if monthly.empty:
        return insights

    for insight in insights:
        reason = _build_reason(prepared, monthly, insight)
        if reason:
            insight.root_causes.append(reason)

    return insights


def _build_reason(source_df: pd.DataFrame, monthly: pd.DataFrame, insight) -> str:
    if insight.type == "growth":
        return _build_growth_reason(source_df, monthly, insight)
    if insight.type == "dominance":
        return _build_dominance_reason(insight)
    if insight.type == "trend":
        return _build_trend_reason(monthly, insight)
    if insight.type == "rate":
        return _build_rate_reason(insight)
    if insight.type == "rate_change":
        return _build_rate_change_reason(insight)
    return ""


def _build_growth_reason(
    source_df: pd.DataFrame, monthly: pd.DataFrame, insight
) -> str:
    current_period = _resolve_current_period(insight, monthly.index)
    if current_period is None:
        return ""

    previous_period = _resolve_previous_period(insight, current_period, monthly.index)
    if previous_period not in monthly.index:
        return ""

    target_col = _resolve_metric_column(monthly, insight.name)
    if target_col is None:
        target_col = _pick_fallback_driver(monthly)
        if target_col is None:
            return ""

    curr_val = float(monthly.loc[current_period, target_col])
    prev_val = float(monthly.loc[previous_period, target_col])
    if prev_val == 0:
        return ""

    target_change_pct = ((curr_val - prev_val) / prev_val) * 100.0
    month_name = current_period.strftime("%B")
    prev_month_name = previous_period.strftime("%B")
    target_label = _humanize(target_col)

    direction_word = "above" if target_change_pct >= 0 else "below"
    base = (
        f"{target_label} in {month_name} was {abs(target_change_pct):.1f}% "
        f"{direction_word} {prev_month_name} ({prev_val:.0f})"
    )

    relationship = _best_related_driver(
        monthly, target_col, current_period, previous_period
    )
    daily = _daily_context(source_df, target_col, current_period, insight.score)

    parts = [base]
    if relationship:
        parts.append(relationship)
    if daily:
        parts.append(daily)

    return "; ".join(parts)


def _build_dominance_reason(insight) -> str:
    data = insight.message_data or {}
    entity = data.get("entity", "top contributor")
    dimension = _humanize(data.get("dimension", "segment"))
    share_pct = abs(float(insight.score)) * 100.0
    groups = data.get("num_groups")
    group_note = (
        f" across {groups} groups" if isinstance(groups, int) and groups > 1 else ""
    )
    return f"{entity} controls {share_pct:.1f}% of {dimension}{group_note}, indicating clear concentration"


def _build_trend_reason(monthly: pd.DataFrame, insight) -> str:
    target_col = _resolve_metric_column(monthly, insight.name)
    if target_col is None:
        direction = "upward" if insight.score > 0 else "downward"
        return f"sustained {direction} movement detected across consecutive periods for this metric"

    series = monthly[target_col].dropna()
    if len(series) < 2:
        direction = "upward" if insight.score > 0 else "downward"
        return f"sustained {direction} movement detected across consecutive periods for this metric"

    changes = series.pct_change().dropna()
    if changes.empty:
        direction = "upward" if insight.score > 0 else "downward"
        return f"sustained {direction} movement detected across consecutive periods for this metric"

    recent = changes.tail(min(3, len(changes)))
    direction = "upward" if insight.score > 0 else "downward"
    consistent = (
        int((recent > 0).sum()) if insight.score > 0 else int((recent < 0).sum())
    )

    start_period = series.index[0]
    end_period = series.index[-1]
    start_val = float(series.iloc[0])
    end_val = float(series.iloc[-1])
    total_change = (
        ((end_val - start_val) / start_val) * 100.0 if start_val != 0 else 0.0
    )

    label = _humanize(target_col)
    return (
        f"{label} moved {direction} in {consistent} of the last {len(recent)} periods; "
        f"from {start_val:.0f} in {start_period.strftime('%B')} to {end_val:.0f} in {end_period.strftime('%B')} "
        f"({abs(total_change):.1f}% net change)"
    )


def _build_rate_reason(insight) -> str:
    data = insight.message_data or {}
    period = _format_period(data.get("period"))
    category = data.get("rate_category", "material")
    return f"current rate level in {period} is {category.replace('-', ' ')} versus baseline behavior"


def _build_rate_change_reason(insight) -> str:
    data = insight.message_data or {}
    prev = data.get("previous")
    curr = data.get("current")
    if prev is not None and curr is not None:
        delta = (curr - prev) * 100.0
        direction = "improvement" if delta > 0 else "deterioration"
        return f"{direction} of {abs(delta):.1f} points versus previous period ({prev:.2f} -> {curr:.2f})"
    return "rate moved meaningfully from prior period, indicating a structural change"


def _best_related_driver(
    monthly: pd.DataFrame,
    target_col: str,
    current_period,
    previous_period,
) -> str:
    candidates = [col for col in monthly.columns if col != target_col]
    if not candidates:
        return ""

    target_changes = monthly[target_col].pct_change().replace([pd.NA, pd.NaT], pd.NA)
    best_col = None
    best_corr = 0.0

    for col in candidates:
        candidate_changes = monthly[col].pct_change().replace([pd.NA, pd.NaT], pd.NA)
        joined = pd.concat([target_changes, candidate_changes], axis=1).dropna()
        if len(joined) < 2:
            continue
        corr = joined.iloc[:, 0].corr(joined.iloc[:, 1])
        if pd.isna(corr):
            continue
        if abs(corr) > abs(best_corr):
            best_corr = float(corr)
            best_col = col

    if best_col is None:
        return ""

    prev_val = float(monthly.loc[previous_period, best_col])
    curr_val = float(monthly.loc[current_period, best_col])
    if prev_val == 0:
        return ""

    change_pct = ((curr_val - prev_val) / prev_val) * 100.0
    if abs(change_pct) < 1.0:
        return ""

    relation = (
        "moved with this metric" if best_corr >= 0 else "moved opposite to this metric"
    )
    return (
        f"{_humanize(best_col)} changed {abs(change_pct):.1f}% and {relation}, "
        f"making it the strongest co-movement signal"
    )


def _daily_context(
    source_df: pd.DataFrame, target_col: str, period, insight_score: float
) -> str:
    if target_col not in source_df.columns:
        return ""

    mask = source_df[_STORAI_DATE_COL].dt.to_period("M") == period
    month_df = source_df.loc[mask]
    if month_df.empty:
        return ""

    daily = (
        month_df.groupby(month_df[_STORAI_DATE_COL].dt.day)[target_col]
        .sum()
        .sort_index()
    )
    if len(daily) < 7:
        return ""

    avg = float(daily.mean())
    if avg <= 0:
        return ""

    month_name = period.strftime("%B")
    if insight_score >= 0:
        strong = daily[daily > (avg * 1.15)].nlargest(2)
        if strong.empty:
            return ""
        return f"strongest days were {_format_days(sorted(strong.index.tolist()), month_name)}"

    weak = daily[daily < (avg * 0.85)].nsmallest(2)
    if weak.empty:
        return ""
    return f"weakest days were {_format_days(sorted(weak.index.tolist()), month_name)}"


def _prepare_source_dates(source_df: pd.DataFrame) -> pd.DataFrame | None:
    work_df = source_df.copy()
    date_series = _extract_date_series(work_df)
    if date_series is None:
        return None

    work_df[_STORAI_DATE_COL] = pd.to_datetime(date_series, errors="coerce")
    work_df = work_df.dropna(subset=[_STORAI_DATE_COL])
    if work_df.empty:
        return None
    return work_df


def _extract_date_series(df: pd.DataFrame) -> pd.Series | None:
    for col in df.columns:
        series = df[col]
        if pd.api.types.is_datetime64_any_dtype(series):
            return pd.to_datetime(series, errors="coerce")

    for col in df.columns:
        col_lower = col.lower()
        if col_lower in {"year", "month"}:
            continue
        if not any(token in col_lower for token in DATE_PARSE_HINTS):
            continue
        parsed = pd.to_datetime(df[col], errors="coerce")
        if _enough_dates(parsed, len(df)):
            return parsed

    year_col = _find_col(df, ("year",))
    month_col = _find_col(df, ("month",))
    day_col = _find_col(df, ("day",))
    if year_col and month_col:
        day = df[day_col] if day_col else 1
        parsed = pd.to_datetime(
            {"year": df[year_col], "month": df[month_col], "day": day},
            errors="coerce",
        )
        if _enough_dates(parsed, len(df)):
            return parsed

    return None


def _monthly_numeric(source_df: pd.DataFrame) -> pd.DataFrame:
    numeric_cols = [
        col
        for col in source_df.select_dtypes(include="number").columns
        if col.lower() not in _SKIP_DRIVER_COLS
    ]
    if not numeric_cols:
        return pd.DataFrame()

    grouped = (
        source_df.assign(_period=source_df[_STORAI_DATE_COL].dt.to_period("M"))
        .groupby("_period")[numeric_cols]
        .sum()
        .sort_index()
    )
    return grouped


def _resolve_metric_column(monthly: pd.DataFrame, insight_name: str) -> str | None:
    target = (insight_name or "").strip().lower()
    if not target:
        return None

    normalized = {_normalize_metric_key(col): col for col in monthly.columns}
    norm_target = _normalize_metric_key(target)
    if norm_target in normalized:
        return normalized[norm_target]

    for alias in _metric_alias_candidates(norm_target):
        if alias in normalized:
            return normalized[alias]

    candidates: list[tuple[int, str]] = []
    aliases = _metric_alias_candidates(norm_target)
    for col in monthly.columns:
        norm_col = _normalize_metric_key(col)
        for alias in aliases:
            if alias and (alias in norm_col or norm_col in alias):
                candidates.append((len(alias), col))
                break
    if candidates:
        candidates.sort(key=lambda item: item[0], reverse=True)
        return candidates[0][1]

    return None


def _pick_fallback_driver(monthly: pd.DataFrame) -> str | None:
    if monthly.empty:
        return None
    preferred_tokens = ("visit", "encounter", "payment", "charge", "ar", "count")
    for token in preferred_tokens:
        for col in monthly.columns:
            if token in col.lower():
                return col
    return monthly.columns[0]


def _metric_alias_candidates(norm_target: str) -> tuple[str, ...]:
    aliases = list(METRIC_ALIASES.get(norm_target, (norm_target,)))
    if norm_target not in aliases:
        aliases.append(norm_target)
    normalized_aliases = [_normalize_metric_key(alias) for alias in aliases if alias]
    return tuple(dict.fromkeys(normalized_aliases))


def _normalize_metric_key(value: str) -> str:
    return (value or "").strip().lower().replace("_", "").replace(" ", "")


def _resolve_current_period(insight, available_periods):
    raw_period = (insight.message_data or {}).get("period")
    parsed = pd.to_datetime(raw_period, errors="coerce")
    if not pd.isna(parsed):
        period = parsed.to_period("M")
        if period in available_periods:
            return period

    month_hint = _extract_month_hint(raw_period)
    if month_hint is None:
        return None
    candidates = [p for p in available_periods if p.month == month_hint]
    if not candidates:
        return None
    return sorted(candidates)[-1]


def _resolve_previous_period(insight, current_period, available_periods):
    raw_prev = (insight.message_data or {}).get("previous_period")
    parsed = pd.to_datetime(raw_prev, errors="coerce")
    if not pd.isna(parsed):
        period = parsed.to_period("M")
        if period in available_periods:
            return period

    month_hint = _extract_month_hint(raw_prev)
    if month_hint is not None:
        candidates = [p for p in available_periods if p.month == month_hint]
        if candidates:
            return sorted(candidates)[-1]

    return current_period - 1


def _humanize(name: str) -> str:
    if not name:
        return "metric"
    return name.replace("_", " ")


def _format_period(raw_period):
    if raw_period is None or raw_period == "":
        return "the period"
    parsed = pd.to_datetime(raw_period, errors="coerce")
    if not pd.isna(parsed):
        return parsed.strftime("%B %Y")
    return str(raw_period)


def _find_col(df: pd.DataFrame, terms: tuple[str, ...]) -> str | None:
    for col in df.columns:
        col_lower = col.lower()
        if any(term == col_lower or col_lower.endswith(f"_{term}") for term in terms):
            return col
    return None


def _enough_dates(parsed: pd.Series, total_rows: int) -> bool:
    return parsed.notna().sum() >= max(2, int(total_rows * 0.4))


def _extract_month_hint(raw_value):
    if raw_value is None:
        return None
    text = str(raw_value).strip()
    if text.isdigit():
        month = int(text)
        if 1 <= month <= 12:
            return month
    return None


def _format_days(days: list[int], month_name: str) -> str:
    labels = [f"{day}{_ordinal(day)}" for day in days]
    if len(labels) == 1:
        return f"{labels[0]} {month_name}"
    if len(labels) == 2:
        return f"{labels[0]} & {labels[1]} {month_name}"
    return f"{', '.join(labels[:-1])} & {labels[-1]} {month_name}"


def _ordinal(value: int) -> str:
    if 10 <= value % 100 <= 20:
        return "th"
    if value % 10 == 1:
        return "st"
    if value % 10 == 2:
        return "nd"
    if value % 10 == 3:
        return "rd"
    return "th"
