"""Deduplication mode: matching a dataset against itself.

Special handling:
- Self-pairs (i, i) are excluded
- Only upper triangle: (i, j) where i < j (avoids duplicates)
- Connected-component clustering to group duplicate records
"""

from __future__ import annotations

from typing import Optional

import numpy as np
import pandas as pd

from blazematch.config import LinkConfig
from blazematch.linker import Linker


class Deduplicator:
    """End-to-end deduplication pipeline.

    Usage:
        dedup = Deduplicator(df, config)
        dedup.block().compute_features().estimate()
        results = dedup.predict(threshold=0.8)
        clusters = dedup.cluster(threshold=0.8)
    """

    def __init__(
        self,
        df: pd.DataFrame,
        config: LinkConfig,
        in_memory: bool = True,
    ):
        self.df = df.reset_index(drop=True)
        self.config = config

        # Create Linker with df as both left and right
        self._linker = Linker(df, df, config, in_memory=in_memory)

    @property
    def pairs(self) -> Optional[pd.DataFrame]:
        return self._linker.pairs

    @property
    def features(self) -> Optional[np.ndarray]:
        return self._linker.features

    @property
    def feature_names(self):
        return self._linker.feature_names

    def block(self) -> "Deduplicator":
        """Execute blocking with self-join filtering.

        Removes self-pairs and keeps only upper triangle (i < j).
        """
        self._linker.block()

        # Filter: remove self-pairs and keep only upper triangle
        pairs = self._linker._pairs
        mask = pairs["left_idx"] < pairs["right_idx"]
        self._linker._pairs = pairs[mask].reset_index(drop=True)

        return self

    def compute_features(self) -> "Deduplicator":
        """Compute similarity features for candidate pairs."""
        self._linker.compute_features()
        return self

    def fit(self, labels_df: pd.DataFrame, model: str = "logistic", **kwargs) -> "Deduplicator":
        """Train supervised model on labelled pairs."""
        self._linker.fit(labels_df, model=model, **kwargs)
        return self

    def estimate(self, model: str = "fellegi_sunter", **kwargs) -> "Deduplicator":
        """Estimate unsupervised model."""
        self._linker.estimate(model=model, **kwargs)
        return self

    def predict(self, threshold: Optional[float] = None) -> pd.DataFrame:
        """Score all candidate pairs."""
        return self._linker.predict(threshold=threshold)

    def cluster(self, threshold: float = 0.5) -> pd.DataFrame:
        """Cluster records into duplicate groups using connected components.

        Args:
            threshold: Minimum score to consider a pair as a match.

        Returns:
            DataFrame with columns [record_idx, cluster_id] where each
            record is assigned to a cluster. Records in the same cluster
            are likely duplicates.
        """
        from scipy.sparse import csr_matrix
        from scipy.sparse.csgraph import connected_components

        results = self.predict(threshold=threshold)
        n = len(self.df)

        if len(results) == 0:
            # No matches — each record is its own cluster
            return pd.DataFrame({
                "record_idx": range(n),
                "cluster_id": range(n),
            })

        # Build adjacency matrix
        left = results["left_idx"].values
        right = results["right_idx"].values
        data = np.ones(len(results) * 2)
        row = np.concatenate([left, right])
        col = np.concatenate([right, left])

        adj = csr_matrix((data, (row, col)), shape=(n, n))

        n_components, labels = connected_components(adj, directed=False)

        return pd.DataFrame({
            "record_idx": range(n),
            "cluster_id": labels,
        })
