"""
B2C Domain Bloom Filter
=======================
Fast lookup data structure for B2C domain checking with minimal memory usage.
Provides O(1) lookups with configurable false positive rate.
"""

import pickle
import logging
import hashlib
from pathlib import Path
from typing import Set, Optional, Union
from bitarray import bitarray

log = logging.getLogger(__name__)

HEALTHCARE_ALLOWLIST = {
    # Temporary override for legitimate enterprise healthcare domains that were
    # mistakenly added to the B2C list or trigger bloom false-positives.
    "sutterhealth.org",
    "yorkhospital.com",
}


class B2CBloomFilter:
    """
    Bloom filter implementation optimized for B2C domain lookups.

    Memory usage: ~0.8 MB for 15,000 domains at 0.01% false positive rate
    """

    def __init__(
        self, expected_items: int = 15000, false_positive_rate: float = 0.0001
    ):
        """
        Initialize bloom filter.

        Args:
            expected_items: Expected number of domains
            false_positive_rate: Target false positive rate (default 0.01%)
        """
        self.expected_items = expected_items
        self.false_positive_rate = false_positive_rate

        # Calculate optimal parameters
        self.size = self._optimal_size(expected_items, false_positive_rate)
        self.hash_count = self._optimal_hash_count(self.size, expected_items)

        # Initialize bit array
        self.bits = bitarray(self.size)
        self.bits.setall(0)

        # Track actual items for stats
        self.item_count = 0

        log.info(
            f"Initialized BloomFilter: size={self.size:,} bits "
            f"({self.size/8/1024:.1f} KB), hashes={self.hash_count}"
        )

    @staticmethod
    def _optimal_size(n: int, p: float) -> int:
        """Calculate optimal bit array size."""
        import math

        m = -(n * math.log(p)) / (math.log(2) ** 2)
        return int(m)

    @staticmethod
    def _optimal_hash_count(m: int, n: int) -> int:
        """Calculate optimal number of hash functions."""
        import math

        k = (m / n) * math.log(2)
        return max(1, int(k))

    def _hash(self, item: str, seed: int) -> int:
        """Generate hash value for item with given seed."""
        h = hashlib.sha256(f"{seed}{item}".encode()).digest()
        return int.from_bytes(h[:8], "big") % self.size

    def add(self, domain: str) -> None:
        """Add a domain to the filter."""
        domain = domain.lower().strip()
        for i in range(self.hash_count):
            pos = self._hash(domain, i)
            self.bits[pos] = 1
        self.item_count += 1

    def add_many(self, domains: Set[str]) -> None:
        """Add multiple domains efficiently."""
        for domain in domains:
            self.add(domain)

    def __contains__(self, domain: str) -> bool:
        """Check if domain might be in the filter."""
        domain = domain.lower().strip()
        for i in range(self.hash_count):
            pos = self._hash(domain, i)
            if not self.bits[pos]:
                return False
        return True

    def get_stats(self) -> dict:
        """Get filter statistics."""
        bits_set = self.bits.count(1)
        fill_ratio = bits_set / self.size

        # Estimate false positive rate based on current fill

        estimated_fpr = fill_ratio**self.hash_count

        return {
            "item_count": self.item_count,
            "size_bits": self.size,
            "size_bytes": self.size // 8,
            "size_kb": self.size / 8 / 1024,
            "hash_functions": self.hash_count,
            "bits_set": bits_set,
            "fill_ratio": fill_ratio,
            "target_fpr": self.false_positive_rate,
            "estimated_fpr": estimated_fpr,
        }

    def save(self, path: Union[str, Path]) -> None:
        """Save filter to disk."""
        path = Path(path)
        data = {
            "expected_items": self.expected_items,
            "false_positive_rate": self.false_positive_rate,
            "size": self.size,
            "hash_count": self.hash_count,
            "item_count": self.item_count,
            "bits": self.bits.tobytes(),
        }

        with open(path, "wb") as f:
            pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)

        log.info(f"Saved bloom filter to {path}")

    @classmethod
    def load(cls, path: Union[str, Path]) -> "B2CBloomFilter":
        """Load filter from disk."""
        path = Path(path)

        with open(path, "rb") as f:
            data = pickle.load(f)

        # Create new instance
        bf = cls(data["expected_items"], data["false_positive_rate"])

        # Restore state
        bf.size = data["size"]
        bf.hash_count = data["hash_count"]
        bf.item_count = data["item_count"]
        bf.bits = bitarray()
        bf.bits.frombytes(data["bits"])

        log.info(f"Loaded bloom filter from {path}")
        return bf


