"""Type stubs for the reclink._core Rust extension module."""

from collections.abc import Sequence
from typing import Literal

import numpy as np
from numpy.typing import NDArray

type BuiltinScorer = Literal[
    "levenshtein",
    "damerau_levenshtein",
    "hamming",
    "jaro",
    "jaro_winkler",
    "cosine",
    "jaccard",
    "sorensen_dice",
    "weighted_levenshtein",
    "token_sort",
    "token_set",
    "partial_ratio",
    "lcs",
    "longest_common_substring",
    "ngram_similarity",
    "smith_waterman",
    "phonetic_hybrid",
    "ratcliff_obershelp",
    "needleman_wunsch",
    "gotoh",
    "monge_elkan",
]

# Accepts built-in metric names and custom metrics registered via register_metric()
type Scorer = str

type PhoneticAlgorithm = Literal[
    "soundex",
    "metaphone",
    "double_metaphone",
    "nysiis",
    "caverphone",
    "cologne_phonetic",
    "beider_morse",
    "phonex",
    "mra",
    "match_rating_approach",
    "daitch_mokotoff",
]

type NormalizationForm = Literal["nfc", "nfd", "nfkc", "nfkd"]

type DateResolution = Literal["year", "month", "day"]

type Linkage = Literal["single", "complete", "average"]

type CaseFoldLocale = Literal["default", "turkish"]

# Raises ValueError for unknown presets
type CompositePreset = str

# String metrics

def levenshtein(a: str, b: str) -> int: ...
def levenshtein_similarity(a: str, b: str) -> float: ...
def damerau_levenshtein(a: str, b: str) -> int: ...
def damerau_levenshtein_similarity(a: str, b: str) -> float: ...
def hamming(a: str, b: str) -> int: ...
def hamming_similarity(a: str, b: str) -> float: ...
def jaro(a: str, b: str) -> float: ...
def jaro_winkler(a: str, b: str, prefix_weight: float = 0.1) -> float: ...
def cosine(a: str, b: str, n: int = 2) -> float: ...
def jaccard(a: str, b: str) -> float: ...
def sorensen_dice(a: str, b: str) -> float: ...
def weighted_levenshtein(
    a: str,
    b: str,
    insert_cost: float = 1.0,
    delete_cost: float = 1.0,
    substitute_cost: float = 1.0,
    transpose_cost: float = 1.0,
) -> float: ...
def weighted_levenshtein_similarity(
    a: str,
    b: str,
    insert_cost: float = 1.0,
    delete_cost: float = 1.0,
    substitute_cost: float = 1.0,
    transpose_cost: float = 1.0,
) -> float: ...
def token_sort_ratio(a: str, b: str) -> float: ...
def token_set_ratio(a: str, b: str) -> float: ...
def partial_ratio(a: str, b: str) -> float: ...
def lcs_length(a: str, b: str) -> int: ...
def lcs_similarity(a: str, b: str) -> float: ...
def longest_common_substring_length(a: str, b: str) -> int: ...
def longest_common_substring_similarity(a: str, b: str) -> float: ...
def ngram_similarity(a: str, b: str, n: int = 2) -> float: ...
def smith_waterman(
    a: str,
    b: str,
    match_score: float = 2.0,
    mismatch_penalty: float = -1.0,
    gap_penalty: float = -1.0,
) -> float: ...
def smith_waterman_similarity(a: str, b: str) -> float: ...
def phonetic_hybrid(
    a: str,
    b: str,
    phonetic: PhoneticAlgorithm = "soundex",
    metric: Scorer = "jaro_winkler",
    phonetic_weight: float = 0.3,
) -> float: ...
def levenshtein_threshold(a: str, b: str, max_distance: int) -> int | None: ...
def ratcliff_obershelp(a: str, b: str) -> float: ...
def needleman_wunsch(a: str, b: str) -> float: ...
def gotoh(a: str, b: str) -> float: ...
def monge_elkan(
    a: str,
    b: str,
    inner_metric: Scorer | None = None,
) -> float: ...
def set_max_string_length(max_len: int) -> None: ...
def get_max_string_length() -> int: ...
def register_metric(name: str, func: object) -> None: ...
def unregister_metric(name: str) -> bool: ...
def list_custom_metrics() -> list[str]: ...

# Custom plugins

