"""
Automated data profiling and quality assessment.
"""

from typing import Dict, List, Optional
from dataclasses import dataclass, field

import pandas as pd
import numpy as np


@dataclass
class ProfileReport:
    """Comprehensive data profile report."""

    dataset_name: str
    row_count: int
    column_count: int
    memory_usage: str
    column_profiles: Dict[str, Dict]
    correlations: Optional[pd.DataFrame] = None
    warnings: List[str] = field(default_factory=list)
    recommendations: List[str] = field(default_factory=list)

    def to_html(self) -> str:
        """Generate HTML report."""
        html = f"""
        <html>
        <head><title>MLGuardian Profile: {self.dataset_name}</title></head>
        <body>
            <h1>Data Profile: {self.dataset_name}</h1>
            <h2>Overview</h2>
            <ul>
                <li>Rows: {self.row_count:,}</li>
                <li>Columns: {self.column_count}</li>
                <li>Memory: {self.memory_usage}</li>
            </ul>
            <h2>Column Details</h2>
            <table border="1">
                <tr>
                    <th>Column</th>
                    <th>Type</th>
                    <th>Nulls</th>
                    <th>Unique</th>
                    <th>Notes</th>
                </tr>
        """

        for col, profile in self.column_profiles.items():
            html += f"""
                <tr>
                    <td>{col}</td>
                    <td>{profile.get('type', 'unknown')}</td>
                    <td>{profile.get('null_pct', 0):.1%}</td>
                    <td>{profile.get('unique', 0):,}</td>
                    <td>{', '.join(profile.get('flags', []))}</td>
                </tr>
            """

        html += "</table></body></html>"
        return html

    def to_dict(self) -> Dict:
        """Convert to dictionary."""
        return {
            "dataset_name": self.dataset_name,
            "row_count": self.row_count,
            "column_count": self.column_count,
            "memory_usage": self.memory_usage,
            "column_profiles": self.column_profiles,
            "warnings": self.warnings,
            "recommendations": self.recommendations,
        }


