"""Statistical profiling for datasets.

Computes per-column statistics: null counts, numeric stats (min, max,
mean, std, median, percentiles), and string stats (lengths, top values).
All stdlib: uses statistics and collections modules.
"""

from __future__ import annotations

import statistics
from collections import Counter
from dataclasses import dataclass, field


@dataclass
class ColumnProfile:
    """Profile of a single column."""

    name: str
    dtype: str  # inferred
    null_count: int = 0
    null_pct: float = 0.0
    unique_count: int = 0
    # Numeric stats
    min_val: float | None = None
    max_val: float | None = None
    mean: float | None = None
    std: float | None = None
    median: float | None = None
    p25: float | None = None
    p75: float | None = None
    # String stats
    min_length: int | None = None
    max_length: int | None = None
    top_values: list[tuple[str, int]] = field(default_factory=list)


@dataclass
class DataProfile:
    """Profile of an entire dataset."""

    row_count: int
    column_count: int
    columns: dict[str, ColumnProfile] = field(default_factory=dict)


def _infer_dtype(values: list) -> str:
    """Infer the dominant dtype of a list of values."""
    type_counts: dict[str, int] = {}
    for v in values:
        if v is None:
            continue
        if isinstance(v, bool):
            type_counts["bool"] = type_counts.get("bool", 0) + 1
        elif isinstance(v, int):
            type_counts["int"] = type_counts.get("int", 0) + 1
        elif isinstance(v, float):
            type_counts["float"] = type_counts.get("float", 0) + 1
        elif isinstance(v, str):
            type_counts["str"] = type_counts.get("str", 0) + 1
        else:
            type_counts["other"] = type_counts.get("other", 0) + 1

    if not type_counts:
        return "unknown"

    # Float supersedes int (mixed int/float → float)
    if "float" in type_counts and "int" in type_counts:
        type_counts["float"] += type_counts.pop("int")

    return max(type_counts, key=type_counts.get)  # type: ignore[arg-type]


class DataProfiler:
    """Profile datasets for statistical summaries."""

    def profile(self, data: list[dict]) -> DataProfile:
        """Compute statistical profile of the dataset."""
        if not data:
            return DataProfile(row_count=0, column_count=0)

        # Collect all column names
        all_columns: list[str] = []
        seen: set[str] = set()
        for row in data:
            for key in row:
                if key not in seen:
                    all_columns.append(key)
                    seen.add(key)

        n = len(data)
        profiles: dict[str, ColumnProfile] = {}

        for col in all_columns:
            values = [row.get(col) for row in data]
            non_null = [v for v in values if v is not None]
            null_count = n - len(non_null)

            dtype = _infer_dtype(non_null)
            unique_count = len(set(str(v) for v in non_null))

            cp = ColumnProfile(
                name=col,
                dtype=dtype,
                null_count=null_count,
                null_pct=null_count / n if n > 0 else 0.0,
                unique_count=unique_count,
            )

            if dtype in ("float", "int") and non_null:
                nums = [float(v) for v in non_null]
                nums_sorted = sorted(nums)
                cp.min_val = nums_sorted[0]
                cp.max_val = nums_sorted[-1]
                cp.mean = statistics.mean(nums)
                cp.std = statistics.pstdev(nums) if len(nums) > 1 else 0.0
                cp.median = statistics.median(nums)
                # Percentiles
                q1_idx = max(0, len(nums_sorted) // 4 - 1)
                q3_idx = min(len(nums_sorted) - 1, 3 * len(nums_sorted) // 4)
                cp.p25 = nums_sorted[q1_idx]
                cp.p75 = nums_sorted[q3_idx]

            elif dtype == "str" and non_null:
                lengths = [len(str(v)) for v in non_null]
                cp.min_length = min(lengths)
                cp.max_length = max(lengths)
                counter = Counter(str(v) for v in non_null)
                cp.top_values = counter.most_common(5)

            profiles[col] = cp

        return DataProfile(
            row_count=n,
            column_count=len(all_columns),
            columns=profiles,
        )