def register_blocker(name: str, obj: object) -> None: ...
def unregister_blocker(name: str) -> bool: ...
def list_custom_blockers() -> list[str]: ...
def register_comparator(name: str, func: object) -> None: ...
def unregister_comparator(name: str) -> bool: ...
def list_custom_comparators() -> list[str]: ...
def register_classifier(name: str, func: object) -> None: ...
def unregister_classifier(name: str) -> bool: ...
def list_custom_classifiers() -> list[str]: ...
def register_preprocessor(name: str, func: object) -> None: ...
def unregister_preprocessor(name: str) -> bool: ...
def list_custom_preprocessors() -> list[str]: ...
def register_tokenizer(name: str, func: object) -> None: ...
def unregister_tokenizer(name: str) -> bool: ...
def list_custom_tokenizers() -> list[str]: ...
def damerau_levenshtein_threshold(a: str, b: str, max_distance: int) -> int | None: ...
def levenshtein_align(a: str, b: str) -> dict[str, object]: ...
def match_best(
    query: str,
    candidates: list[str],
    scorer: Scorer = "jaro_winkler",
    threshold: float | None = None,
    workers: int | None = None,
) -> tuple[str, float, int] | None: ...
def match_batch(
    query: str,
    candidates: list[str],
    scorer: Scorer = "jaro_winkler",
    threshold: float | None = None,
    limit: int | None = None,
    workers: int | None = None,
) -> list[tuple[str, float, int]]: ...
def cdist(
    a: Sequence[str],
    b: Sequence[str],
    scorer: Scorer = "jaro_winkler",
    workers: int | None = None,
) -> NDArray[np.float64]: ...

# Phonetic

def soundex(s: str) -> str: ...
def metaphone(s: str) -> str: ...
def double_metaphone(s: str) -> tuple[str, str]: ...
def nysiis(s: str) -> str: ...
def caverphone(s: str) -> str: ...
def cologne_phonetic(s: str) -> str: ...
def beider_morse(s: str, ashkenazi: bool = False) -> str: ...
def detect_language(s: str) -> str: ...
def phonex(s: str) -> str: ...
def mra(s: str) -> str: ...
def mra_compare(a: str, b: str) -> bool: ...
def daitch_mokotoff(s: str) -> str: ...

# Preprocessing

def fold_case(s: str) -> str: ...
def normalize_whitespace(s: str) -> str: ...
def strip_punctuation(s: str) -> str: ...
def standardize_name(s: str) -> str: ...
def remove_stop_words(s: str) -> str: ...
def expand_abbreviations(s: str) -> str: ...
def strip_diacritics(s: str) -> str: ...
def regex_replace(s: str, pattern: str, replacement: str) -> str: ...
def fold_case_locale(s: str, locale: CaseFoldLocale = "default") -> str: ...
def locale_aware_compare(a: str, b: str, locale: CaseFoldLocale = "default") -> bool: ...

# Domain preprocessors

def clean_name(s: str) -> str: ...
def clean_address(s: str) -> str: ...
def clean_company(s: str) -> str: ...
def normalize_email(s: str) -> str: ...
def normalize_url(s: str) -> str: ...
def synonym_expand(s: str, table: dict[str, str]) -> str: ...
def transliterate_cyrillic(s: str) -> str: ...
def transliterate_greek(s: str) -> str: ...
def transliterate_arabic(s: str) -> str: ...
def transliterate_hebrew(s: str) -> str: ...
def strip_arabic_diacritics(s: str) -> str: ...
def strip_hebrew_diacritics(s: str) -> str: ...
def normalize_arabic(s: str) -> str: ...
def strip_bidi_marks(s: str) -> str: ...
def transliterate_devanagari(s: str) -> str: ...
def transliterate_hangul(s: str) -> str: ...

# Tokenization & Unicode normalization

def ngram_tokenize(s: str, n: int = 2) -> list[str]: ...
def whitespace_tokenize(s: str) -> list[str]: ...
def character_tokenize(s: str) -> list[str]: ...
def smart_tokenize(s: str) -> list[str]: ...
def normalize_unicode(s: str, form: NormalizationForm = "nfkc") -> str: ...

# Batch preprocessing

