"""
Emerged Personas - Extract persona patterns from successful investigation branches.

When an investigation branch successfully converges, extract:
- Initial epistemic vector state
- Delta pattern over loops (how knowledge evolved)
- Convergence thresholds that worked
- Task characteristics that led to success

This creates data-driven personas that can inform future Sentinel orchestration.
"""

from __future__ import annotations

import json
import logging
import os
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Dict, List, Optional, Any, TYPE_CHECKING
from pathlib import Path

if TYPE_CHECKING:
    from empirica.core.sentinel.orchestrator import EpistemicLoopTracker

logger = logging.getLogger(__name__)


@dataclass
class EmergedPersona:
    """A persona derived from successful investigation patterns."""
    persona_id: str
    name: str
    source_session_id: str
    source_branch_id: Optional[str] = None

    # Vector profile
    initial_vectors: Dict[str, float] = field(default_factory=dict)
    final_vectors: Dict[str, float] = field(default_factory=dict)
    delta_pattern: Dict[str, float] = field(default_factory=dict)

    # Convergence characteristics
    loops_to_converge: int = 0
    convergence_threshold: float = 0.03
    scope_breadth: float = 0.5
    scope_duration: float = 0.5

    # Task characteristics
    task_domains: List[str] = field(default_factory=list)
    task_keywords: List[str] = field(default_factory=list)

    # Provenance
    extracted_at: str = field(default_factory=lambda: datetime.now().isoformat())
    findings_count: int = 0
    unknowns_resolved: int = 0

    # Reputation (can be updated over time)
    reputation_score: float = 0.5
    uses_count: int = 0
    success_count: int = 0

    def to_dict(self) -> Dict[str, Any]:
        """Convert persona to dictionary representation."""
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'EmergedPersona':
        """Create persona from dictionary representation."""
        return cls(**data)

    def to_yaml(self) -> str:
        """Export as YAML for storage."""
        import yaml
        return yaml.dump(self.to_dict(), default_flow_style=False, sort_keys=False)


def extract_persona_from_loop_tracker(
    session_id: str,
    loop_tracker: 'EpistemicLoopTracker',
    task_description: str = "",
    branch_id: str = None
) -> Optional[EmergedPersona]:
    """
    Extract an emerged persona from a successful loop tracker.

    Call this after a successful investigation branch completion
    (when loop_tracker.is_converged() or all loops completed successfully).

    Args:
        session_id: The session where this persona emerged
        loop_tracker: The EpistemicLoopTracker with completed loop history
        task_description: Original task for domain extraction
        branch_id: Optional branch ID for provenance

    Returns:
        EmergedPersona if extraction successful, None otherwise
    """
    if not loop_tracker.loop_history:
        logger.debug("No loop history to extract persona from")
        return None

    # Get initial and final states
    first_loop = loop_tracker.loop_history[0]
    last_loop = loop_tracker.loop_history[-1]

    initial_vectors = first_loop.preflight_vectors or {}
    final_vectors = last_loop.postflight_vectors or {}

    # Calculate delta pattern (how each vector evolved)
    delta_pattern = {}
    for key in set(initial_vectors.keys()) | set(final_vectors.keys()):
        initial = initial_vectors.get(key, 0.5)
        final = final_vectors.get(key, 0.5)
        delta_pattern[key] = final - initial

    # Calculate total findings and unknowns resolved
    total_findings = sum(loop.findings_count or 0 for loop in loop_tracker.loop_history)
    total_unknowns_resolved = sum(
        (loop.unknowns_start or 0) - (loop.unknowns_count or 0)
        for loop in loop_tracker.loop_history
        if loop.unknowns_start is not None and loop.unknowns_count is not None
    )

    # Extract domains from task description
    task_domains = _extract_domains(task_description)
    task_keywords = _extract_keywords(task_description)

    # Generate persona name
    primary_domain = task_domains[0] if task_domains else "general"
    persona_name = f"{primary_domain.title()} Investigator ({len(loop_tracker.loop_history)} loops)"

    persona = EmergedPersona(
        persona_id=f"emerged_{str(uuid.uuid4())[:8]}",
        name=persona_name,
        source_session_id=session_id,
        source_branch_id=branch_id,
        initial_vectors=initial_vectors,
        final_vectors=final_vectors,
        delta_pattern=delta_pattern,
        loops_to_converge=len(loop_tracker.loop_history),
        convergence_threshold=loop_tracker.convergence_threshold,
        scope_breadth=loop_tracker.scope_breadth,
        scope_duration=loop_tracker.scope_duration,
        task_domains=task_domains,
        task_keywords=task_keywords,
        findings_count=total_findings,
        unknowns_resolved=max(0, total_unknowns_resolved),
        reputation_score=0.5 + (0.1 * min(total_findings, 5))  # Initial boost from findings
    )

    return persona


