"""Exact and near-duplicate fingerprinting for document deduplication.

Provides:
- SHA-256 exact fingerprint on normalized text
- SimHash for near-duplicate detection with configurable threshold
- Hamming distance comparison between SimHash values
"""

from __future__ import annotations

import hashlib
import re
import struct


def _normalize_text(text: str) -> str:
    """Normalize text for fingerprinting.

    Lowercases, collapses whitespace, strips leading/trailing space.
    This ensures minor formatting differences don't defeat deduplication.
    """
    text = text.lower()
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def compute_exact_fingerprint(text: str) -> str:
    """Compute SHA-256 fingerprint of normalized text.

    Args:
        text: Raw document text.

    Returns:
        Hex-encoded SHA-256 digest of normalized text.
    """
    normalized = _normalize_text(text)
    return hashlib.sha256(normalized.encode("utf-8")).hexdigest()


def _tokenize(text: str) -> list[str]:
    """Split normalized text into tokens for SimHash computation."""
    normalized = _normalize_text(text)
    return normalized.split()


def _hash_token(token: str) -> int:
    """Hash a single token to a 64-bit integer using MD5 truncation."""
    digest = hashlib.md5(token.encode("utf-8")).digest()  # noqa: S324
    return struct.unpack("<Q", digest[:8])[0]


def compute_simhash(text: str) -> int:
    """Compute 64-bit SimHash of text for near-duplicate detection.

    SimHash produces locality-sensitive fingerprints: similar documents
    will have SimHash values with low Hamming distance.

    Args:
        text: Raw document text.

    Returns:
        64-bit SimHash integer value.
    """
    tokens = _tokenize(text)
    if not tokens:
        return 0

    # Accumulator for each bit position
    v = [0] * 64

    for token in tokens:
        token_hash = _hash_token(token)
        for i in range(64):
            if token_hash & (1 << i):
                v[i] += 1
            else:
                v[i] -= 1

    # Build final hash from accumulator signs
    fingerprint = 0
    for i in range(64):
        if v[i] > 0:
            fingerprint |= 1 << i

    return fingerprint


def hamming_distance(a: int, b: int) -> int:
    """Compute Hamming distance between two 64-bit SimHash values.

    Args:
        a: First SimHash value.
        b: Second SimHash value.

    Returns:
        Number of differing bits (0-64).
    """
    xor = a ^ b
    # Count set bits (Brian Kernighan's algorithm)
    distance = 0
    while xor:
        xor &= xor - 1
        distance += 1
    return distance


def is_near_duplicate(fp1: int, fp2: int, threshold: int = 3) -> bool:
    """Determine if two SimHash fingerprints indicate near-duplicate content.

    Args:
        fp1: First SimHash value.
        fp2: Second SimHash value.
        threshold: Maximum Hamming distance to consider near-duplicate.
            Default of 3 is conservative for 64-bit SimHash.

    Returns:
        True if the documents are near-duplicates.
    """
    return hamming_distance(fp1, fp2) <= threshold