class DataProfiler:
    """Generate comprehensive data profiles for ML datasets."""

    def __init__(
        self,
        correlation_threshold: float = 0.8,
        high_cardinality_threshold: int = 1000,
        missing_threshold: float = 0.5,
    ):
        self.correlation_threshold = correlation_threshold
        self.high_cardinality_threshold = high_cardinality_threshold
        self.missing_threshold = missing_threshold

    def profile(self, df: pd.DataFrame, name: str = "dataset") -> ProfileReport:
        """
        Generate comprehensive profile.
        """
        column_profiles = {}
        warnings_list = []
        # recommendations = []
        recommendations: List[str] = []

        row_count = len(df)
        column_count = len(df.columns)
        memory_usage = f"{df.memory_usage(deep=True).sum() / 1024**2:.2f} MB"

        for col in df.columns:
            profile = self._profile_column(df[col])
            column_profiles[col] = profile

            if profile["null_pct"] > self.missing_threshold:
                msg = f"Column '{col}' has {profile['null_pct']:.1%} " f"missing values"
                warnings_list.append(msg)

            if profile.get("is_high_cardinality"):
                msg = f"Column '{col}' has high cardinality " f"({profile['unique']} unique)"
                warnings_list.append(msg)

            if profile.get("is_constant"):
                warnings_list.append(f"Column '{col}' is constant")

            if profile.get("is_duplicate"):
                msg = f"Column '{col}' may be duplicate of " f"{profile['duplicate_of']}"
                warnings_list.append(msg)

        correlations = None
        numeric_cols = df.select_dtypes(include=[np.number]).columns
        if len(numeric_cols) > 1:
            corr_matrix = df[numeric_cols].corr()
            high_corr = []
            for i in range(len(corr_matrix.columns)):
                for j in range(i + 1, len(corr_matrix.columns)):
                    if abs(corr_matrix.iloc[i, j]) > self.correlation_threshold:
                        high_corr.append(
                            (
                                corr_matrix.columns[i],
                                corr_matrix.columns[j],
                                corr_matrix.iloc[i, j],
                            )
                        )

            if high_corr:
                msg = f"Found {len(high_corr)} highly correlated feature pairs"
                warnings_list.append(msg)
                for col1, col2, corr in high_corr:
                    rec = f"Consider removing {col1} or {col2} " f"(correlation: {corr:.2f})"
                    recommendations.append(rec)

            correlations = corr_matrix

        missing_cols = [c for c, p in column_profiles.items() if p["null_pct"] > 0]
        if missing_cols:
            recommendations.append(
                f"Consider imputation for columns with missing values: " f"{missing_cols}"
            )  # type: ignore

        return ProfileReport(
            dataset_name=name,
            row_count=row_count,
            column_count=column_count,
            memory_usage=memory_usage,
            column_profiles=column_profiles,
            correlations=correlations,
            warnings=warnings_list,
            recommendations=recommendations,
        )

    def _profile_column(self, series: pd.Series) -> Dict:
        """Profile single column."""
        profile = {
            "type": str(series.dtype),
            "null_count": int(series.isnull().sum()),
            "null_pct": series.isnull().mean(),
            "unique": int(series.nunique(dropna=True)),
        }

        if pd.api.types.is_numeric_dtype(series):
            numeric_series = series.dropna()
            if len(numeric_series) > 0:
                profile.update(
                    {
                        "min": float(numeric_series.min()),
                        "max": float(numeric_series.max()),
                        "mean": float(numeric_series.mean()),
                        "std": float(numeric_series.std()),
                        "skewness": float(numeric_series.skew()),
                        "kurtosis": float(numeric_series.kurtosis()),
                        "zeros": int((numeric_series == 0).sum()),
                        "negatives": int((numeric_series < 0).sum()),
                    }
                )
        else:
            str_series = series.dropna().astype(str)
            if len(str_series) > 0:
                lengths = str_series.str.len()
                profile.update(
                    {
                        "min_length": int(lengths.min()),
                        "max_length": int(lengths.max()),
                        "avg_length": float(lengths.mean()),
                        "empty_strings": int((str_series == "").sum()),
                    }
                )

        flags = []
        if profile["unique"] == 1:
            flags.append("constant")
            profile["is_constant"] = True

        if profile["unique"] > self.high_cardinality_threshold:
            flags.append("high_cardinality")
            profile["is_high_cardinality"] = True

        if profile["null_pct"] == 0:
            flags.append("complete")

        if profile["unique"] == len(series.dropna()):
            flags.append("unique")

        profile["flags"] = flags

        return profile

    def compare_profiles(self, profile1: ProfileReport, profile2: ProfileReport) -> Dict:
        """Compare two dataset profiles and highlight differences."""
        differences = {
            "shape_changed": (
                profile1.row_count != profile2.row_count
                or profile1.column_count != profile2.column_count
            ),
            "row_diff": profile2.row_count - profile1.row_count,
            "column_diff": profile2.column_count - profile1.column_count,
            "new_columns": [],
            "removed_columns": [],
            "changed_columns": [],
        }

        cols1 = set(profile1.column_profiles.keys())
        cols2 = set(profile2.column_profiles.keys())

        differences["new_columns"] = list(cols2 - cols1)
        differences["removed_columns"] = list(cols1 - cols2)

        for col in cols1 & cols2:
            p1 = profile1.column_profiles[col]
            p2 = profile2.column_profiles[col]

            changes = {}
            if p1["type"] != p2["type"]:
                changes["type_changed"] = (p1["type"], p2["type"])
            if abs(p1["null_pct"] - p2["null_pct"]) > 0.1:
                changes["null_pct_change"] = (p1["null_pct"], p2["null_pct"])
            if p1["unique"] != p2["unique"]:
                changes["cardinality_change"] = (p1["unique"], p2["unique"])

            if changes:
                differences["changed_columns"].append({"column": col, "changes": changes})

        return differences


# """
# Automated data profiling and quality assessment.
# """

# from typing import Dict, List, Optional, Any
# from dataclasses import dataclass, field
# import json

# import pandas as pd
# import numpy as np


# @dataclass
# class ProfileReport:
#     """Comprehensive data profile report."""