def _extract_domains(task: str) -> List[str]:
    """Extract domain signals from task description."""
    from empirica.core.sentinel.decision_logic import DOMAIN_PATTERNS
    import re

    task_lower = task.lower()
    domains = []

    for domain, patterns in DOMAIN_PATTERNS.items():
        for pattern in patterns:
            if re.search(pattern, task_lower):
                if domain not in domains:
                    domains.append(domain)
                break

    return domains or ["general"]


def _extract_keywords(task: str) -> List[str]:
    """Extract significant keywords from task description."""
    # Simple keyword extraction - could be enhanced with NLP
    import re

    # Remove common words
    stop_words = {
        'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
        'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
        'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
        'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'this',
        'that', 'these', 'those', 'it', 'its', 'i', 'we', 'you', 'he', 'she',
        'they', 'what', 'which', 'who', 'when', 'where', 'why', 'how'
    }

    words = re.findall(r'\b[a-z]{3,}\b', task.lower())
    keywords = [w for w in words if w not in stop_words]

    # Return unique keywords, limited to 10
    seen = set()
    unique = []
    for kw in keywords:
        if kw not in seen:
            seen.add(kw)
            unique.append(kw)
            if len(unique) >= 10:
                break

    return unique


class EmergedPersonaStore:
    """
    Store and retrieve emerged personas.

    Storage location: .empirica/personas/
    Format: emerged_{persona_id}.yaml
    """

    def __init__(self, base_path: str = None):
        """Initialize persona store with optional custom base path."""
        if base_path:
            self.base_path = Path(base_path)
        else:
            self.base_path = Path.cwd() / ".empirica" / "personas"
        self.base_path.mkdir(parents=True, exist_ok=True)

    def save(self, persona: EmergedPersona) -> str:
        """Save persona to storage. Returns file path."""
        filename = f"emerged_{persona.persona_id}.yaml"
        filepath = self.base_path / filename

        with open(filepath, 'w') as f:
            f.write(persona.to_yaml())

        logger.info(f"Saved emerged persona: {filepath}")
        return str(filepath)

    def load(self, persona_id: str) -> Optional[EmergedPersona]:
        """Load persona by ID."""
        # Try with and without emerged_ prefix
        for pattern in [f"emerged_{persona_id}.yaml", f"{persona_id}.yaml"]:
            filepath = self.base_path / pattern
            if filepath.exists():
                return self._load_file(filepath)
        return None

    def _load_file(self, filepath: Path) -> Optional[EmergedPersona]:
        """Load persona from file."""
        try:
            import yaml
            with open(filepath) as f:
                data = yaml.safe_load(f)
            return EmergedPersona.from_dict(data)
        except Exception as e:
            logger.warning(f"Failed to load persona from {filepath}: {e}")
            return None

    def list_all(self) -> List[EmergedPersona]:
        """List all emerged personas."""
        personas = []
        for filepath in self.base_path.glob("emerged_*.yaml"):
            persona = self._load_file(filepath)
            if persona:
                personas.append(persona)
        return sorted(personas, key=lambda p: p.extracted_at, reverse=True)

    def find_by_domain(self, domain: str) -> List[EmergedPersona]:
        """Find personas that match a domain."""
        return [p for p in self.list_all() if domain in p.task_domains]

    def find_similar(self, task: str, limit: int = 5) -> List[EmergedPersona]:
        """Find personas similar to a task description."""
        task_domains = _extract_domains(task)
        task_keywords = set(_extract_keywords(task))

        scored = []
        for persona in self.list_all():
            # Score by domain overlap
            domain_score = len(set(persona.task_domains) & set(task_domains)) / max(len(task_domains), 1)

            # Score by keyword overlap
            keyword_score = len(set(persona.task_keywords) & task_keywords) / max(len(task_keywords), 1)

            # Combined score (weighted)
            score = 0.6 * domain_score + 0.4 * keyword_score

            if score > 0:
                scored.append((score, persona))

        scored.sort(key=lambda x: x[0], reverse=True)
        return [p for _, p in scored[:limit]]

    def update_reputation(self, persona_id: str, success: bool) -> bool:
        """Update persona reputation after use."""
        persona = self.load(persona_id)
        if not persona:
            return False

        persona.uses_count += 1
        if success:
            persona.success_count += 1

        # Update reputation: Bayesian-ish update
        success_rate = persona.success_count / persona.uses_count
        persona.reputation_score = 0.3 + 0.7 * success_rate  # Range 0.3 - 1.0

        self.save(persona)
        return True