def preprocess_batch(strings: list[str], operations: list[str]) -> list[str]: ...
def ngram_tokenize_batch(strings: list[str], n: int = 2) -> list[list[str]]: ...
def whitespace_tokenize_batch(strings: list[str]) -> list[list[str]]: ...
def character_tokenize_batch(strings: list[str]) -> list[list[str]]: ...
def smart_tokenize_batch(strings: list[str]) -> list[list[str]]: ...
def cjk_ngram_tokenize(s: str, n: int = 2) -> list[str]: ...
def smart_tokenize_ngram(s: str, n: int = 2) -> list[str]: ...
def cjk_ngram_tokenize_batch(strings: list[str], n: int = 2) -> list[list[str]]: ...
def smart_tokenize_ngram_batch(strings: list[str], n: int = 2) -> list[list[str]]: ...
def explain(a: str, b: str, algorithms: list[Scorer] | None = None) -> dict[str, float]: ...

# BK-Tree

class PyBkTree:
    @staticmethod
    def build(strings: list[str], metric: Scorer) -> PyBkTree: ...
    def find_within(self, query: str, max_distance: int) -> list[tuple[str, int, int]]: ...
    def find_nearest(self, query: str, k: int) -> list[tuple[str, int, int]]: ...
    def __len__(self) -> int: ...
    def insert(self, s: str) -> int: ...
    def remove(self, index: int) -> bool: ...
    def __contains__(self, index: int) -> bool: ...
    def memory_usage(self) -> int: ...
    def memory_usage_human(self) -> str: ...
    def save(self, path: str) -> None: ...
    @staticmethod
    def load(path: str) -> PyBkTree: ...

# VP-Tree

class PyVpTree:
    @staticmethod
    def build(strings: list[str], metric: Scorer) -> PyVpTree: ...
    def find_within(self, query: str, max_distance: float) -> list[tuple[str, int, float]]: ...
    def find_nearest(self, query: str, k: int) -> list[tuple[str, int, float]]: ...
    def __len__(self) -> int: ...
    def insert(self, s: str) -> int: ...
    def remove(self, index: int) -> bool: ...
    def __contains__(self, index: int) -> bool: ...
    def rebuild(self) -> None: ...
    def memory_usage(self) -> int: ...
    def memory_usage_human(self) -> str: ...
    def save(self, path: str) -> None: ...
    @staticmethod
    def load(path: str) -> PyVpTree: ...

# N-gram Index

class PyNgramIndex:
    @staticmethod
    def build(strings: list[str], n: int) -> PyNgramIndex: ...
    def search(self, query: str, threshold: int) -> list[tuple[str, int, int]]: ...
    def search_top_k(self, query: str, k: int) -> list[tuple[str, int, int]]: ...
    def __len__(self) -> int: ...
    def insert(self, s: str) -> int: ...
    def remove(self, index: int) -> bool: ...
    def __contains__(self, index: int) -> bool: ...
    def memory_usage(self) -> int: ...
    def memory_usage_human(self) -> str: ...
    def save(self, path: str) -> None: ...
    @staticmethod
    def load(path: str) -> PyNgramIndex: ...

# Memory-mapped N-gram Index

class PyMmapNgramIndex:
    @staticmethod
    def build_and_save(strings: list[str], n: int, path: str) -> None: ...
    @staticmethod
    def open(path: str) -> PyMmapNgramIndex: ...
    def search(self, query: str, threshold: int) -> list[tuple[str, int, int]]: ...
    def search_top_k(self, query: str, k: int) -> list[tuple[str, int, int]]: ...
    def __len__(self) -> int: ...
    def memory_usage(self) -> int: ...
    def memory_usage_human(self) -> str: ...

# MinHash Index

class PyMinHashIndex:
    @staticmethod
    def build(
        strings: list[str],
        num_hashes: int = 100,
        num_bands: int = 20,
        shingle_size: int = 3,
    ) -> PyMinHashIndex: ...
    def query(self, query: str, threshold: float = 0.5) -> list[tuple[int, str, float]]: ...
    def insert(self, s: str) -> int: ...
    def remove(self, index: int) -> bool: ...
    def __len__(self) -> int: ...
    def memory_usage(self) -> int: ...
    def memory_usage_human(self) -> str: ...
    def save(self, path: str) -> None: ...
    @staticmethod
    def load(path: str) -> PyMinHashIndex: ...

# CompositeScorer

