# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Content Filter Engine for Private Browser.

Blocks ads, trackers, and unwanted content before
it reaches the user's device.
"""

import logging
import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
from urllib.parse import urljoin, urlparse

logger = logging.getLogger(__name__)


class FilterLevel(Enum):
    """Content filtering levels."""

    NONE = "none"
    STANDARD = "standard"
    STRICT = "strict"
    PARANOID = "paranoid"


# Built-in blocklists (subset - real lists would be loaded from files)
TRACKER_DOMAINS = {
    # Analytics
    "google-analytics.com",
    "googletagmanager.com",
    "doubleclick.net",
    "facebook.com/tr",
    "connect.facebook.net",
    "pixel.facebook.com",
    "analytics.twitter.com",
    "bat.bing.com",
    "ads.linkedin.com",
    "snap.licdn.com",
    "px.ads.linkedin.com",
    "hotjar.com",
    "mixpanel.com",
    "segment.io",
    "segment.com",
    "amplitude.com",
    "heapanalytics.com",
    "fullstory.com",
    "crazyegg.com",
    "mouseflow.com",
    "luckyorange.com",
    # Ad networks
    "googlesyndication.com",
    "googleadservices.com",
    "googleads.g.doubleclick.net",
    "adnxs.com",
    "adsrvr.org",
    "advertising.com",
    "adform.net",
    "criteo.com",
    "criteo.net",
    "outbrain.com",
    "taboola.com",
    "mgid.com",
    "revcontent.com",
    "amazon-adsystem.com",
    "media.net",
    # Social widgets (tracking)
    "platform.twitter.com",
    "connect.facebook.net",
    "apis.google.com/js/plusone",
    "widgets.pinterest.com",
    # Fingerprinting
    "cdn.jsdelivr.net/npm/fingerprintjs",
    "fpjs.io",
    "clarity.ms",
}

AD_PATTERNS = [
    r'<div[^>]+class="[^"]*(?:ad|ads|advert|advertisement|sponsor|promo)[^"]*"[^>]*>.*?</div>',
    r'<aside[^>]+class="[^"]*(?:ad|ads|sidebar-ad)[^"]*"[^>]*>.*?</aside>',
    r'<ins[^>]+class="adsbygoogle"[^>]*>.*?</ins>',
    r"<iframe[^>]+(?:doubleclick|googlesyndication|googleads)[^>]*>.*?</iframe>",
    r"<!--\s*(?:ad|advertisement)\s*-->.*?<!--\s*/(?:ad|advertisement)\s*-->",
]

TRACKER_PATTERNS = [
    # Google Analytics
    r"<script[^>]*(?:google-analytics|googletagmanager|gtag)[^>]*>.*?</script>",
    r'ga\s*\(\s*[\'"]create[\'"]',
    r'gtag\s*\(\s*[\'"]config[\'"]',
    # Facebook Pixel
    r"<script[^>]*facebook[^>]*>.*?</script>",
    r'fbq\s*\(\s*[\'"]init[\'"]',
    # General tracking pixels
    r"<img[^>]+(?:pixel|beacon|tracker|1x1|tracking)[^>]*>",
    r'<img[^>]+(?:width|height)=["\']1["\'][^>]*>',
    # Tracking scripts
    r"<script[^>]*(?:analytics|tracking|pixel|beacon)[^>]*>.*?</script>",
]

SOCIAL_WIDGET_PATTERNS = [
    r'<div[^>]+class="[^"]*(?:fb-like|twitter-share|social-buttons)[^"]*"[^>]*>.*?</div>',
    r"<iframe[^>]+(?:facebook\.com/plugins|platform\.twitter\.com/widgets)[^>]*>.*?</iframe>",
]


@dataclass
class FilterStats:
    """Statistics for filtered content."""

    ads_blocked: int = 0
    trackers_blocked: int = 0
    scripts_blocked: int = 0
    images_blocked: int = 0
    social_blocked: int = 0
    requests_blocked: int = 0


class FilterEngine:
    """
    Content filtering engine.

    Processes HTML to remove:
    - Advertisements
    - Tracking scripts/pixels
    - Social media widgets
    - Third-party scripts (in strict mode)
    - Images (in paranoid mode)
    """

    def __init__(self, config):
        self.config = config

        # Determine filter level
        if config.privacy_mode == "paranoid":
            self.level = FilterLevel.PARANOID
        elif config.privacy_mode == "strict":
            self.level = FilterLevel.STRICT
        else:
            self.level = FilterLevel.STANDARD

        # Compile patterns
        self._ad_patterns = [re.compile(p, re.IGNORECASE | re.DOTALL) for p in AD_PATTERNS]
        self._tracker_patterns = [
            re.compile(p, re.IGNORECASE | re.DOTALL) for p in TRACKER_PATTERNS
        ]
        self._social_patterns = [
            re.compile(p, re.IGNORECASE | re.DOTALL) for p in SOCIAL_WIDGET_PATTERNS
        ]

        # Custom blocklists
        self._custom_domains: Set[str] = set()
        self._custom_patterns: List[re.Pattern] = []

        # Stats
        self._stats = FilterStats()

    async def load_blocklists(self, blocklist_dir: Optional[Path] = None):
        """Load additional blocklists from files."""
        if blocklist_dir and blocklist_dir.exists():
            # Load custom domain lists
            domains_file = blocklist_dir / "domains.txt"
            if domains_file.exists():
                for line in domains_file.read_text().splitlines():
                    line = line.strip()
                    if line and not line.startswith("#"):
                        self._custom_domains.add(line)

            # Load custom patterns
            patterns_file = blocklist_dir / "patterns.txt"
            if patterns_file.exists():
                for line in patterns_file.read_text().splitlines():
                    line = line.strip()
                    if line and not line.startswith("#"):
                        try:
                            self._custom_patterns.append(
                                re.compile(line, re.IGNORECASE | re.DOTALL)
                            )
                        except re.error:
                            logger.warning(f"Invalid pattern: {line}")

        logger.info(f"Filter engine loaded (level: {self.level.value})")

    async def process(
        self,
        content: str,
        url: str,
        content_type: str,
    ) -> Dict[str, Any]:
        """
        Process and filter content.

        Args:
            content: Raw HTML/content
            url: Page URL (for link rewriting)
            content_type: MIME type

        Returns:
            {
                'html': filtered HTML,
                'blocked': counts of blocked items,
            }
        """
        if "html" not in content_type:
            # Not HTML, return as-is
            return {
                "html": content,
                "blocked": {},
            }

        blocked = {
            "ads": 0,
            "trackers": 0,
            "scripts": 0,
            "images": 0,
            "social": 0,
        }

        html = content

        # Remove ads
        if self.config.block_ads:
            html, count = self._remove_ads(html)
            blocked["ads"] = count
            self._stats.ads_blocked += count

        # Remove trackers
        if self.config.block_trackers:
            html, count = self._remove_trackers(html)
            blocked["trackers"] = count
            self._stats.trackers_blocked += count

        # Remove external scripts (strict/paranoid)
        if self.config.block_scripts:
            html, count = self._remove_scripts(html, url)
            blocked["scripts"] = count
            self._stats.scripts_blocked += count

        # Remove images (paranoid only)
        if self.config.block_images:
            html, count = self._remove_images(html)
            blocked["images"] = count
            self._stats.images_blocked += count

        # Remove social widgets
        html, count = self._remove_social(html)
        blocked["social"] = count
        self._stats.social_blocked += count

        # Filter URLs in remaining content
        html = self._filter_urls(html, url)

        # Apply custom patterns
        for pattern in self._custom_patterns:
            html = pattern.sub("", html)

        return {
            "html": html,
            "blocked": blocked,
        }

    def _remove_ads(self, html: str) -> tuple[str, int]:
        """Remove advertisements."""
        count = 0

        for pattern in self._ad_patterns:
            matches = pattern.findall(html)
            count += len(matches)
            html = pattern.sub("<!-- ad removed -->", html)

        # Remove ad-related divs by ID
        ad_ids = ["ad", "ads", "advertisement", "banner", "sponsor", "promo"]
        for ad_id in ad_ids:
            pattern = re.compile(
                rf'<div[^>]+id="{ad_id}"[^>]*>.*?</div>', re.IGNORECASE | re.DOTALL
            )
            matches = pattern.findall(html)
            count += len(matches)
            html = pattern.sub("", html)

        return html, count

    def _remove_trackers(self, html: str) -> tuple[str, int]:
        """Remove tracking scripts and pixels."""
        count = 0

        for pattern in self._tracker_patterns:
            matches = pattern.findall(html)
            count += len(matches)
            html = pattern.sub("<!-- tracker removed -->", html)

        # Remove scripts loading from tracker domains
        for domain in TRACKER_DOMAINS:
            pattern = re.compile(
                rf'<script[^>]+src="[^"]*{re.escape(domain)}[^"]*"[^>]*>.*?</script>',
                re.IGNORECASE | re.DOTALL,
            )
            matches = pattern.findall(html)
            count += len(matches)
            html = pattern.sub("", html)

            # Also remove image pixels from tracker domains
            pattern = re.compile(
                rf'<img[^>]+src="[^"]*{re.escape(domain)}[^"]*"[^>]*/?>', re.IGNORECASE
            )
            matches = pattern.findall(html)
            count += len(matches)
            html = pattern.sub("", html)

        return html, count

    def _remove_scripts(self, html: str, page_url: str) -> tuple[str, int]:
        """Remove external scripts (strict mode)."""
        count = 0
        page_domain = urlparse(page_url).netloc

        def replace_script(match):
            nonlocal count
            script_tag = match.group(0)

            # Check if it's an external script
            src_match = re.search(r'src=["\']([^"\']+)["\']', script_tag)
            if src_match:
                src = src_match.group(1)
                src_domain = urlparse(src).netloc

                # Allow same-domain scripts
                if src_domain and src_domain != page_domain:
                    count += 1
                    return "<!-- external script removed -->"

            return script_tag

        html = re.sub(
            r"<script[^>]*>.*?</script>", replace_script, html, flags=re.IGNORECASE | re.DOTALL
        )

        return html, count

    def _remove_images(self, html: str) -> tuple[str, int]:
        """Remove all images (paranoid mode)."""
        pattern = re.compile(r"<img[^>]*/?>", re.IGNORECASE)
        matches = pattern.findall(html)
        html = pattern.sub("[image removed]", html)

        # Also remove picture elements
        pattern = re.compile(r"<picture[^>]*>.*?</picture>", re.IGNORECASE | re.DOTALL)
        matches2 = pattern.findall(html)
        html = pattern.sub("[image removed]", html)

        return html, len(matches) + len(matches2)

    def _remove_social(self, html: str) -> tuple[str, int]:
        """Remove social media widgets."""
        count = 0

        for pattern in self._social_patterns:
            matches = pattern.findall(html)
            count += len(matches)
            html = pattern.sub("<!-- social widget removed -->", html)

        return html, count

    def _filter_urls(self, html: str, page_url: str) -> str:
        """Filter URLs: resolve relative, block trackers."""

        def filter_src(match):
            tag = match.group(1)
            attr = match.group(2)
            quote = match.group(3)
            url = match.group(4)

            parsed = urlparse(url)
            if not parsed.netloc and page_url:
                url = urljoin(page_url, url)
                parsed = urlparse(url)

            domain = parsed.netloc
            if domain:
                for tracker in TRACKER_DOMAINS:
                    if tracker in domain:
                        return f"<{tag} data-blocked={quote}{attr}{quote}>"
                for custom in self._custom_domains:
                    if custom in domain:
                        return f"<{tag} data-blocked={quote}{attr}{quote}>"

            return match.group(0)

        html = re.sub(
            r'<(\w+)[^>]+(src|href)=(["\'])([^"\']+)\3',
            filter_src,
            html,
            flags=re.IGNORECASE,
        )
        return html

    def should_block_url(self, url: str) -> bool:
        """Check if a URL should be blocked."""
        parsed = urlparse(url)
        domain = parsed.netloc

        # Check tracker domains
        for tracker in TRACKER_DOMAINS:
            if tracker in domain:
                return True

        # Check custom domains
        for custom in self._custom_domains:
            if custom in domain:
                return True

        return False

    def add_blocked_domain(self, domain: str):
        """Add a domain to the blocklist."""
        self._custom_domains.add(domain.lower())

    def remove_blocked_domain(self, domain: str):
        """Remove a domain from the blocklist."""
        self._custom_domains.discard(domain.lower())

    def get_stats(self) -> Dict[str, Any]:
        """Get filter statistics."""
        return {
            "level": self.level.value,
            "ads_blocked": self._stats.ads_blocked,
            "trackers_blocked": self._stats.trackers_blocked,
            "scripts_blocked": self._stats.scripts_blocked,
            "images_blocked": self._stats.images_blocked,
            "social_blocked": self._stats.social_blocked,
            "custom_domains": len(self._custom_domains),
            "custom_patterns": len(self._custom_patterns),
        }

    def reset_stats(self):
        """Reset statistics."""
        self._stats = FilterStats()