class B2CDomainChecker:
    """
    High-level interface for B2C domain checking.
    Handles loading domain lists and creating filters.
    """

    def __init__(
        self,
        domain_file: Optional[Path] = None,
        filter_file: Optional[Path] = None,
        use_bloom: bool = True,
    ):
        """
        Initialize domain checker.

        Args:
            domain_file: Path to domain list file
            filter_file: Path to pre-built bloom filter
            use_bloom: Use bloom filter (True) or set (False)
        """
        self.use_bloom = use_bloom
        self._filter = None
        self._domain_set = None

        if filter_file and filter_file.exists():
            # Load pre-built filter
            self._filter = B2CBloomFilter.load(filter_file)
            log.info(
                f"Loaded pre-built filter with {self._filter.item_count:,} domains"
            )

        elif domain_file and domain_file.exists():
            # Build from domain list
            self._load_domains(domain_file)

        else:
            # Use minimal fallback set
            log.warning("No domain file found, using minimal fallback set")
            self._fallback_domains()

    def _load_domains(self, path: Path) -> None:
        """Load domains from file."""
        domains = set()

        with open(path, "r") as f:
            for line in f:
                domain = line.strip().lower()
                if domain:
                    domains.add(domain)

        log.info(f"Loaded {len(domains):,} domains from {path}")

        if self.use_bloom:
            self._filter = B2CBloomFilter(expected_items=len(domains))
            self._filter.add_many(domains)
        else:
            self._domain_set = domains

    def _fallback_domains(self) -> None:
        """Load minimal fallback domain set."""
        fallback = {
            "gmail.com",
            "yahoo.com",
            "hotmail.com",
            "outlook.com",
            "aol.com",
            "icloud.com",
            "protonmail.com",
            "yandex.com",
        }

        if self.use_bloom:
            self._filter = B2CBloomFilter(expected_items=len(fallback))
            self._filter.add_many(fallback)
        else:
            self._domain_set = fallback

    def is_b2c_domain(self, domain: str) -> bool:
        """
        Check if domain is a B2C/personal email domain.

        Args:
            domain: Domain to check (e.g., 'gmail.com')

        Returns:
            True if domain is B2C, False otherwise
        """
        if not domain:
            return False

        domain = domain.lower().strip()
        if domain in HEALTHCARE_ALLOWLIST:
            return False

        if self.use_bloom and self._filter:
            return domain in self._filter
        elif self._domain_set:
            return domain in self._domain_set
        else:
            return False

    def get_stats(self) -> dict:
        """Get checker statistics."""
        if self.use_bloom and self._filter:
            return self._filter.get_stats()
        elif self._domain_set:
            return {
                "type": "set",
                "domain_count": len(self._domain_set),
                "memory_bytes": len(self._domain_set) * 50,  # Rough estimate
            }
        else:
            return {"type": "empty"}

    def save_filter(self, path: Path) -> None:
        """Save bloom filter to disk for fast loading."""
        if self.use_bloom and self._filter:
            self._filter.save(path)
        else:
            raise ValueError("No bloom filter to save")


# Global singleton instance
_b2c_checker: Optional[B2CDomainChecker] = None


def get_b2c_checker() -> B2CDomainChecker:
    """Get or create the global B2C domain checker instance."""
    global _b2c_checker

    if _b2c_checker is None:
        # Try to find domain file in standard locations
        search_paths = [
            Path("resources/b2c_domains_v*.txt"),
            Path(__file__).parent / "resources/b2c_domains_v*.txt",
            Path.home() / ".fuzzymatcher/b2c_domains_v*.txt",
        ]

        domain_file = None
        for pattern in search_paths:
            files = list(Path(pattern.parent).glob(pattern.name))
            if files:
                # Use the newest version
                domain_file = max(files, key=lambda p: p.name)
                break

        # Also check for pre-built filter
        filter_paths = [
            Path("resources/b2c_domains.bloom"),
            Path(__file__).parent / "resources/b2c_domains.bloom",
        ]

        filter_file = None
        for path in filter_paths:
            if path.exists():
                filter_file = path
                break

        _b2c_checker = B2CDomainChecker(
            domain_file=domain_file, filter_file=filter_file
        )

    return _b2c_checker


def is_b2c_domain(domain: str) -> bool:
    """
    Convenience function to check if a domain is B2C.

    This is the main API that the engine should use.
    """
    return get_b2c_checker().is_b2c_domain(domain)


# Example usage and testing
if __name__ == "__main__":
    import time

    # Test bloom filter
    print("Testing B2C Bloom Filter...")

    # Create test domains
    test_b2c = {"gmail.com", "yahoo.com", "hotmail.com", "outlook.com"}
    test_corporate = {"acme.com", "example.com", "company.org"}

    # Build filter
    bf = B2CBloomFilter(expected_items=len(test_b2c))
    bf.add_many(test_b2c)

    # Test lookups
    print("\nTesting lookups:")
    for domain in test_b2c:
        assert domain in bf, f"{domain} should be in filter"
        print(f"  ✓ {domain} -> B2C")

    for domain in test_corporate:
        result = domain in bf
        print(
            f"  {'✗' if result else '✓'} {domain} -> {'B2C (FALSE POSITIVE!)' if result else 'Corporate'}"
        )

    # Stats
    print("\nFilter stats:")
    for key, value in bf.get_stats().items():
        print(f"  {key}: {value}")

    # Performance test
    print("\nPerformance test (1M lookups):")
    start = time.time()
    for _ in range(1_000_000):
        _ = "gmail.com" in bf
    elapsed = time.time() - start
    print(f"  Time: {elapsed:.3f}s ({1_000_000/elapsed:,.0f} lookups/sec)")

    # Test high-level API
    print("\nTesting high-level API:")
    print(f"  is_b2c_domain('gmail.com'): {is_b2c_domain('gmail.com')}")
    print(f"  is_b2c_domain('acme.com'): {is_b2c_domain('acme.com')}")