class PyCompositeScorer:
    def __init__(self, components: list[tuple[Scorer, float]]) -> None: ...
    @staticmethod
    def preset(name: CompositePreset) -> PyCompositeScorer: ...
    def similarity(self, a: str, b: str) -> float: ...
    def match_best(
        self,
        query: str,
        candidates: list[str],
        threshold: float | None = None,
    ) -> tuple[str, float, int] | None: ...
    def match_batch(
        self,
        query: str,
        candidates: list[str],
        threshold: float | None = None,
        limit: int | None = None,
    ) -> list[tuple[str, float, int]]: ...

# StreamingMatcher

class PyStreamingMatcher:
    def __init__(
        self,
        query: str,
        scorer: Scorer = "jaro_winkler",
        threshold: float | None = None,
    ) -> None: ...
    def score(self, candidate: str) -> float | None: ...
    def score_chunk(self, candidates: list[str]) -> list[tuple[int, float]]: ...

# Bounded Streaming Matcher

class PyBoundedStreamingMatcher:
    def __init__(
        self,
        query: str,
        scorer: Scorer = "jaro_winkler",
        threshold: float | None = None,
        buffer_size: int = 64,
    ) -> None: ...
    def score_bounded(
        self, candidates: list[str], offset: int = 0
    ) -> list[tuple[str, float, int]]: ...

# TF-IDF

class PyTfIdfMatcher:
    @staticmethod
    def fit(corpus: list[str]) -> PyTfIdfMatcher: ...
    def similarity(self, a: str, b: str) -> float: ...
    def match_batch(
        self,
        query: str,
        candidates: list[str],
        threshold: float | None = None,
    ) -> list[tuple[str, float, int]]: ...
    def __len__(self) -> int: ...

# EM estimation

class PyEmResult:
    m_probs: list[float]
    u_probs: list[float]
    p_match: float
    iterations: int
    converged: bool

def estimate_fellegi_sunter_params(
    vectors: list[list[float]],
    max_iterations: int = 100,
    convergence_threshold: float = 1e-6,
    initial_p_match: float = 0.1,
) -> PyEmResult: ...

# Arrow-friendly batch operations

def cdist_arrow(a: list[str], b: list[str], scorer: Scorer = "jaro_winkler") -> list[float]: ...
def match_best_arrow(
    query: str,
    candidates: list[str],
    scorer: Scorer = "jaro_winkler",
    threshold: float = 0.0,
) -> tuple[str, float, int] | None: ...
def match_batch_arrow(
    query: str,
    candidates: list[str],
    scorer: Scorer = "jaro_winkler",
    threshold: float = 0.0,
    limit: int | None = None,
) -> list[tuple[str, float, int]]: ...
def pairwise_similarity(
    a: list[str], b: list[str], scorer: Scorer = "jaro_winkler"
) -> list[float]: ...
def phonetic_batch_arrow(
    strings: list[str], algorithm: PhoneticAlgorithm = "soundex"
) -> list[str]: ...

# Pipeline

class PyRecord:
    def __init__(self, id: str) -> None: ...
    def set_field(self, name: str, value: str) -> None: ...
    def get_field(self, name: str) -> str | None: ...

class PyMatchResult:
    left_id: str
    right_id: str
    score: float
    scores: list[float]
    match_class: str
    def __repr__(self) -> str: ...
    def __str__(self) -> str: ...
    def __eq__(self, other: object) -> bool: ...
    def __hash__(self) -> int: ...