#     dataset_name: str
#     row_count: int
#     column_count: int
#     memory_usage: str
#     column_profiles: Dict[str, Dict]
#     correlations: Optional[pd.DataFrame] = None
#     warnings: List[str] = field(default_factory=list)
#     recommendations: List[str] = field(default_factory=list)

#     def to_html(self) -> str:
#         """Generate HTML report."""
#         html = f"""
#         <html>
#         <head><title>MLGuardian Profile: {self.dataset_name}</title></head>
#         <body>
#             <h1>Data Profile: {self.dataset_name}</h1>
#             <h2>Overview</h2>
#             <ul>
#                 <li>Rows: {self.row_count:,}</li>
#                 <li>Columns: {self.column_count}</li>
#                 <li>Memory: {self.memory_usage}</li>
#             </ul>
#             <h2>Column Details</h2>
#             <table border="1">
#                 <tr><th>Column</th><th>Type</th><th>Nulls</th><th>Unique</th><th>Notes</th></tr>
#         """

#         for col, profile in self.column_profiles.items():
#             html += f"""
#                 <tr>
#                     <td>{col}</td>
#                     <td>{profile.get('type', 'unknown')}</td>
#                     <td>{profile.get('null_pct', 0):.1%}</td>
#                     <td>{profile.get('unique', 0):,}</td>
#                     <td>{', '.join(profile.get('flags', []))}</td>
#                 </tr>
#             """

#         html += "</table></body></html>"
#         return html

#     def to_dict(self) -> Dict:
#         """Convert to dictionary."""
#         return {
#             "dataset_name": self.dataset_name,
#             "row_count": self.row_count,
#             "column_count": self.column_count,
#             "memory_usage": self.memory_usage,
#             "column_profiles": self.column_profiles,
#             "warnings": self.warnings,
#             "recommendations": self.recommendations,
#         }


# class DataProfiler:
#     """Generate comprehensive data profiles for ML datasets."""

#     def __init__(
#         self,
#         correlation_threshold: float = 0.8,
#         high_cardinality_threshold: int = 1000,
#         missing_threshold: float = 0.5,
#     ):
#         self.correlation_threshold = correlation_threshold
#         self.high_cardinality_threshold = high_cardinality_threshold
#         self.missing_threshold = missing_threshold

#     def profile(self, df: pd.DataFrame, name: str = "dataset") -> ProfileReport:
#         """
#         Generate comprehensive profile.

#         Args:
#             df: DataFrame to profile
#             name: Dataset identifier

#         Returns:
#             ProfileReport with detailed statistics
#         """
#         column_profiles = {}
#         warnings = []
#         recommendations = []

#         # Dataset-level stats
#         row_count = len(df)
#         column_count = len(df.columns)
#         memory_usage = f"{df.memory_usage(deep=True).sum() / 1024**2:.2f} MB"

#         # Profile each column
#         for col in df.columns:
#             profile = self._profile_column(df[col])
#             column_profiles[col] = profile

#             # Generate warnings
#             if profile["null_pct"] > self.missing_threshold:
#                 warnings.append(
#                     f"Column '{col}' has {profile['null_pct']:.1%} missing values"
#                 )

#             if profile.get("is_high_cardinality"):
#                 warnings.append(
#                     f"Column '{col}' has high cardinality ({profile['unique']} unique)"
#                 )

#             if profile.get("is_constant"):
#                 warnings.append(f"Column '{col}' is constant")

#             if profile.get("is_duplicate"):
#                 warnings.append(
#                     f"Column '{col}' may be duplicate of {profile['duplicate_of']}"
#                 )

#         # Check for correlations
#         correlations = None
#         numeric_cols = df.select_dtypes(include=[np.number]).columns
#         if len(numeric_cols) > 1:
#             corr_matrix = df[numeric_cols].corr()
#             high_corr = []
#             for i in range(len(corr_matrix.columns)):
#                 for j in range(i + 1, len(corr_matrix.columns)):
#                     if abs(corr_matrix.iloc[i, j]) > self.correlation_threshold:
#                         high_corr.append(
#                             (
#                                 corr_matrix.columns[i],
#                                 corr_matrix.columns[j],
#                                 corr_matrix.iloc[i, j],
#                             )
#                         )

