"""
BizFrame — Enhanced DataFrame wrapper with business intelligence methods.
"""

from __future__ import annotations

import hashlib
from typing import Any, Callable, Iterator, Sequence

import numpy as np
import pandas as pd
from pandas import DataFrame, Series


class BizFrame(DataFrame):
    """
    A pandas DataFrame subclass with built-in business analysis helpers.

    All standard pandas operations are preserved. BizFrame adds:
    - .profile()          — fast data profiling
    - .quality()          — data quality report
    - .segment(col)       — quick value-based segmentation
    - .top(n, col)        — top N rows by column
    - .growth(col, freq)  — period-over-period growth rates
    - .anomalies(col)     — IQR-based anomaly flagging
    - .to_biz_dict()      — structured dict export
    """

    @property
    def _constructor(self):
        return BizFrame

    # ── Profiling ─────────────────────────────────────────────────────────────
    def profile(self, max_categories: int = 20) -> dict[str, Any]:
        """
        Return a comprehensive data profile.

        Returns dict with: shape, dtypes, missing values, cardinality,
        numeric summaries, and top categories per column.
        """
        profile: dict[str, Any] = {
            "shape":    {"rows": len(self), "cols": len(self.columns)},
            "memory_mb": round(self.memory_usage(deep=True).sum() / 1e6, 3),
            "columns":  {},
        }
        for col in self.columns:
            s = self[col]
            null_count = int(s.isna().sum())
            col_info: dict[str, Any] = {
                "dtype":       str(s.dtype),
                "nulls":       null_count,
                "null_pct":    round(null_count / max(len(s), 1) * 100, 2),
                "unique":      int(s.nunique()),
                "cardinality": "high" if s.nunique() > max_categories else "low",
            }
            if pd.api.types.is_numeric_dtype(s):
                col_info.update({
                    "min":    float(s.min()),
                    "max":    float(s.max()),
                    "mean":   round(float(s.mean()), 4),
                    "median": float(s.median()),
                    "std":    round(float(s.std()), 4),
                    "p25":    float(s.quantile(0.25)),
                    "p75":    float(s.quantile(0.75)),
                    "skew":   round(float(s.skew()), 4),
                    "kurt":   round(float(s.kurtosis()), 4),
                    "zeros":  int((s == 0).sum()),
                    "negatives": int((s < 0).sum()),
                })
            elif pd.api.types.is_object_dtype(s) or isinstance(s.dtype, pd.CategoricalDtype):
                top = s.value_counts().head(max_categories).to_dict()
                col_info["top_values"] = {str(k): int(v) for k, v in top.items()}
            elif pd.api.types.is_datetime64_any_dtype(s):
                col_info.update({
                    "min": str(s.min()),
                    "max": str(s.max()),
                    "range_days": int((s.max() - s.min()).days) if not s.isna().all() else 0,
                })
            profile["columns"][col] = col_info
        return profile

    def quality(self) -> DataFrame:
        """Return a DataFrame quality summary with actionable flags."""
        rows = []
        total = len(self)
        for col in self.columns:
            s = self[col]
            null_pct = s.isna().sum() / max(total, 1) * 100
            dup_pct  = s.duplicated(keep=False).sum() / max(total, 1) * 100
            flags    = []
            if null_pct > 50:  flags.append("HIGH_NULL")
            if null_pct > 0:   flags.append(f"nulls={null_pct:.1f}%")
            if s.nunique() == 1: flags.append("CONSTANT")
            if pd.api.types.is_numeric_dtype(s):
                q1, q3 = s.quantile(0.25), s.quantile(0.75)
                iqr = q3 - q1
                outliers = ((s < q1 - 3 * iqr) | (s > q3 + 3 * iqr)).sum()
                if outliers > 0: flags.append(f"outliers={outliers}")
            rows.append({
                "column":   col,
                "dtype":    str(s.dtype),
                "null_pct": round(null_pct, 2),
                "unique":   s.nunique(),
                "dup_pct":  round(dup_pct, 2),
                "flags":    ", ".join(flags) if flags else "OK",
            })
        return BizFrame(rows)

    # ── Business helpers ──────────────────────────────────────────────────────
    def segment(self, col: str, bins: int | Sequence = 4, labels: Sequence | None = None) -> BizFrame:
        """Add a segment column based on quantile bins."""
        out = self.copy()
        if labels is None:
            labels = [f"Segment_{i+1}" for i in range(bins if isinstance(bins, int) else len(bins) - 1)]
        out[f"{col}_segment"] = pd.qcut(out[col], q=bins, labels=labels, duplicates="drop")
        return BizFrame(out)

    def top(self, n: int = 10, col: str | None = None, ascending: bool = False) -> BizFrame:
        """Return top N rows sorted by column."""
        sort_col = col or self.select_dtypes(include="number").columns[0]
        return BizFrame(self.nlargest(n, sort_col) if not ascending else self.nsmallest(n, sort_col))

    def growth(self, col: str, periods: int = 1, pct: bool = True) -> BizFrame:
        """Compute period-over-period absolute or percentage change."""
        out = self.copy()
        key = f"{col}_growth_pct" if pct else f"{col}_growth"
        out[key] = out[col].pct_change(periods) * 100 if pct else out[col].diff(periods)
        return BizFrame(out)

    def anomalies(self, col: str, multiplier: float = 3.0) -> BizFrame:
        """Flag IQR-based anomalies in a numeric column."""
        s  = self[col].dropna()
        q1, q3 = s.quantile(0.25), s.quantile(0.75)
        iqr = q3 - q1
        lo, hi = q1 - multiplier * iqr, q3 + multiplier * iqr
        out = self.copy()
        out[f"{col}_anomaly"] = ~self[col].between(lo, hi)
        out[f"{col}_anomaly_type"] = "normal"
        out.loc[self[col] < lo, f"{col}_anomaly_type"] = "low_outlier"
        out.loc[self[col] > hi, f"{col}_anomaly_type"] = "high_outlier"
        return BizFrame(out)

    def rolling_kpi(self, col: str, window: int = 7, funcs: list[str] | None = None) -> BizFrame:
        """Add rolling window aggregations for a column."""
        funcs = funcs or ["mean", "sum", "std", "min", "max"]
        out = self.copy()
        r = out[col].rolling(window)
        for f in funcs:
            out[f"{col}_roll{window}_{f}"] = getattr(r, f)()
        return BizFrame(out)

    def to_biz_dict(self) -> dict[str, Any]:
        """Export as a structured dict with metadata."""
        return {
            "meta": {
                "rows": len(self),
                "cols": len(self.columns),
                "columns": list(self.columns),
                "checksum": hashlib.md5(pd.util.hash_pandas_object(self, index=True).values.tobytes()).hexdigest(),
            },
            "data": self.to_dict(orient="records"),
        }

    def chunks(self, size: int = 10_000) -> Iterator[BizFrame]:
        """Iterate over chunks of the DataFrame."""
        for start in range(0, len(self), size):
            yield BizFrame(self.iloc[start : start + size])

    def apply_pipeline(self, steps: list[Callable]) -> BizFrame:
        """Apply a list of transform functions sequentially."""
        result = self
        for step in steps:
            result = step(result)
        return BizFrame(result)
