"""
Advanced search and filtering capabilities for Meta Ads campaigns.

This module provides full-text search, complex filtering, and campaign
organization features for managing large-scale advertising operations.
"""

import re
from typing import Any, Dict, List, Optional
from dataclasses import dataclass
from enum import Enum

from .logging import get_logger
from .meta_client import MetaClient
from .errors import MetaApiError

logger = get_logger(__name__)


class FilterOperator(str, Enum):
    """Filter operators for complex queries."""
    EQUALS = "equals"
    NOT_EQUALS = "not_equals"
    CONTAINS = "contains"
    NOT_CONTAINS = "not_contains"
    STARTS_WITH = "starts_with"
    ENDS_WITH = "ends_with"
    GREATER_THAN = "greater_than"
    LESS_THAN = "less_than"
    GREATER_EQUAL = "greater_equal"
    LESS_EQUAL = "less_equal"
    IN = "in"
    NOT_IN = "not_in"
    IS_NULL = "is_null"
    IS_NOT_NULL = "is_not_null"
    REGEX = "regex"


class SortDirection(str, Enum):
    """Sort direction for search results."""
    ASC = "asc"
    DESC = "desc"


@dataclass
class FilterCondition:
    """Represents a single filter condition."""
    field: str
    operator: FilterOperator
    value: Any
    case_sensitive: bool = False


@dataclass
class SortCondition:
    """Represents a sort condition."""
    field: str
    direction: SortDirection


@dataclass
class SearchQuery:
    """Represents a complex search query."""
    text_query: Optional[str] = None
    filters: List[FilterCondition] = None
    sort_conditions: List[SortCondition] = None
    tags: List[str] = None
    limit: int = 100
    offset: int = 0
    
    def __post_init__(self):
        if self.filters is None:
            self.filters = []
        if self.sort_conditions is None:
            self.sort_conditions = []
        if self.tags is None:
            self.tags = []


@dataclass
class SearchResult:
    """Represents a search result."""
    id: str
    name: str
    type: str  # campaign, adset, ad
    status: str
    score: float
    matched_fields: List[str]
    metadata: Dict[str, Any]


@dataclass
class CampaignTag:
    """Represents a campaign tag."""
    tag_id: str
    name: str
    color: str
    description: Optional[str] = None
    created_time: Optional[str] = None


