"""
Humanization evaluator for optimizing text to appear more human-written.

This is a composite evaluator that can combine multiple signals:
- GPTZero AI detection probability
- Perplexity (human text has moderate, varied perplexity)
- Burstiness (human text has more variance in sentence patterns)
- Custom scoring functions

Usage:
    from wisent.core.evaluators.custom.examples.humanization import create_humanization_evaluator
    
    evaluator = create_humanization_evaluator(
        gptzero_api_key="your-key",
        weights={"gptzero": 0.6, "perplexity": 0.2, "burstiness": 0.2}
    )
"""

from __future__ import annotations

import logging
import math
from typing import Any, Callable, Dict, List, Optional

from wisent.core.evaluators.custom.custom_evaluator import (
    CustomEvaluator,
    CustomEvaluatorConfig,
)

__all__ = ["HumanizationEvaluator", "create_humanization_evaluator"]

logger = logging.getLogger(__name__)


class HumanizationEvaluator(CustomEvaluator):
    """Composite evaluator for humanization optimization.
    
    Combines multiple signals to score how "human-like" text appears.
    All component scores are normalized to [0, 1] where higher = better.
    
    Args:
        gptzero_api_key: API key for GPTZero (optional)
        weights: Dict of component weights, e.g. {"gptzero": 0.6, "perplexity": 0.4}
        custom_scorer: Optional custom scoring function(response) -> float
        perplexity_model: Model for perplexity calculation (optional)
    """
    
    DEFAULT_WEIGHTS = {
        "gptzero": 0.5,
        "perplexity": 0.25,
        "burstiness": 0.15,
        "length_penalty": 0.1,
    }
    
    def __init__(
        self,
        gptzero_api_key: Optional[str] = None,
        weights: Optional[Dict[str, float]] = None,
        custom_scorer: Optional[Callable[[str], float]] = None,
        perplexity_model: Optional[Any] = None,
        target_perplexity_range: tuple = (20.0, 80.0),
        target_length_range: tuple = (100, 500),
    ):
        config = CustomEvaluatorConfig(
            name="humanization",
            description="Composite humanization evaluator (higher = more human-like)",
        )
        super().__init__(name="humanization", description=config.description, config=config)
        
        self.weights = weights or self.DEFAULT_WEIGHTS.copy()
        self.custom_scorer = custom_scorer
        self.perplexity_model = perplexity_model
        self.target_perplexity_range = target_perplexity_range
        self.target_length_range = target_length_range
        
        self._gptzero = None
        if gptzero_api_key:
            from wisent.core.evaluators.custom.examples.gptzero import GPTZeroEvaluator
            self._gptzero = GPTZeroEvaluator(api_key=gptzero_api_key)
        
        total_weight = sum(self.weights.values())
        if total_weight > 0:
            self.weights = {k: v / total_weight for k, v in self.weights.items()}
    
    def evaluate_response(self, response: str, **kwargs) -> Dict[str, Any]:
        """Evaluate response on multiple humanization metrics."""
        scores = {}
        details = {}
        
        if self._gptzero and self.weights.get("gptzero", 0) > 0:
            try:
                gptzero_result = self._gptzero(response)
                scores["gptzero"] = gptzero_result["score"]
                details["gptzero"] = gptzero_result
            except Exception as e:
                logger.warning(f"GPTZero evaluation failed: {e}")
                scores["gptzero"] = 0.5
        
        if self.weights.get("perplexity", 0) > 0:
            scores["perplexity"] = self._score_perplexity(response)
        
        if self.weights.get("burstiness", 0) > 0:
            scores["burstiness"] = self._score_burstiness(response)
            details["burstiness_raw"] = scores["burstiness"]
        
        if self.weights.get("length_penalty", 0) > 0:
            scores["length_penalty"] = self._score_length(response)
        
        if self.custom_scorer and "custom" in self.weights:
            try:
                scores["custom"] = float(self.custom_scorer(response))
            except Exception as e:
                logger.warning(f"Custom scorer failed: {e}")
        
        final_score = sum(
            scores.get(k, 0.5) * w 
            for k, w in self.weights.items()
            if k in scores or w == 0
        )
        
        return {
            "score": final_score,
            "component_scores": scores,
            "weights": self.weights,
            "details": details,
        }
    
    def _score_perplexity(self, response: str) -> float:
        """Score based on perplexity (moderate is good, extreme is bad)."""
        if self.perplexity_model is None:
            return 0.5
        
        try:
            import torch
            inputs = self.perplexity_model.tokenizer(response, return_tensors="pt")
            with torch.no_grad():
                outputs = self.perplexity_model.model(**inputs, labels=inputs["input_ids"])
                perplexity = math.exp(outputs.loss.item())
        except Exception:
            return 0.5
        
        min_ppl, max_ppl = self.target_perplexity_range
        center = (min_ppl + max_ppl) / 2
        width = (max_ppl - min_ppl) / 2
        
        distance = abs(perplexity - center) / width
        score = math.exp(-distance ** 2 / 2)
        return score
    
    def _score_burstiness(self, response: str) -> float:
        """Score based on sentence length variance (human text has more variance)."""
        import re
        sentences = re.split(r'[.!?]+', response)
        sentences = [s.strip() for s in sentences if s.strip()]
        
        if len(sentences) < 2:
            return 0.5
        
        lengths = [len(s.split()) for s in sentences]
        mean_len = sum(lengths) / len(lengths)
        variance = sum((l - mean_len) ** 2 for l in lengths) / len(lengths)
        std_dev = math.sqrt(variance) if variance > 0 else 0
        
        coefficient_of_variation = std_dev / mean_len if mean_len > 0 else 0
        
        score = min(1.0, coefficient_of_variation / 0.5)
        return score
    
    def _score_length(self, response: str) -> float:
        """Score based on response length (penalize too short or too long)."""
        length = len(response)
        min_len, max_len = self.target_length_range
        
        if min_len <= length <= max_len:
            return 1.0
        elif length < min_len:
            return max(0.0, length / min_len)
        else:
            return max(0.0, 1.0 - (length - max_len) / max_len)


def create_humanization_evaluator(
    gptzero_api_key: Optional[str] = None,
    weights: Optional[Dict[str, float]] = None,
    **kwargs,
) -> HumanizationEvaluator:
    """Create a humanization evaluator.
    
    Args:
        gptzero_api_key: Optional GPTZero API key for AI detection
        weights: Component weights, e.g. {"gptzero": 0.6, "burstiness": 0.4}
        **kwargs: Additional arguments to HumanizationEvaluator
    
    Returns:
        HumanizationEvaluator instance
    
    Example:
        # Simple usage with just GPTZero
        evaluator = create_humanization_evaluator(gptzero_api_key="your-key")
        
        # Custom weights
        evaluator = create_humanization_evaluator(
            gptzero_api_key="your-key",
            weights={"gptzero": 0.7, "burstiness": 0.3}
        )
    """
    return HumanizationEvaluator(
        gptzero_api_key=gptzero_api_key,
        weights=weights,
        **kwargs,
    )


def create_evaluator(**kwargs) -> HumanizationEvaluator:
    """Factory function for module-based loading."""
    return create_humanization_evaluator(**kwargs)
