"""
CutySoup Query Language (CSQL) - Advanced JSON-based query system
for complex, structured data extraction from HTML documents.

Phase 1: Core Engine - Basic query execution with selectors and text extraction
"""

import json
import re
from typing import Dict, List, Union, Optional, Any
from .core import Tag, CutySoup, NavigableString
from .css_selectors import select_tags, select_one_tag
from .exceptions import CSQLError, NavigationError

_SIMPLE_TAG_SELECTOR = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]*$')
_SIMPLE_CLASS_SELECTOR = re.compile(r'^\.[a-zA-Z_][a-zA-Z0-9_-]*$')
_SIMPLE_ID_SELECTOR = re.compile(r'^#[a-zA-Z_][a-zA-Z0-9_-]*$')
_FAST_MISS = object()


class CSQLEngine:
    """Core CSQL query execution engine."""
    
    def __init__(self, soup: CutySoup):
        """Initialize CSQL engine with CutySoup instance."""
        self.soup = soup
        self.context_stack = []  # Track current context for nested queries

    def _get_simple_selector(self, selector: str):
        """Classify selectors that can bypass the CSS parser."""
        if _SIMPLE_TAG_SELECTOR.match(selector):
            return ("tag", selector)
        if _SIMPLE_CLASS_SELECTOR.match(selector):
            return ("class", selector[1:])
        if _SIMPLE_ID_SELECTOR.match(selector):
            return ("id", selector[1:])
        return (None, None)

    def _select_context_elements(self, context, selector: str):
        """Select elements with the cheapest available strategy for simple selectors."""
        selector_kind, selector_value = self._get_simple_selector(selector)
        if selector_kind is None:
            return None

        if isinstance(context, CutySoup):
            root = context._get_root_tag()
            if not root:
                return []

            if selector_kind == "tag":
                if root._node and hasattr(root._node, "find_all_bulk"):
                    return [
                        Tag._create_from_tree_node(node, context._tree)
                        for node in root._node.find_all_bulk(selector_value, {})
                    ]
                return context.find_all(selector_value)

            if selector_kind == "class":
                if root._node and hasattr(root._node, "find_all_bulk"):
                    return [
                        Tag._create_from_tree_node(node, context._tree)
                        for node in root._node.find_all_bulk("", {"class": selector_value})
                    ]
                return context.find_all(class_=selector_value)

            if selector_kind == "id":
                if root._node and hasattr(root._node, "find"):
                    node = root._node.find("", {"id": selector_value})
                    return [Tag._create_from_tree_node(node, context._tree)] if node else []
                result = context.find(id=selector_value)
                return [result] if result else []

            return None

        if selector_kind == "tag":
            return context.find_all(selector_value)
        if selector_kind == "class":
            return context.find_all(class_=selector_value)
        if selector_kind == "id":
            result = context.find(id=selector_value)
            return [result] if result else []
        return None

    def _try_fast_field(self, field_spec: Dict, context) -> Any:
        """Use bulk and zero-context operations for simple top-level extraction."""
        if "fields" in field_spec:
            return _FAST_MISS

        selector = field_spec.get("selector", "").strip()
        if not selector:
            return _FAST_MISS

        selector_kind, selector_value = self._get_simple_selector(selector)
        if selector_kind is None:
            return _FAST_MISS

        extract_type = field_spec.get("extract", "text")
        list_type = field_spec.get("type") == "list"
        limit = field_spec.get("limit")
        values = None

        if isinstance(context, CutySoup) and extract_type == "text":
            if selector_kind == "tag":
                values = [
                    self._apply_transformations(text.strip(), field_spec)
                    for text in context.get_all_text_by_tag(selector_value)
                    if text and text.strip()
                ]
            elif selector_kind == "class":
                values = [
                    self._apply_transformations(text.strip(), field_spec)
                    for text in context.get_all_text_by_class(selector_value)
                    if text and text.strip()
                ]

        if isinstance(context, CutySoup) and extract_type == "attribute":
            attr_name = field_spec.get("attribute")
            if not attr_name:
                raise CSQLError("'attribute' key required for attribute extraction")

            if selector_kind == "tag" and selector_value == "a" and attr_name == "href":
                values = [
                    self._apply_transformations(value, field_spec)
                    for value in context.get_all_hrefs()
                    if value
                ]
            elif selector_kind == "tag" and selector_value == "img" and attr_name == "src":
                values = [
                    self._apply_transformations(value, field_spec)
                    for value in context.get_all_src_attributes()
                    if value
                ]

        if values is None:
            elements = self._select_context_elements(context, selector)
            if elements is None:
                return _FAST_MISS

            values = []
            for element in elements:
                extracted = self._extract_from_element(element, extract_type, field_spec)
                if extracted is not None:
                    values.append(extracted)

        if limit:
            values = values[:limit]

        if list_type:
            return values

        if not values:
            return field_spec.get("default")

        return values[0]
        
    def execute_query(self, query_plan: Union[str, Dict]) -> Dict[str, Any]:
        """
        Execute a CSQL query plan and return structured results.
        
        Args:
            query_plan: JSON string or dict containing the query plan
            
        Returns:
            Dict containing the extracted data according to the query plan
        """
        try:
            # Parse JSON if string provided
            if isinstance(query_plan, str):
                plan = json.loads(query_plan)
            else:
                plan = query_plan
                
            if not isinstance(plan, dict):
                raise CSQLError("Query plan must be a JSON object")
                
            # Execute the main query
            return self._execute_plan(plan, self.soup)
            
        except json.JSONDecodeError as e:
            raise CSQLError(f"Invalid JSON in query plan: {e}")
        except Exception as e:
            raise CSQLError(f"Query execution failed: {e}")
    
    def _execute_plan(self, plan: Dict, context) -> Dict[str, Any]:
        """Execute a query plan in the given context."""
        results = {}
        
        for key, value in plan.items():
            if key == "variables":
                # Handle variables (Phase 6 feature - placeholder)
                continue
                
            # Execute field query
            field_result = self._execute_field(value, context, is_nested=False)
            results[key] = field_result
            
        return results
    
    def _execute_nested_plan(self, plan: Dict, context) -> Dict[str, Any]:
        """Execute a nested query plan where fields don't need selectors."""
        results = {}
        
        for key, value in plan.items():
            if key == "variables":
                # Handle variables (Phase 6 feature - placeholder)
                continue
                
            # Execute field query with is_nested=True
            field_result = self._execute_field(value, context, is_nested=True)
            results[key] = field_result
            
        return results
    
    def _execute_field(self, field_spec, context, is_nested=False) -> Any:
        """Execute a single field specification."""
        if isinstance(field_spec, str):
            # Simple string selector with implicit text extraction
            return self._extract_simple_text(field_spec, context)
        elif isinstance(field_spec, dict):
            # Validate that top-level complex fields have required keys
            if not is_nested and "extract" in field_spec and "selector" not in field_spec and "fields" not in field_spec:
                raise CSQLError("Complex field specifications must have 'selector' key")
            # Complex field specification
            return self._execute_complex_field(field_spec, context)
        else:
            raise CSQLError(f"Invalid field specification: {field_spec}")
    
    def _extract_simple_text(self, selector: str, context) -> Union[str, List[str]]:
        """Extract text using simple selector."""
        try:
            elements = self._select_context_elements(context, selector)
            if elements is None:
                if hasattr(context, 'select'):
                    elements = context.select(selector)
                else:
                    elements = self.soup.select(selector)
                
            if not elements:
                return None
                
            # Return single text if one element, list if multiple
            texts = [elem.get_text().strip() for elem in elements if elem.get_text().strip()]
            
            if len(texts) == 0:
                return None
            elif len(texts) == 1:
                return texts[0]
            else:
                return texts
                
        except Exception as e:
            raise CSQLError(f"Failed to extract text with selector '{selector}': {e}")
    
    def _execute_complex_field(self, field_spec: Dict, context) -> Any:
        """Execute complex field specification with full options."""
        # Validation is handled at a higher level - nested fields within "fields" 
        # objects don't need selectors as they operate on the parent context

        fast_result = self._try_fast_field(field_spec, context)
        if fast_result is not _FAST_MISS:
            return fast_result
            
        selector = field_spec.get("selector", "")
        
        # Handle empty selector as "current context"
        if selector == "":
            elements = [context]  # Use current context element
        else:
            # Find elements using selector
            try:
                if hasattr(context, 'select'):
                    elements = context.select(selector)
                else:
                    elements = self.soup.select(selector)
            except Exception as e:
                raise CSQLError(f"Selector '{selector}' failed: {e}")
            
        if not elements:
            # Return default value if no elements found
            return field_spec.get("default", None)
        
        # Handle different extraction types
        extract_type = field_spec.get("extract", "text")
        list_type = field_spec.get("type") == "list"
        
        if list_type:
            # Process multiple elements
            results = []
            limit = field_spec.get("limit")
            
            for i, element in enumerate(elements):
                if limit and i >= limit:
                    break
                    
                if "fields" in field_spec:
                    # Nested object extraction
                    nested_result = self._execute_nested_plan(field_spec["fields"], element)
                    results.append(nested_result)
                else:
                    # Simple extraction
                    extracted = self._extract_from_element(element, extract_type, field_spec)
                    if extracted is not None:
                        results.append(extracted)
            
            return results
        else:
            # Process single element (first match)
            element = elements[0]
            
            if "fields" in field_spec:
                # Nested object extraction
                return self._execute_nested_plan(field_spec["fields"], element)
            else:
                # Simple extraction
                extracted = self._extract_from_element(element, extract_type, field_spec)
                return extracted if extracted is not None else field_spec.get("default")
    
    def _extract_from_element(self, element: Tag, extract_type: str, field_spec: Dict) -> Any:
        """Extract data from a single element."""
        try:
            if extract_type == "text":
                text = element.get_text().strip()
                return self._apply_transformations(text, field_spec)
                
            elif extract_type == "attribute":
                attr_name = field_spec.get("attribute")
                if not attr_name:
                    raise CSQLError("'attribute' key required for attribute extraction")
                
                attr_value = element.get(attr_name)
                if attr_value is None:
                    return field_spec.get("default")
                
                # Handle class attribute (list) specially
                if attr_name == "class" and isinstance(attr_value, list):
                    attr_value = " ".join(attr_value)
                
                return self._apply_transformations(str(attr_value), field_spec)
                
            elif extract_type == "html":
                return str(element)
                
            elif extract_type == "element":
                # Return the element itself (for advanced use cases)
                return element
                
            else:
                raise CSQLError(f"Unknown extraction type: {extract_type}")
                
        except Exception as e:
            raise CSQLError(f"Failed to extract from element: {e}")
    
    def _apply_transformations(self, value: str, field_spec: Dict) -> Any:
        """Apply transformations to extracted value (Phase 3 feature)."""
        # Basic implementation - will be expanded in Phase 3
        transforms = field_spec.get("transform", [])
        if not transforms:
            # Check for cast operation
            cast_type = field_spec.get("cast")
            if cast_type:
                return self._cast_value(value, cast_type)
            return value
        
        # Apply transformations in sequence
        result = value
        for transform in transforms:
            result = self._apply_single_transform(result, transform)
        
        # Apply cast after transformations
        cast_type = field_spec.get("cast")
        if cast_type:
            result = self._cast_value(result, cast_type)
            
        return result
    
    def _apply_single_transform(self, value: str, transform: Dict) -> str:
        """Apply a single transformation (Phase 3 implementation)."""
        if "regex" in transform:
            pattern = transform["regex"]
            
            # Special handling for common number extraction patterns
            if pattern == r"[\d.]+":
                # Expand to include commas for better number extraction
                pattern = r"[\d.,]+"
            
            match = re.search(pattern, value)
            return match.group(1) if match and match.groups() else match.group(0) if match else value
            
        elif "replace" in transform:
            replace_spec = transform["replace"]
            if isinstance(replace_spec, dict):
                for old, new in replace_spec.items():
                    value = value.replace(old, new)
            return value
            
        elif "strip" in transform:
            return value.strip()
            
        elif "upper" in transform:
            return value.upper()
            
        elif "lower" in transform:
            return value.lower()
            
        elif "slice" in transform:
            slice_spec = transform["slice"]
            if isinstance(slice_spec, list) and len(slice_spec) >= 2:
                return value[slice_spec[0]:slice_spec[1]]
            
        # More transformations will be added in Phase 3
        return value
    
    def _cast_value(self, value: str, cast_type: str) -> Any:
        """Cast value to specified type."""
        try:
            if cast_type == "integer":
                # Remove common formatting characters (commas, currency symbols, etc.)
                cleaned = re.sub(r'[^\d.-]', '', str(value))
                return int(float(cleaned)) if cleaned else 0
                
            elif cast_type == "float":
                # Remove common formatting characters except decimal point
                # This handles cases like "$1,299.99" -> "1299.99"
                cleaned = re.sub(r'[^\d.-]', '', str(value))
                return float(cleaned) if cleaned else 0.0
                
            elif cast_type == "boolean":
                return str(value).lower() in ('true', '1', 'yes', 'on')
                
            elif cast_type == "string":
                return str(value)
                
            else:
                raise CSQLError(f"Unknown cast type: {cast_type}")
                
        except (ValueError, TypeError):
            # Return default value on cast failure
            return value


