"""Core type definitions for Promethian."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any
import hashlib


class ConfidenceLevel(Enum):
    """Confidence levels for task execution."""
    HIGH = "high"      # >= threshold, try skills first
    MEDIUM = "medium"  # Borderline, may need tools
    LOW = "low"        # < threshold, build tools


class ExecutionStrategy(Enum):
    """Strategy for executing a plan."""
    SKILLS_ONLY = "skills_only"
    SKILLS_THEN_TOOLS = "skills_then_tools"
    BUILD_TOOLS = "build_tools"


class ExecutionStatus(Enum):
    """Status of an execution attempt."""
    SUCCESS = "success"
    FAILURE = "failure"
    PARTIAL = "partial"
    HEALING = "healing"


class MessageType(Enum):
    """Type of user message in a conversation."""
    INITIAL_REQUEST = "initial_request"      # First task in session
    FOLLOW_UP = "follow_up"                  # Building on previous output
    REFINEMENT = "refinement"                # "No, I meant..."
    POSITIVE_FEEDBACK = "positive_feedback"  # "Thanks!", "Perfect"
    NEGATIVE_FEEDBACK = "negative_feedback"  # "That's wrong"
    CLARIFICATION = "clarification"          # Answering agent's question
    NEW_REQUEST = "new_request"              # Unrelated new task
    CLOSE_SESSION = "close_session"          # User indicating done


class SatisfactionType(Enum):
    """Type of satisfaction signal from user behavior."""
    # Explicit signals (high confidence)
    EXPLICIT_POSITIVE = "explicit_positive"      # "Thanks!", "Perfect"
    EXPLICIT_NEGATIVE = "explicit_negative"      # "Wrong", "No"

    # Implicit signals (medium confidence)
    IMPLICIT_POSITIVE_FOLLOW_UP = "follow_up"    # User builds on output
    IMPLICIT_POSITIVE_CLOSE = "user_close"       # User ends session cleanly
    IMPLICIT_NEGATIVE_REFINEMENT = "refinement"  # User corrects us
    IMPLICIT_NEGATIVE_ABANDON = "abandon"        # Session timeout

    # Technical signals (always captured)
    TECHNICAL_SUCCESS = "tech_success"           # Code executed
    TECHNICAL_FAILURE = "tech_failure"           # Code errored


def compute_content_hash(content: str) -> str:
    """Compute SHA256 hash of content (first 16 chars)."""
    return hashlib.sha256(content.encode()).hexdigest()[:16]


@dataclass
class Skill:
    """A learned procedural skill (markdown knowledge)."""
    id: str
    name: str
    description: str
    content: str  # Markdown content
    tags: list[str] = field(default_factory=list)
    # Required credentials/environment variables (for skills that reference APIs)
    required_env_vars: list[str] = field(default_factory=list)  # e.g., ["OPENAI_API_KEY"]
    # Memory compaction (Beads-inspired)
    summary: str | None = None  # Compact version for context efficiency
    # Hierarchy and provenance
    parent_id: str | None = None  # For hierarchical/meta-skills
    depends_on: list[str] = field(default_factory=list)  # Prerequisite skills
    derived_from: str | None = None  # Lineage tracking
    source_task: str | None = None  # Task that created this skill
    # Related items (co-occurrence based)
    related_skills: list[str] = field(default_factory=list)
    related_tools: list[str] = field(default_factory=list)
    # Standard fields
    embedding: list[float] | None = None
    content_hash: str | None = None
    created_at: datetime = field(default_factory=datetime.utcnow)
    updated_at: datetime = field(default_factory=datetime.utcnow)
    usage_count: int = 0
    success_count: int = 0
    version: int = 1

    def __post_init__(self):
        """Compute content hash if not provided."""
        if self.content_hash is None:
            self.content_hash = compute_content_hash(self.content)

    @property
    def success_rate(self) -> float:
        """Calculate success rate."""
        if self.usage_count == 0:
            return 0.0
        return self.success_count / self.usage_count

    def to_dict(self) -> dict:
        """Convert to dictionary for API/storage."""
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "summary": self.summary,
            "content": self.content,
            "tags": self.tags,
            "required_env_vars": self.required_env_vars,
            "parent_id": self.parent_id,
            "depends_on": self.depends_on,
            "derived_from": self.derived_from,
            "source_task": self.source_task,
            "related_skills": self.related_skills,
            "related_tools": self.related_tools,
            "embedding": self.embedding,
            "content_hash": self.content_hash,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "usage_count": self.usage_count,
            "success_count": self.success_count,
            "version": self.version,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Skill":
        """Create from dictionary."""
        data = data.copy()
        if isinstance(data.get("created_at"), str):
            data["created_at"] = datetime.fromisoformat(data["created_at"].replace("Z", "+00:00"))
        if isinstance(data.get("updated_at"), str):
            data["updated_at"] = datetime.fromisoformat(data["updated_at"].replace("Z", "+00:00"))
        # Handle optional fields that may not be in old data
        for optional in ["summary", "parent_id", "derived_from", "source_task"]:
            data.setdefault(optional, None)
        for list_field in ["depends_on", "related_skills", "related_tools", "required_env_vars"]:
            data.setdefault(list_field, [])
        return cls(**data)


@dataclass
class Tool:
    """A built executable tool (Python code)."""
    id: str
    name: str
    description: str
    code: str  # Python code
    function_name: str  # Entry point function
    parameters: dict[str, Any] = field(default_factory=dict)  # JSON schema
    dependencies: list[str] = field(default_factory=list)  # pip packages
    tags: list[str] = field(default_factory=list)
    # Required credentials/environment variables
    required_env_vars: list[str] = field(default_factory=list)  # e.g., ["STRIPE_API_KEY"]
    # Memory compaction (Beads-inspired)
    summary: str | None = None  # Compact version for context efficiency
    # Hierarchy and provenance
    parent_id: str | None = None  # For hierarchical tools
    depends_on: list[str] = field(default_factory=list)  # Prerequisite tools/skills
    derived_from: str | None = None  # Lineage tracking
    source_task: str | None = None  # Task that created this tool
    # Related items (co-occurrence based)
    related_tools: list[str] = field(default_factory=list)
    related_skills: list[str] = field(default_factory=list)
    # Standard fields
    embedding: list[float] | None = None
    content_hash: str | None = None
    created_at: datetime = field(default_factory=datetime.utcnow)
    updated_at: datetime = field(default_factory=datetime.utcnow)
    usage_count: int = 0
    success_count: int = 0
    version: int = 1

    def __post_init__(self):
        """Compute content hash if not provided."""
        if self.content_hash is None:
            self.content_hash = compute_content_hash(self.code)

    @property
    def success_rate(self) -> float:
        """Calculate success rate."""
        if self.usage_count == 0:
            return 0.0
        return self.success_count / self.usage_count

    def to_dict(self) -> dict:
        """Convert to dictionary for API/storage."""
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "summary": self.summary,
            "code": self.code,
            "function_name": self.function_name,
            "parameters": self.parameters,
            "dependencies": self.dependencies,
            "tags": self.tags,
            "required_env_vars": self.required_env_vars,
            "parent_id": self.parent_id,
            "depends_on": self.depends_on,
            "derived_from": self.derived_from,
            "source_task": self.source_task,
            "related_tools": self.related_tools,
            "related_skills": self.related_skills,
            "embedding": self.embedding,
            "content_hash": self.content_hash,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "usage_count": self.usage_count,
            "success_count": self.success_count,
            "version": self.version,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Tool":
        """Create from dictionary."""
        data = data.copy()
        if isinstance(data.get("created_at"), str):
            data["created_at"] = datetime.fromisoformat(data["created_at"].replace("Z", "+00:00"))
        if isinstance(data.get("updated_at"), str):
            data["updated_at"] = datetime.fromisoformat(data["updated_at"].replace("Z", "+00:00"))
        # Handle optional fields that may not be in old data
        for optional in ["summary", "parent_id", "derived_from", "source_task"]:
            data.setdefault(optional, None)
        for list_field in ["depends_on", "related_tools", "related_skills", "required_env_vars"]:
            data.setdefault(list_field, [])
        return cls(**data)

    def get_schema(self) -> dict:
        """Get Claude tool use schema."""
        # Sanitize name to match Claude API pattern: ^[a-zA-Z0-9_-]{1,64}$
        sanitized_name = self._sanitize_tool_name(self.name)

        # Ensure input_schema has the correct structure
        input_schema = self._normalize_schema(self.parameters)

        return {
            "name": sanitized_name,
            "description": self.description,
            "input_schema": input_schema,
        }

    @staticmethod
    def _normalize_schema(schema: dict) -> dict:
        """Normalize input_schema to match Claude API requirements.

        Claude expects:
        {
            "type": "object",
            "properties": {...},
            "required": [...]
        }
        """
        if not schema:
            return {"type": "object", "properties": {}}

        # If schema already has type=object, it's probably correct
        if schema.get("type") == "object":
            return schema

        # If schema has properties key, wrap it properly
        if "properties" in schema:
            normalized = {"type": "object", "properties": schema["properties"]}
            if "required" in schema:
                normalized["required"] = schema["required"]
            return normalized

        # If schema looks like bare properties, wrap it
        # e.g., {"query": {"type": "string"}, "max_results": {"type": "integer"}}
        if all(isinstance(v, dict) for v in schema.values()):
            return {"type": "object", "properties": schema}

        # Fallback: return as-is but ensure type is present
        return {"type": "object", "properties": schema}

    @staticmethod
    def _sanitize_tool_name(name: str) -> str:
        """Sanitize tool name to match Claude API requirements.

        Claude requires tool names to match: ^[a-zA-Z0-9_-]{1,64}$
        """
        import re
        # Replace spaces and invalid characters with underscores
        sanitized = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
        # Remove consecutive underscores
        sanitized = re.sub(r'_+', '_', sanitized)
        # Remove leading/trailing underscores
        sanitized = sanitized.strip('_')
        # Truncate to 64 characters
        sanitized = sanitized[:64]
        # If empty, use a default
        if not sanitized:
            sanitized = "unnamed_tool"
        return sanitized


@dataclass
class PlanStep:
    """A single step in an execution plan."""
    id: str
    description: str
    expected_output: str
    skills_to_use: list[str] = field(default_factory=list)  # Skill IDs
    tools_to_use: list[str] = field(default_factory=list)   # Tool IDs
    tools_to_build: list[str] = field(default_factory=list) # Tool descriptions
    dependencies: list[str] = field(default_factory=list)   # Step IDs this depends on

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "description": self.description,
            "expected_output": self.expected_output,
            "skills_to_use": self.skills_to_use,
            "tools_to_use": self.tools_to_use,
            "tools_to_build": self.tools_to_build,
            "dependencies": self.dependencies,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "PlanStep":
        return cls(**data)


@dataclass
class Plan:
    """An execution plan for a task."""
    task: str
    steps: list[PlanStep]
    confidence: float  # 0.0 to 1.0
    confidence_level: ConfidenceLevel
    strategy: ExecutionStrategy
    reasoning: str
    relevant_skills: list[Skill] = field(default_factory=list)
    relevant_tools: list[Tool] = field(default_factory=list)
    direct_response: str | None = None  # If set, answer directly without tools
    gathered_context: str | None = None  # Context from pre-planning (URLs, file contents, etc.)

    def to_dict(self) -> dict:
        return {
            "task": self.task,
            "steps": [s.to_dict() for s in self.steps],
            "confidence": self.confidence,
            "confidence_level": self.confidence_level.value,
            "strategy": self.strategy.value,
            "reasoning": self.reasoning,
            "relevant_skills": [s.id for s in self.relevant_skills],
            "relevant_tools": [t.id for t in self.relevant_tools],
        }


@dataclass
class StepResult:
    """Result of executing a single step."""
    step_id: str
    status: ExecutionStatus
    output: Any = None
    error: str | None = None
    attempts: int = 1
    skills_used: list[str] = field(default_factory=list)
    tools_used: list[str] = field(default_factory=list)
    tools_built: list[str] = field(default_factory=list)
    healing_log: list[str] = field(default_factory=list)
    # Track skill helpfulness for feedback loop
    skills_helpful: list[str] = field(default_factory=list)  # Skills that helped
    skills_unhelpful: list[str] = field(default_factory=list)  # Skills that didn't help


@dataclass
class ExecutionResult:
    """Result of executing a full plan."""
    task: str
    status: ExecutionStatus
    plan: Plan
    step_results: list[StepResult] = field(default_factory=list)
    final_output: Any = None
    error: str | None = None
    skills_learned: list[str] = field(default_factory=list)
    skills_updated: list[str] = field(default_factory=list)
    tools_built: list[str] = field(default_factory=list)
    tools_updated: list[str] = field(default_factory=list)
    total_attempts: int = 0
    execution_time: float = 0.0
    # Dynamic plan updates that occurred during execution
    plan_updates: list[str] = field(default_factory=list)
    # Usage statistics (tokens, costs)
    usage: dict | None = None  # UsageStats.to_dict() output


# API Response types

@dataclass
class SkillSearchResult:
    """A skill returned from search (may be partial)."""
    id: str
    name: str
    description: str
    tags: list[str]
    similarity_score: float
    success_rate: float
    usage_count: int
    # Memory compaction - summary always included
    summary: str | None = None
    # Hierarchy & dependencies
    parent_id: str | None = None
    depends_on: list[str] = field(default_factory=list)
    # Related items (co-occurrence based)
    related_skills: list[str] = field(default_factory=list)
    related_tools: list[str] = field(default_factory=list)
    # Required credentials
    required_env_vars: list[str] = field(default_factory=list)
    # Full content only if requested
    content: str | None = None

    def to_skill(self) -> Skill:
        """Convert to full Skill object (requires content)."""
        if self.content is None:
            raise ValueError("Cannot convert to Skill without content")
        return Skill(
            id=self.id,
            name=self.name,
            description=self.description,
            summary=self.summary,
            content=self.content,
            tags=self.tags,
            required_env_vars=self.required_env_vars,
            parent_id=self.parent_id,
            depends_on=self.depends_on,
            related_skills=self.related_skills,
            related_tools=self.related_tools,
            usage_count=self.usage_count,
            success_count=int(self.usage_count * self.success_rate),
        )


@dataclass
class ToolSearchResult:
    """A tool returned from search (may be partial)."""
    id: str
    name: str
    description: str
    function_name: str
    parameters: dict[str, Any]
    tags: list[str]
    similarity_score: float
    success_rate: float
    usage_count: int
    dependencies: list[str] = field(default_factory=list)
    # Memory compaction - summary always included
    summary: str | None = None
    # Hierarchy & dependencies
    parent_id: str | None = None
    depends_on: list[str] = field(default_factory=list)
    # Related items (co-occurrence based)
    related_tools: list[str] = field(default_factory=list)
    related_skills: list[str] = field(default_factory=list)
    # Required credentials
    required_env_vars: list[str] = field(default_factory=list)
    # Full content only if requested
    code: str | None = None

    def to_tool(self) -> Tool:
        """Convert to full Tool object (requires code)."""
        if self.code is None:
            raise ValueError("Cannot convert to Tool without code")
        return Tool(
            id=self.id,
            name=self.name,
            description=self.description,
            summary=self.summary,
            code=self.code,
            function_name=self.function_name,
            parameters=self.parameters,
            dependencies=self.dependencies,
            tags=self.tags,
            required_env_vars=self.required_env_vars,
            parent_id=self.parent_id,
            depends_on=self.depends_on,
            related_tools=self.related_tools,
            related_skills=self.related_skills,
            usage_count=self.usage_count,
            success_count=int(self.usage_count * self.success_rate),
        )


# ============================================
# CONVERSATION AND SATISFACTION TYPES
# ============================================

@dataclass
class Message:
    """A message in a conversation."""
    role: str  # "user" or "assistant"
    content: str
    timestamp: datetime = field(default_factory=datetime.utcnow)
    message_type: MessageType | None = None  # Classified type (for user messages)
    references_prior: bool = False  # Does this reference earlier output?
    execution_result: ExecutionResult | None = None  # If assistant executed something

    def to_dict(self) -> dict:
        return {
            "role": self.role,
            "content": self.content,
            "timestamp": self.timestamp.isoformat(),
            "message_type": self.message_type.value if self.message_type else None,
            "references_prior": self.references_prior,
        }


@dataclass
class SatisfactionSignal:
    """A signal indicating user satisfaction or dissatisfaction."""
    signal_type: SatisfactionType
    timestamp: datetime = field(default_factory=datetime.utcnow)
    message_index: int | None = None  # Which message triggered this
    confidence: float = 1.0  # How confident we are in this signal

    def to_dict(self) -> dict:
        return {
            "signal_type": self.signal_type.value,
            "timestamp": self.timestamp.isoformat(),
            "message_index": self.message_index,
            "confidence": self.confidence,
        }


# Weights for calculating success score
SATISFACTION_WEIGHTS = {
    SatisfactionType.EXPLICIT_POSITIVE: 0.4,
    SatisfactionType.EXPLICIT_NEGATIVE: -0.5,
    SatisfactionType.IMPLICIT_POSITIVE_FOLLOW_UP: 0.2,
    SatisfactionType.IMPLICIT_POSITIVE_CLOSE: 0.1,
    SatisfactionType.IMPLICIT_NEGATIVE_REFINEMENT: -0.1,
    SatisfactionType.IMPLICIT_NEGATIVE_ABANDON: -0.3,
    SatisfactionType.TECHNICAL_SUCCESS: 0.2,
    SatisfactionType.TECHNICAL_FAILURE: -0.3,
}


def calculate_success_score(signals: list[SatisfactionSignal]) -> float:
    """
    Calculate a 0-1 success score from satisfaction signals.

    Starts at 0.5 (neutral), adds weighted signals, clamps to [0, 1].
    """
    score = 0.5  # Start neutral

    for signal in signals:
        weight = SATISFACTION_WEIGHTS.get(signal.signal_type, 0)
        score += weight * signal.confidence

    return max(0.0, min(1.0, score))


@dataclass
class ConversationSession:
    """A conversation session spanning multiple user messages."""
    session_id: str
    task_description: str  # Original task
    messages: list[Message] = field(default_factory=list)
    satisfaction_signals: list[SatisfactionSignal] = field(default_factory=list)
    execution_history: list[ExecutionResult] = field(default_factory=list)
    retrieved_skills: dict[str, Skill] = field(default_factory=dict)
    retrieved_tools: dict[str, Tool] = field(default_factory=dict)
    # Improved items (skills/tools that were enhanced during self-healing)
    improved_skills: dict[str, Skill] = field(default_factory=dict)
    improved_tools: dict[str, Tool] = field(default_factory=dict)
    started_at: datetime = field(default_factory=datetime.utcnow)
    expires_at: datetime | None = None

    def add_signal(
        self,
        signal_type: SatisfactionType,
        message_index: int | None = None,
        confidence: float = 1.0,
    ) -> None:
        """Add a satisfaction signal."""
        self.satisfaction_signals.append(SatisfactionSignal(
            signal_type=signal_type,
            message_index=message_index,
            confidence=confidence,
        ))

    def get_success_score(self) -> float:
        """Calculate the current success score."""
        return calculate_success_score(self.satisfaction_signals)

    def get_conversation_context(self, max_messages: int = 10) -> str:
        """Get formatted conversation history for context."""
        recent = self.messages[-max_messages:] if len(self.messages) > max_messages else self.messages
        lines = []
        for msg in recent:
            role = "User" if msg.role == "user" else "Assistant"
            lines.append(f"{role}: {msg.content}")
        return "\n\n".join(lines)

    @property
    def conversation_length(self) -> int:
        """Number of user messages (turns)."""
        return sum(1 for m in self.messages if m.role == "user")

    @property
    def had_refinements(self) -> bool:
        """Did the user need to correct/refine?"""
        return any(
            s.signal_type == SatisfactionType.IMPLICIT_NEGATIVE_REFINEMENT
            for s in self.satisfaction_signals
        )

    def to_dict(self) -> dict:
        return {
            "session_id": self.session_id,
            "task_description": self.task_description,
            "messages": [m.to_dict() for m in self.messages],
            "satisfaction_signals": [s.to_dict() for s in self.satisfaction_signals],
            "conversation_length": self.conversation_length,
            "success_score": self.get_success_score(),
            "had_refinements": self.had_refinements,
            "started_at": self.started_at.isoformat(),
        }