#             if high_corr:
#                 warnings.append(
#                     f"Found {len(high_corr)} highly correlated feature pairs"
#                 )
#                 for col1, col2, corr in high_corr:
#                     recommendations.append(
#                         f"Consider removing {col1} or {col2} (correlation: {corr:.2f})"
#                     )

#             correlations = corr_matrix

#         # Dataset recommendations
#         missing_cols = [c for c, p in column_profiles.items() if p["null_pct"] > 0]
#         if missing_cols:
#             recommendations.append(
#                 f"Consider imputation for columns with missing values: {missing_cols}"
#             )

#         return ProfileReport(
#             dataset_name=name,
#             row_count=row_count,
#             column_count=column_count,
#             memory_usage=memory_usage,
#             column_profiles=column_profiles,
#             correlations=correlations,
#             warnings=warnings,
#             recommendations=recommendations,
#         )

#     def _profile_column(self, series: pd.Series) -> Dict:
#         """Profile single column."""
#         profile = {
#             "type": str(series.dtype),
#             "null_count": int(series.isnull().sum()),
#             "null_pct": series.isnull().mean(),
#             "unique": int(series.nunique(dropna=True)),
#         }

#         # Type-specific stats
#         if pd.api.types.is_numeric_dtype(series):
#             numeric_series = series.dropna()
#             if len(numeric_series) > 0:
#                 profile.update(
#                     {
#                         "min": float(numeric_series.min()),
#                         "max": float(numeric_series.max()),
#                         "mean": float(numeric_series.mean()),
#                         "std": float(numeric_series.std()),
#                         "skewness": float(numeric_series.skew()),
#                         "kurtosis": float(numeric_series.kurtosis()),
#                         "zeros": int((numeric_series == 0).sum()),
#                         "negatives": int((numeric_series < 0).sum()),
#                     }
#                 )
#         else:
#             # String stats
#             str_series = series.dropna().astype(str)
#             if len(str_series) > 0:
#                 lengths = str_series.str.len()
#                 profile.update(
#                     {
#                         "min_length": int(lengths.min()),
#                         "max_length": int(lengths.max()),
#                         "avg_length": float(lengths.mean()),
#                         "empty_strings": int((str_series == "").sum()),
#                     }
#                 )

#         # Flags
#         flags = []
#         if profile["unique"] == 1:
#             flags.append("constant")
#             profile["is_constant"] = True

#         if profile["unique"] > self.high_cardinality_threshold:
#             flags.append("high_cardinality")
#             profile["is_high_cardinality"] = True

#         if profile["null_pct"] == 0:
#             flags.append("complete")

#         if profile["unique"] == len(series.dropna()):
#             flags.append("unique")

#         profile["flags"] = flags

#         return profile

#     def compare_profiles(
#         self, profile1: ProfileReport, profile2: ProfileReport
#     ) -> Dict:
#         """Compare two dataset profiles and highlight differences."""
#         differences = {
#             "shape_changed": profile1.row_count != profile2.row_count
#             or profile1.column_count != profile2.column_count,
#             "row_diff": profile2.row_count - profile1.row_count,
#             "column_diff": profile2.column_count - profile1.column_count,
#             "new_columns": [],
#             "removed_columns": [],
#             "changed_columns": [],
#         }

#         cols1 = set(profile1.column_profiles.keys())
#         cols2 = set(profile2.column_profiles.keys())

#         differences["new_columns"] = list(cols2 - cols1)
#         differences["removed_columns"] = list(cols1 - cols2)

#         for col in cols1 & cols2:
#             p1 = profile1.column_profiles[col]
#             p2 = profile2.column_profiles[col]

#             changes = {}
#             if p1["type"] != p2["type"]:
#                 changes["type_changed"] = (p1["type"], p2["type"])
#             if abs(p1["null_pct"] - p2["null_pct"]) > 0.1:
#                 changes["null_pct_change"] = (p1["null_pct"], p2["null_pct"])
#             if p1["unique"] != p2["unique"]:
#                 changes["cardinality_change"] = (p1["unique"], p2["unique"])

#             if changes:
#                 differences["changed_columns"].append(
#                     {"column": col, "changes": changes}
#                 )

#         return differences