def extract_and_store_persona(
    session_id: str,
    loop_tracker: 'EpistemicLoopTracker',
    task_description: str = "",
    branch_id: str = None,
    store_path: str = None
) -> Optional[str]:
    """
    Convenience function to extract and store a persona in one call.

    Returns persona_id if successful, None otherwise.
    """
    persona = extract_persona_from_loop_tracker(
        session_id=session_id,
        loop_tracker=loop_tracker,
        task_description=task_description,
        branch_id=branch_id
    )

    if not persona:
        return None

    store = EmergedPersonaStore(store_path)
    store.save(persona)

    logger.info(f"Extracted and stored emerged persona: {persona.persona_id}")
    return persona.persona_id


def sentinel_match_persona(
    task: str,
    grounding_vectors: Dict[str, float] = None,
    min_reputation: float = 0.5,
    store_path: str = None
) -> Optional[EmergedPersona]:
    """
    Sentinel-level persona matching: finds best persona for task + grounding.

    Args:
        task: Task description
        grounding_vectors: Current epistemic grounding (know, uncertainty, etc.)
        min_reputation: Minimum reputation score to consider
        store_path: Custom store path

    Returns:
        Best matching EmergedPersona or None

    The matching considers:
    1. Task similarity (domain + keyword matching)
    2. Vector compatibility (if grounding provided)
    3. Reputation score
    """
    store = EmergedPersonaStore(store_path)
    candidates = store.find_similar(task, limit=10)

    if not candidates:
        return None

    # Filter by reputation
    candidates = [p for p in candidates if p.reputation_score >= min_reputation]

    if not candidates:
        return None

    # If no grounding provided, just return best by reputation
    if not grounding_vectors:
        candidates.sort(key=lambda p: p.reputation_score, reverse=True)
        return candidates[0]

    # Score by vector compatibility: prefer personas whose initial_vectors
    # are similar to current grounding (they started from similar state)
    scored = []
    for persona in candidates:
        # Vector distance (lower is better)
        vector_diff = 0
        count = 0
        for key, value in grounding_vectors.items():
            if key in persona.initial_vectors:
                vector_diff += abs(persona.initial_vectors[key] - value)
                count += 1
        avg_diff = vector_diff / max(count, 1)

        # Combined score: reputation + vector compatibility
        compatibility = 1.0 - min(avg_diff, 1.0)
        combined_score = 0.4 * persona.reputation_score + 0.6 * compatibility

        scored.append((combined_score, persona))

    scored.sort(key=lambda x: x[0], reverse=True)
    return scored[0][1] if scored else None


def match_or_decompose(
    task: str,
    session_id: str,
    grounding_vectors: Dict[str, float] = None,
    min_reputation: float = 0.3,
    store_path: str = None
) -> Dict[str, Any]:
    """
    Attempt to match a persona for a task. If no match, trigger decomposition.

    This is the adaptive growth path (immune system analog):
    1. Try to match existing persona via sentinel_match_persona
    2. If match found → return persona config for agent routing
    3. If no match → return decomposition directive for the caller
       (parallel branch investigation → extract emerged persona → embed → regenerate)

    Args:
        task: Task description to match
        session_id: Current session for context
        grounding_vectors: Current epistemic state
        min_reputation: Minimum reputation threshold
        store_path: Custom persona store path

    Returns:
        Dict with either:
        - {"matched": True, "persona": EmergedPersona, "agent_name": str}
        - {"matched": False, "decompose": True, "task": str, "reason": str}
    """
    persona = sentinel_match_persona(
        task=task,
        grounding_vectors=grounding_vectors,
        min_reputation=min_reputation,
        store_path=store_path
    )

    if persona:
        # Convert persona_id to agent name format
        agent_name = persona.persona_id.replace("_", "-")
        logger.info(
            f"Persona matched: {persona.name} (rep={persona.reputation_score:.2f}) "
            f"for task: {task[:50]}"
        )
        return {
            "matched": True,
            "persona": persona.to_dict(),
            "agent_name": agent_name,
            "persona_id": persona.persona_id,
            "reputation": persona.reputation_score,
            "domains": persona.task_domains
        }

    # No match — signal decomposition needed
    logger.info(
        f"No persona match for task: {task[:50]}. "
        f"Decomposition recommended (parallel branch → emerged persona)."
    )
    return {
        "matched": False,
        "decompose": True,
        "task": task,
        "session_id": session_id,
        "reason": "No existing persona matches this task's domain profile. "
                  "A parallel investigation branch should explore this task, "
                  "and the resulting epistemic pattern will be extracted as "
                  "a new emerged persona for future use.",
        "suggested_action": "empirica investigate --session-id {session_id} "
                          "--investigation-goal \"{task}\" --turtle",
        "post_action": "After investigation completes, run: "
                      "python generate_agents.py --force "
                      "to regenerate agent definitions from new personas."
    }


