import time
from typing import Optional, List
import requests
from requests.exceptions import RequestException

from llm_redact.config import get_settings
from llm_redact.db.database import get_db_manager
from llm_redact.db.models import RedactionResult, RedactionRule, Replacement


def prepare_rules_and_group(
    rules: Optional[List[RedactionRule]], 
    rule_group_id: Optional[str]
) -> tuple[List[RedactionRule], Optional[str]]:
    """
    Prepare rules and determine the rule group ID to use.
    
    Returns:
        Tuple of (rule_objects, used_rule_group_id)
    """
    rule_objects = []
    used_rule_group_id = None
    
    # Priority: rules list > rule_group_id > default
    if rules:
        # Use provided rules directly and ensure they exist in database
        rule_objects = rules
        used_rule_group_id = create_rules_and_temp_group(rules)
    elif rule_group_id:
        # Get rules from existing rule group
        rule_objects, used_rule_group_id = get_rules_from_group(rule_group_id)
    
    return rule_objects, used_rule_group_id


def create_rules_and_temp_group(rules: List[RedactionRule]) -> Optional[str]:
    """
    Create rules in database and return a temporary rule group ID.
    
    Returns:
        Rule group ID or None if creation failed
    """
    from llm_redact.db.manager import insert_rule
    from llm_redact.db.manager import create_rule_group
    
    rule_ids = []
    for rule in rules:
        try:
            # Try to create the rule
            created_rule = insert_rule(
                rule.name, rule.description, rule.data_type
            )
            rule_ids.append(created_rule.id)
        except ValueError:
            # Rule already exists, calculate its ID
            import hashlib
            rule_content = f"{rule.name}|{rule.description}|{rule.data_type}"
            rule_hash = hashlib.sha256(
                rule_content.encode()
            ).hexdigest()
            rule_id = f"rule_{rule_hash[:8]}"
            rule_ids.append(rule_id)
    
    # Create a temporary rule group for these rules
    try:
        import hashlib
        
        # Create unique group name based on rules and timestamp
        rules_signature = "|".join([
            f"{r.name}:{r.description}" for r in rules
        ])
        timestamp = str(int(time.time()))
        group_signature = f"temp_group_{timestamp}_{rules_signature}"
        group_hash = hashlib.sha256(
            group_signature.encode()
        ).hexdigest()
        
        temp_group = create_rule_group(
            name=f"Temporary rule group {group_hash[:8]}",
            description=(
                "Automatically created group for provided rules"
            ),
            rule_ids=rule_ids
        )
        return temp_group.id
        
    except ValueError:
        # Group creation failed, continue without rule group ID
        return None


def get_rules_from_group(
    rule_group_id: str
) -> tuple[List[RedactionRule], str]:
    """
    Get rules from an existing rule group.
    
    Returns:
        Tuple of (rule_objects, rule_group_id)
    """
    from llm_redact.db.manager import get_rules_in_group
    rule_records = get_rules_in_group(rule_group_id)
    
    rule_objects = [
        RedactionRule(
            name=rule.name, 
            description=rule.description, 
            data_type=rule.data_type
        )
        for rule in rule_records
    ]
    return rule_objects, rule_group_id


def prepare_llm_payload(
    text: str, 
    model: Optional[str], 
    rule_objects: List[RedactionRule]
) -> dict:
    """
    Prepare the payload for the LLM host request.
    """
    payload = {"text": text}
    
    if model:
        payload["model"] = model
    
    # Add rules to payload if we have any
    if rule_objects:
        payload["rules"] = [
            {
                "name": rule.name, 
                "description": rule.description,
                "data_type": rule.data_type
            }
            for rule in rule_objects
        ]
    
    return payload


def process_llm_response(data: dict) -> tuple[List[Replacement], str]:
    """
    Process LLM response and create proper placeholders with IDs.
    
    Returns:
        Tuple of (replacements, redacted_text)
    """
    import re
    from llm_redact.db.database import create_placeholder_with_id
    
    def extract_clean_data_type(placeholder: str) -> str:
        """Extract data type from clean placeholder format (e.g., |_NAME_|)"""
        match = re.match(r'^\|_([A-Z_]+)_\|$', placeholder)
        return match.group(1) if match else None
    
    replacements = []
    redacted_text = data["redacted_text"]
    
    for r in data.get("replacements", []):
        original_text = r["original_text"]
        llm_replacement = r["replacement_text"]
        
        # Extract data type from LLM response (e.g., |_NAME_|)
        data_type = extract_clean_data_type(llm_replacement)
        if data_type:
            # Create proper placeholder with real hash
            proper_placeholder = create_placeholder_with_id(
                original_text, data_type
            )
            
            # Replace in redacted text
            redacted_text = redacted_text.replace(
                llm_replacement, proper_placeholder
            )
            
            replacements.append(Replacement(
                original_text=original_text,
                replacement_text=proper_placeholder
            ))
        else:
            # Fallback for unexpected format
            replacements.append(Replacement(
                original_text=original_text,
                replacement_text=llm_replacement
            ))
    
    return replacements, redacted_text


