from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional, Tuple


class Metric(str, Enum):
    # String metrics
    JARO_WINKLER = "jaro_winkler"
    LEVENSHTEIN = "levenshtein"
    DAMERAU_LEVENSHTEIN = "damerau_levenshtein"
    EXACT = "exact"
    SOUNDEX = "soundex"
    JACCARD = "jaccard"
    COSINE = "cosine"
    TOKEN_SORT_RATIO = "token_sort_ratio"
    # Numeric metrics
    NUMERIC = "numeric"
    NUMERIC_SIMILARITY = "numeric_similarity"
    DATE_DISTANCE = "date_distance"


# Metrics that operate on numeric (float) data rather than strings
NUMERIC_METRICS = {Metric.NUMERIC, Metric.NUMERIC_SIMILARITY, Metric.DATE_DISTANCE}

# Metrics that operate on string data
STRING_METRICS = {
    Metric.JARO_WINKLER, Metric.LEVENSHTEIN, Metric.DAMERAU_LEVENSHTEIN,
    Metric.EXACT, Metric.SOUNDEX, Metric.JACCARD, Metric.COSINE,
    Metric.TOKEN_SORT_RATIO,
}


@dataclass
class FieldComparison:
    """Defines how to compare a pair of fields between left and right datasets."""

    left_field: str
    right_field: str
    metric: Metric
    output_name: Optional[str] = None
    date_format: Optional[str] = None  # for DATE_DISTANCE, e.g. "%Y-%m-%d"
    preprocess: Optional[List[str]] = None  # e.g. ["lowercase", "strip"]

    def __post_init__(self):
        if self.output_name is None:
            self.output_name = f"{self.left_field}_{self.metric.value}"


@dataclass
class BlockingRule:
    """A SQL expression used to join left (l) and right (r) tables for blocking."""

    sql: str  # e.g. "l.postcode = r.postcode"


@dataclass
class EmbeddingBlockingConfig:
    """Configuration for embedding-based ANN blocking."""

    fields: List[str]  # fields to embed (combined into one string per record)
    top_k: int = 10
    min_sim: float = 0.0
    ngram_range: Tuple[int, int] = (2, 4)
    max_features: int = 50000


@dataclass
class ComparisonLevel:
    """Defines discretization thresholds for a comparison in Fellegi-Sunter."""

    thresholds: List[float] = field(default_factory=lambda: [0.0, 0.7, 1.0])
    # e.g., [0.0, 0.7, 1.0] creates 3 levels:
    #   level 0: score < 0.7 (no match)
    #   level 1: 0.7 <= score < 1.0 (close)
    #   level 2: score == 1.0 (exact)


@dataclass
class LinkConfig:
    """Configuration for the record linkage pipeline."""

    blocking_rules: List[BlockingRule] = field(default_factory=list)
    comparisons: List[FieldComparison] = field(default_factory=list)
    embedding_blocking: Optional[EmbeddingBlockingConfig] = None
    in_memory: bool = True
    chunk_size: int = 2_000_000
    output_dir: Optional[str] = None
    max_candidates_per_record: int = 100