class CSQLQuery:
    """
    High-level CSQL query builder and executor.
    Provides convenient methods for building common query patterns.
    """
    
    def __init__(self, soup: CutySoup):
        """Initialize with CutySoup instance."""
        self.soup = soup
        self.engine = CSQLEngine(soup)
    
    @classmethod
    def from_html(cls, html: str) -> 'CSQLQuery':
        """Create CSQL query from HTML string."""
        soup = CutySoup(html)
        return cls(soup)
    
    def execute(self, query_plan: Union[str, Dict]) -> Dict[str, Any]:
        """Execute query plan."""
        return self.engine.execute_query(query_plan)
    
    def extract_list(self, selector: str, fields: Dict[str, str]) -> List[Dict]:
        """
        Convenience method for extracting structured lists.
        
        Args:
            selector: CSS selector for list items
            fields: Dict mapping field names to selectors
        
        Returns:
            List of dictionaries with extracted data
        """
        plan = {
            "items": {
                "selector": selector,
                "type": "list",
                "fields": fields
            }
        }
        result = self.execute(plan)
        return result.get("items", [])
    
    def extract_single(self, selector: str, fields: Dict[str, str]) -> Dict:
        """
        Convenience method for extracting single structured object.
        
        Args:
            selector: CSS selector for the element
            fields: Dict mapping field names to selectors or extraction specs
        
        Returns:
            Dictionary with extracted data
        """
        plan = {
            "item": {
                "selector": selector,
                "fields": fields
            }
        }
        result = self.execute(plan)
        return result.get("item", {})
    
    def extract_text_list(self, selector: str) -> List[str]:
        """
        Convenience method for extracting simple text lists.
        
        Args:
            selector: CSS selector for elements
        
        Returns:
            List of text strings
        """
        plan = {
            "texts": {
                "selector": selector,
                "type": "list",
                "extract": "text"
            }
        }
        result = self.execute(plan)
        return result.get("texts", [])
    
    def extract_attributes(self, selector: str, attribute: str) -> List[str]:
        """
        Convenience method for extracting attribute values.
        
        Args:
            selector: CSS selector for elements
            attribute: Attribute name to extract
        
        Returns:
            List of attribute values
        """
        plan = {
            "attributes": {
                "selector": selector,
                "type": "list",
                "extract": "attribute",
                "attribute": attribute
            }
        }
        result = self.execute(plan)
        return result.get("attributes", [])