def ensure_default_rule_group() -> str:
    """
    Ensure default rule group exists and return its ID.
    Loads PII rules from YAML configuration.
    """
    import os
    import yaml
    from llm_redact.db.manager import insert_rule
    from llm_redact.db.manager import create_rule_group
    
    try:
        # Load default rules from YAML
        current_dir = os.path.dirname(__file__)
        yaml_path = os.path.join(
            current_dir, "..", "..", "default_data", "default_rule_group.yaml"
        )
        
        with open(yaml_path, 'r', encoding='utf-8') as file:
            config = yaml.safe_load(file)
        
        # Create all default rules and collect their IDs
        rule_ids = []
        for rule_config in config['rules']:
            try:
                created_rule = insert_rule(
                    rule_config['name'], 
                    rule_config['description'],
                    rule_config['data_type']
                )
                rule_ids.append(created_rule.id)
            except ValueError:
                # Rule already exists, calculate its ID
                import hashlib
                rule_content = (
                    f"{rule_config['name']}|{rule_config['description']}|"
                    f"{rule_config['data_type']}"
                )
                rule_hash = hashlib.sha256(rule_content.encode()).hexdigest()
                rule_ids.append(f"rule_{rule_hash[:8]}")
        
        # Create default rule group with all rules
        default_group = create_rule_group(
            name=config['name'],
            description=config['description'],
            rule_ids=rule_ids
        )
        return default_group.id
        
    except ValueError:
        # Default group already exists, calculate the expected ID
        import hashlib
        
        group_content = (
            f"{config['name']}|{config['description']}|{len(config['rules'])}"
        )
        group_hash = hashlib.sha256(group_content.encode()).hexdigest()
        return f"group_{group_hash[:8]}"


def save_failed_result(
    text: str,
    start_time: float, 
    used_rule_group_id: Optional[str],
    final_rule_group_id: Optional[str],
    status: str = "failed"
) -> RedactionResult:
    """
    Create and save a failed redaction result.
    """
    db_manager = get_db_manager()
    
    processing_time = int((time.time() - start_time) * 1000)
    result = RedactionResult(
        original_text=text,
        redacted_text=text,  # Return original on failure
        replacements=[],
        is_redacted=False,
        processing_time_ms=processing_time,
        rule_group_id=used_rule_group_id,
        cached=False
    )
    
    db_manager.save_redaction_result(
        result, 
        rule_group_id=final_rule_group_id or "group_default", 
        status=status
    )
    
    return result


def mask_text(
    text: str,
    rule_group_id: Optional[str] = None,
    rules: Optional[List[RedactionRule]] = None,
    model: Optional[str] = None,
    llm_host_url: Optional[str] = None
) -> RedactionResult:
    """
    Redact sensitive information from text.
    
    Args:
        text: The text to redact
        rule_group_id: Optional rule group ID to use for redaction
        rules: Optional list of RedactionRule objects
        model: Optional LLM model to use
        llm_host_url: Optional custom LLM host URL
        
    Note:
        If both rule_group_id and rules are provided, 
        rules takes precedence.
        If neither is provided, uses default rule group.
        
    Returns:
        RedactionResult with redacted text and metadata
    """
    settings = get_settings()
    db_manager = get_db_manager()
    host_url = llm_host_url or settings.llm_host_url
    
    start_time = time.time()
    final_rule_group_id = None
    used_rule_group_id = None  # Initialize early to avoid scope issues
    
    try:
        # Prepare rules and determine rule group first
        rule_objects, used_rule_group_id = prepare_rules_and_group(
            rules, rule_group_id
        )
        
        # Determine final rule group ID for caching
        cache_rule_group_id = used_rule_group_id
        if not cache_rule_group_id:
            cache_rule_group_id = ensure_default_rule_group()
        
        # Check for cached result with proper rule group
        cached_result = db_manager.get_cached_result(text, cache_rule_group_id)
        if cached_result:
            return cached_result
        
        # Prepare LLM request payload
        payload = prepare_llm_payload(text, model, rule_objects)
        
        # Make request to LLM host
        response = requests.post(
            f"{host_url}/redact",
            json=payload,
            headers={"Content-Type": "application/json"},
            timeout=30
        )
        
        if response.status_code != 200:
            raise Exception(f"LLM host error: {response.text}")
        
        data = response.json()
        
        # Process LLM response
        replacements, redacted_text = process_llm_response(data)
        
        processing_time = int((time.time() - start_time) * 1000)
        
        result = RedactionResult(
            original_text=text,
            redacted_text=redacted_text,
            replacements=replacements,
            is_redacted=len(replacements) > 0,
            processing_time_ms=processing_time,
            rule_group_id=used_rule_group_id,
            cached=False
        )
        
        # Save to database with appropriate rule group ID
        final_rule_group_id = used_rule_group_id
        if not rules and not rule_group_id:
            # Ensure default rule group exists when no rules specified
            final_rule_group_id = ensure_default_rule_group()
        
        db_manager.save_redaction_result(
            result, rule_group_id=final_rule_group_id
        )
        
        return result
        
    except RequestException as e:
        save_failed_result(
            text, start_time, used_rule_group_id, final_rule_group_id
        )
        raise Exception(f"Failed to connect to LLM host: {str(e)}")
    
    except Exception as e:
        save_failed_result(
            text, start_time, used_rule_group_id, final_rule_group_id
        )
        raise Exception(f"Redaction failed: {str(e)}") 