class PyPipeline:
    def __init__(self) -> None: ...
    def preprocess_lowercase(self, fields: list[str]) -> None: ...
    def preprocess(self, field: str, operations: list[str]) -> None: ...
    def block_exact(self, field: str) -> None: ...
    def block_phonetic(self, field: str, algorithm: PhoneticAlgorithm = "soundex") -> None: ...
    def block_sorted_neighborhood(self, field: str, window: int = 3) -> None: ...
    def block_trie(self, field: str, min_prefix_len: int = 2, max_frequency: int = 100) -> None: ...
    def block_qgram(self, field: str, q: int = 3, threshold: int = 1) -> None: ...
    def block_numeric(self, field: str, bucket_size: float = 5.0) -> None: ...
    def block_date(self, field: str, resolution: DateResolution = "year") -> None: ...
    def compare_string(self, field: str, metric: Scorer = "jaro_winkler") -> None: ...
    def compare_exact(self, field: str) -> None: ...
    def compare_numeric(self, field: str, max_diff: float = 10.0) -> None: ...
    def compare_date(self, field: str) -> None: ...
    def compare_phonetic(self, field: str, algorithm: PhoneticAlgorithm = "soundex") -> None: ...
    def block_custom(self, name: str) -> None: ...
    def compare_custom(self, field: str, name: str) -> None: ...
    def classify_custom(self, name: str) -> None: ...
    def classify_threshold(self, threshold: float) -> None: ...
    def classify_weighted(self, weights: list[float], threshold: float) -> None: ...
    def classify_threshold_bands(self, upper: float, lower: float) -> None: ...
    def classify_weighted_bands(self, weights: list[float], upper: float, lower: float) -> None: ...
    def classify_fellegi_sunter(
        self,
        m_probs: list[float],
        u_probs: list[float],
        upper: float,
        lower: float,
    ) -> None: ...
    def classify_fellegi_sunter_auto(
        self,
        max_iterations: int = 100,
        convergence_threshold: float = 1e-6,
        initial_p_match: float = 0.1,
    ) -> None: ...
    def cluster_connected_components(self) -> None: ...
    def cluster_hierarchical(self, linkage: Linkage = "single", threshold: float = 0.5) -> None: ...
    def block_lsh(self, field: str, num_hashes: int = 100, num_bands: int = 20) -> None: ...
    def block_canopy(
        self,
        field: str,
        t_tight: float = 0.9,
        t_loose: float = 0.5,
        metric: Scorer = "jaro_winkler",
    ) -> None: ...
    def with_profiling(self, enabled: bool) -> None: ...
    def get_profiling_stats(self) -> dict[str, int]: ...
    def to_json(self) -> str: ...
    @staticmethod
    def from_json(json: str) -> PyPipeline: ...
    def dedup(self, records: list[PyRecord]) -> list[PyMatchResult]: ...
    def dedup_cluster(self, records: list[PyRecord]) -> list[list[str]]: ...
    def link(
        self,
        left: list[PyRecord],
        right: list[PyRecord],
    ) -> list[PyMatchResult]: ...

class PyIncrementalCluster:
    def __init__(self, metric: str = "jaro_winkler", threshold: float = 0.85) -> None: ...
    def add_record(self, value: str) -> tuple[int, bool, float | None]: ...
    def get_clusters(self) -> list[list[int]]: ...
    def cluster_count(self) -> int: ...
    def record_count(self) -> int: ...

class PyBloomFilter:
    def __init__(self, expected_items: int = 1000, false_positive_rate: float = 0.01) -> None: ...
    def insert(self, item: str) -> None: ...
    def contains(self, item: str) -> bool: ...
    def __len__(self) -> int: ...
    def __contains__(self, item: str) -> bool: ...
    def memory_usage(self) -> str: ...
    def estimated_fp_rate(self) -> float: ...

class PyInvertedIndex:
    @staticmethod
    def build(strings: list[str], tokenizer: str = "whitespace") -> PyInvertedIndex: ...
    def search(self, query: str, min_shared: int) -> list[tuple[str, int, int]]: ...
    def search_top_k(self, query: str, k: int) -> list[tuple[str, int, int]]: ...
    def __len__(self) -> int: ...
    def vocab_size(self) -> int: ...

# Clustering functions
def silhouette_score(
    num_nodes: int,
    similarities: list[tuple[int, int, float]],
    labels: list[int],
) -> float: ...
def davies_bouldin_index(
    num_nodes: int,
    similarities: list[tuple[int, int, float]],
    labels: list[int],
) -> float: ...
def dbscan_cluster(
    num_nodes: int,
    similarities: list[tuple[int, int, float]],
    min_similarity: float,
    min_samples: int,
) -> tuple[list[list[int]], list[int], list[int]]: ...
def optics_cluster(
    num_nodes: int,
    similarities: list[tuple[int, int, float]],
    min_samples: int,
    extract_threshold: float,
) -> tuple[list[list[int]], list[int]]: ...

# ML classifier training
def train_logistic_regression(
    vectors: list[list[float]],
    labels: list[bool],
    learning_rate: float = 0.1,
    max_iterations: int = 1000,
    regularization: float = 0.01,
    threshold: float = 0.5,
) -> tuple[list[float], float, float]: ...
def train_decision_tree(
    vectors: list[list[float]],
    labels: list[bool],
    max_depth: int = 5,
    min_samples_leaf: int = 5,
    min_samples_split: int = 10,
    match_threshold: float = 0.5,
) -> str: ...