def convert_emerged_to_persona_json(emerged: EmergedPersona) -> Dict[str, Any]:
    """
    Convert an EmergedPersona to the standard persona JSON format
    used by generate_agents.py for Claude Code agent generation.

    This bridges the gap between emerged personas (extracted from
    investigation branches) and persona profiles (.empirica/personas/*.json).
    """
    # Map emerged persona vectors to epistemic config priors
    priors = {}
    for key in ["engagement", "know", "do", "context", "clarity", "coherence",
                "signal", "density", "state", "change", "completion", "impact", "uncertainty"]:
        if key in emerged.final_vectors:
            priors[key] = emerged.final_vectors[key]
        elif key in emerged.initial_vectors:
            priors[key] = emerged.initial_vectors[key]
        else:
            priors[key] = 0.5  # Default

    # Derive thresholds from convergence characteristics
    thresholds = {
        "uncertainty_trigger": min(emerged.convergence_threshold * 10, 0.4),
        "confidence_to_proceed": max(0.7, emerged.reputation_score),
        "signal_quality_min": 0.6,
        "engagement_gate": 0.6
    }

    # Determine capabilities from reputation and scope
    can_modify = emerged.reputation_score >= 0.6
    can_external = emerged.reputation_score >= 0.7

    return {
        "persona_id": emerged.persona_id,
        "name": emerged.name,
        "version": "1.0.0-emerged",
        "signing_identity": {
            "user_id": "emerged",
            "identity_name": emerged.persona_id,
            "public_key": "",
            "reputation_score": emerged.reputation_score
        },
        "epistemic_config": {
            "priors": priors,
            "thresholds": thresholds,
            "weights": {
                "foundation": 0.30,
                "comprehension": 0.30,
                "execution": 0.25,
                "engagement": 0.15
            },
            "focus_domains": emerged.task_domains
        },
        "capabilities": {
            "can_spawn_subpersonas": False,
            "can_call_external_tools": can_external,
            "can_modify_code": can_modify,
            "can_read_files": True,
            "requires_human_approval": False,
            "max_investigation_depth": max(emerged.loops_to_converge, 3),
            "restricted_operations": []
        },
        "sentinel_config": {
            "reporting_frequency": "per_phase",
            "escalation_triggers": [],
            "timeout_minutes": 30,
            "max_cost_usd": 5.0,
            "requires_sentinel_approval_before_act": False
        },
        "metadata": {
            "created_by": "emerged",
            "created_at": emerged.extracted_at,
            "modified_at": emerged.extracted_at,
            "description": f"Auto-extracted from session {emerged.source_session_id[:8]}",
            "tags": emerged.task_keywords[:5],
            "parent_persona": None,
            "derived_from": emerged.source_branch_id,
            "verified_sessions": emerged.uses_count
        }
    }


def promote_emerged_to_personas_dir(
    persona_id: str,
    personas_dir: str = None,
    store_path: str = None
) -> Optional[str]:
    """
    Promote an emerged persona to the standard personas directory,
    making it available for agent generation.

    Args:
        persona_id: ID of the emerged persona to promote
        personas_dir: Target directory (default: .empirica/personas/)
        store_path: Emerged persona store path

    Returns:
        Path to the written persona JSON file, or None on failure
    """
    store = EmergedPersonaStore(store_path)
    emerged = store.load(persona_id)

    if not emerged:
        logger.warning(f"Emerged persona not found: {persona_id}")
        return None

    # Convert to standard format
    persona_json = convert_emerged_to_persona_json(emerged)

    # Write to personas directory
    if personas_dir is None:
        personas_dir = str(Path.cwd() / ".empirica" / "personas")

    target_dir = Path(personas_dir)
    target_dir.mkdir(parents=True, exist_ok=True)

    output_path = target_dir / f"{persona_id}.json"
    output_path.write_text(json.dumps(persona_json, indent=2))

    logger.info(f"Promoted emerged persona to: {output_path}")
    return str(output_path)