class AdvancedSearchEngine:
    """Advanced search engine for Meta Ads campaigns."""
    
    def __init__(self, client: MetaClient):
        self._client = client
        self._campaign_cache: Dict[str, Dict[str, Any]] = {}
        self._tag_index: Dict[str, List[str]] = {}  # tag -> campaign_ids
        self._text_index: Dict[str, Dict[str, Any]] = {}  # campaign_id -> text data
    
    async def search_campaigns(
        self,
        ad_account_id: str,
        query: SearchQuery,
    ) -> List[SearchResult]:
        """
        Search campaigns with complex filtering and text search.
        
        Args:
            ad_account_id: Ad account ID to search in
            query: Search query with filters and text search
        
        Returns:
            List[SearchResult]: Matching campaigns with relevance scores
        """
        try:
            # Get all campaigns for the account
            campaigns = await self._get_all_campaigns(ad_account_id)
            
            # Apply text search if provided
            if query.text_query:
                campaigns = self._apply_text_search(campaigns, query.text_query)
            
            # Apply filters
            if query.filters:
                campaigns = self._apply_filters(campaigns, query.filters)
            
            # Apply tag filters
            if query.tags:
                campaigns = self._apply_tag_filters(campaigns, query.tags)
            
            # Sort results
            if query.sort_conditions:
                campaigns = self._apply_sorting(campaigns, query.sort_conditions)
            
            # Convert to SearchResult objects
            results = []
            for campaign in campaigns[query.offset:query.offset + query.limit]:
                result = SearchResult(
                    id=campaign["id"],
                    name=campaign.get("name", ""),
                    type="campaign",
                    status=campaign.get("status", ""),
                    score=campaign.get("_search_score", 1.0),
                    matched_fields=campaign.get("_matched_fields", []),
                    metadata=campaign,
                )
                results.append(result)
            
            logger.info(
                "Campaign search completed",
                ad_account_id=ad_account_id,
                query_text=query.text_query,
                result_count=len(results),
            )
            
            return results
            
        except MetaApiError as exc:
            logger.error("Campaign search failed", error=exc.to_dict())
            raise
    
    async def search_adsets(
        self,
        ad_account_id: str,
        query: SearchQuery,
    ) -> List[SearchResult]:
        """Search ad sets with complex filtering."""
        try:
            adsets = await self._get_all_adsets(ad_account_id)
            
            if query.text_query:
                adsets = self._apply_text_search(adsets, query.text_query)
            
            if query.filters:
                adsets = self._apply_filters(adsets, query.filters)
            
            if query.tags:
                adsets = self._apply_tag_filters(adsets, query.tags)
            
            if query.sort_conditions:
                adsets = self._apply_sorting(adsets, query.sort_conditions)
            
            results = []
            for adset in adsets[query.offset:query.offset + query.limit]:
                result = SearchResult(
                    id=adset["id"],
                    name=adset.get("name", ""),
                    type="adset",
                    status=adset.get("status", ""),
                    score=adset.get("_search_score", 1.0),
                    matched_fields=adset.get("_matched_fields", []),
                    metadata=adset,
                )
                results.append(result)
            
            return results
            
        except MetaApiError as exc:
            logger.error("Ad set search failed", error=exc.to_dict())
            raise
    
    async def search_ads(
        self,
        ad_account_id: str,
        query: SearchQuery,
    ) -> List[SearchResult]:
        """Search ads with complex filtering."""
        try:
            ads = await self._get_all_ads(ad_account_id)
            
            if query.text_query:
                ads = self._apply_text_search(ads, query.text_query)
            
            if query.filters:
                ads = self._apply_filters(ads, query.filters)
            
            if query.tags:
                ads = self._apply_tag_filters(ads, query.tags)
            
            if query.sort_conditions:
                ads = self._apply_sorting(ads, query.sort_conditions)
            
            results = []
            for ad in ads[query.offset:query.offset + query.limit]:
                result = SearchResult(
                    id=ad["id"],
                    name=ad.get("name", ""),
                    type="ad",
                    status=ad.get("status", ""),
                    score=ad.get("_search_score", 1.0),
                    matched_fields=ad.get("_matched_fields", []),
                    metadata=ad,
                )
                results.append(result)
            
            return results
            
        except MetaApiError as exc:
            logger.error("Ad search failed", error=exc.to_dict())
            raise
    
    async def create_tag(
        self,
        ad_account_id: str,
        name: str,
        color: str = "#007bff",
        description: Optional[str] = None,
    ) -> CampaignTag:
        """Create a new campaign tag."""
        try:
            # In a real implementation, this would store tags in a database
            # For now, we'll simulate tag creation
            tag_id = f"tag_{len(self._tag_index) + 1}"
            
            tag = CampaignTag(
                tag_id=tag_id,
                name=name,
                color=color,
                description=description,
            )
            
            # Initialize empty tag index
            self._tag_index[tag_id] = []
            
            logger.info("Campaign tag created", tag_id=tag_id, name=name)
            return tag
            
        except Exception as exc:
            logger.error("Failed to create tag", error=str(exc))
            raise
    
    async def tag_campaign(
        self,
        campaign_id: str,
        tag_id: str,
    ) -> bool:
        """Tag a campaign with a specific tag."""
        try:
            if tag_id not in self._tag_index:
                raise ValueError(f"Tag {tag_id} does not exist")
            
            if campaign_id not in self._tag_index[tag_id]:
                self._tag_index[tag_id].append(campaign_id)
            
            logger.info("Campaign tagged", campaign_id=campaign_id, tag_id=tag_id)
            return True
            
        except Exception as exc:
            logger.error("Failed to tag campaign", error=str(exc))
            raise
    
    async def untag_campaign(
        self,
        campaign_id: str,
        tag_id: str,
    ) -> bool:
        """Remove a tag from a campaign."""
        try:
            if tag_id in self._tag_index and campaign_id in self._tag_index[tag_id]:
                self._tag_index[tag_id].remove(campaign_id)
            
            logger.info("Campaign untagged", campaign_id=campaign_id, tag_id=tag_id)
            return True
            
        except Exception as exc:
            logger.error("Failed to untag campaign", error=str(exc))
            raise
    
    async def get_campaign_tags(
        self,
        campaign_id: str,
    ) -> List[CampaignTag]:
        """Get all tags for a campaign."""
        try:
            tags = []
            for tag_id, campaign_ids in self._tag_index.items():
                if campaign_id in campaign_ids:
                    # In a real implementation, we'd fetch tag details from storage
                    tag = CampaignTag(
                        tag_id=tag_id,
                        name=f"Tag {tag_id}",
                        color="#007bff",
                    )
                    tags.append(tag)
            
            return tags
            
        except Exception as exc:
            logger.error("Failed to get campaign tags", error=str(exc))
            raise
    
    async def save_search_query(
        self,
        name: str,
        query: SearchQuery,
        ad_account_id: str,
    ) -> str:
        """Save a search query for reuse."""
        try:
            # In a real implementation, this would save to a database
            query_id = f"query_{len(self._saved_queries) if hasattr(self, '_saved_queries') else 0 + 1}"
            
            if not hasattr(self, '_saved_queries'):
                self._saved_queries = {}
            
            self._saved_queries[query_id] = {
                "name": name,
                "query": query,
                "ad_account_id": ad_account_id,
            }
            
            logger.info("Search query saved", query_id=query_id, name=name)
            return query_id
            
        except Exception as exc:
            logger.error("Failed to save search query", error=str(exc))
            raise
    
    def _apply_text_search(
        self,
        items: List[Dict[str, Any]],
        text_query: str,
    ) -> List[Dict[str, Any]]:
        """Apply full-text search to items."""
        query_terms = text_query.lower().split()
        results = []
        
        for item in items:
            score = 0.0
            matched_fields = []
            
            # Search in name field (highest weight)
            if "name" in item:
                name_matches = sum(1 for term in query_terms if term in item["name"].lower())
                if name_matches > 0:
                    score += name_matches * 2.0
                    matched_fields.append("name")
            
            # Search in other text fields
            text_fields = ["objective", "status", "configured_status"]
            for field in text_fields:
                if field in item and isinstance(item[field], str):
                    field_matches = sum(1 for term in query_terms if term in item[field].lower())
                    if field_matches > 0:
                        score += field_matches * 1.0
                        matched_fields.append(field)
            
            if score > 0:
                item["_search_score"] = score
                item["_matched_fields"] = matched_fields
                results.append(item)
        
        # Sort by relevance score
        results.sort(key=lambda x: x["_search_score"], reverse=True)
        return results
    
    def _apply_filters(
        self,
        items: List[Dict[str, Any]],
        filters: List[FilterCondition],
    ) -> List[Dict[str, Any]]:
        """Apply filter conditions to items."""
        results = []
        
        for item in items:
            matches = True
            
            for filter_condition in filters:
                field_value = item.get(filter_condition.field)
                
                if not self._evaluate_filter(field_value, filter_condition):
                    matches = False
                    break
            
            if matches:
                results.append(item)
        
        return results
    
    def _evaluate_filter(
        self,
        field_value: Any,
        condition: FilterCondition,
    ) -> bool:
        """Evaluate a single filter condition."""
        if condition.operator == FilterOperator.IS_NULL:
            return field_value is None
        
        if condition.operator == FilterOperator.IS_NOT_NULL:
            return field_value is not None
        
        if field_value is None:
            return False
        
        if condition.operator == FilterOperator.EQUALS:
            return field_value == condition.value
        
        if condition.operator == FilterOperator.NOT_EQUALS:
            return field_value != condition.value
        
        if condition.operator == FilterOperator.CONTAINS:
            if isinstance(field_value, str) and isinstance(condition.value, str):
                if condition.case_sensitive:
                    return condition.value in field_value
                else:
                    return condition.value.lower() in field_value.lower()
            return False
        
        if condition.operator == FilterOperator.NOT_CONTAINS:
            if isinstance(field_value, str) and isinstance(condition.value, str):
                if condition.case_sensitive:
                    return condition.value not in field_value
                else:
                    return condition.value.lower() not in field_value.lower()
            return True
        
        if condition.operator == FilterOperator.STARTS_WITH:
            if isinstance(field_value, str) and isinstance(condition.value, str):
                if condition.case_sensitive:
                    return field_value.startswith(condition.value)
                else:
                    return field_value.lower().startswith(condition.value.lower())
            return False
        
        if condition.operator == FilterOperator.ENDS_WITH:
            if isinstance(field_value, str) and isinstance(condition.value, str):
                if condition.case_sensitive:
                    return field_value.endswith(condition.value)
                else:
                    return field_value.lower().endswith(condition.value.lower())
            return False
        
        if condition.operator == FilterOperator.GREATER_THAN:
            return field_value > condition.value
        
        if condition.operator == FilterOperator.LESS_THAN:
            return field_value < condition.value
        
        if condition.operator == FilterOperator.GREATER_EQUAL:
            return field_value >= condition.value
        
        if condition.operator == FilterOperator.LESS_EQUAL:
            return field_value <= condition.value
        
        if condition.operator == FilterOperator.IN:
            return field_value in condition.value
        
        if condition.operator == FilterOperator.NOT_IN:
            return field_value not in condition.value
        
        if condition.operator == FilterOperator.REGEX:
            if isinstance(field_value, str) and isinstance(condition.value, str):
                try:
                    pattern = re.compile(condition.value, re.IGNORECASE if not condition.case_sensitive else 0)
                    return bool(pattern.search(field_value))
                except re.error:
                    return False
            return False
        
        return False
    
    def _apply_tag_filters(
        self,
        items: List[Dict[str, Any]],
        tags: List[str],
    ) -> List[Dict[str, Any]]:
        """Apply tag filters to items."""
        results = []
        
        for item in items:
            item_id = item["id"]
            item_tags = []
            
            # Get tags for this item
            for tag_id, tagged_items in self._tag_index.items():
                if item_id in tagged_items:
                    item_tags.append(tag_id)
            
            # Check if item has any of the required tags
            if any(tag in item_tags for tag in tags):
                results.append(item)
        
        return results
    
    def _apply_sorting(
        self,
        items: List[Dict[str, Any]],
        sort_conditions: List[SortCondition],
    ) -> List[Dict[str, Any]]:
        """Apply sorting to items."""
        def sort_key(item):
            key_values = []
            for condition in sort_conditions:
                value = item.get(condition.field)
                if value is None:
                    value = "" if condition.direction == SortDirection.ASC else "zzz"
                key_values.append(value)
            return key_values
        
        reverse = any(condition.direction == SortDirection.DESC for condition in sort_conditions)
        return sorted(items, key=sort_key, reverse=reverse)
    
    async def _get_all_campaigns(self, ad_account_id: str) -> List[Dict[str, Any]]:
        """Get all campaigns for an ad account."""
        try:
            response = await self._client.get(
                f"{ad_account_id}/campaigns",
                params={"fields": "id,name,status,objective,configured_status,daily_budget,lifetime_budget"},
            )
            return response.get("data", [])
        except MetaApiError as exc:
            logger.error("Failed to get campaigns", error=exc.to_dict())
            raise
    
    async def _get_all_adsets(self, ad_account_id: str) -> List[Dict[str, Any]]:
        """Get all ad sets for an ad account."""
        try:
            response = await self._client.get(
                f"{ad_account_id}/adsets",
                params={"fields": "id,name,status,campaign_id,daily_budget,lifetime_budget"},
            )
            return response.get("data", [])
        except MetaApiError as exc:
            logger.error("Failed to get ad sets", error=exc.to_dict())
            raise
    
    async def _get_all_ads(self, ad_account_id: str) -> List[Dict[str, Any]]:
        """Get all ads for an ad account."""
        try:
            response = await self._client.get(
                f"{ad_account_id}/ads",
                params={"fields": "id,name,status,adset_id,campaign_id"},
            )
            return response.get("data", [])
        except MetaApiError as exc:
            logger.error("Failed to get ads", error=exc.to_dict())
            raise
