"""
Lesson system tool for gptme.

Provides structured lessons with metadata that can be automatically included in context.
Similar to .cursorrules or "Claude Skills". Has keyword-based triggering.

Commands provided:

- ``/lesson list`` - View all available lessons
- ``/lesson search <query>`` - Find lessons matching query
- ``/lesson show <id>`` - Display a specific lesson
- ``/lesson refresh`` - Reload lessons from disk
"""

import logging
from collections.abc import Generator
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from ..commands import CommandContext
from ..config import get_config
from ..hooks import HookType, StopPropagation
from ..lessons import LessonIndex, LessonMatcher, MatchContext
from ..lessons.commands import lesson
from ..message import Message
from .base import ToolSpec

if TYPE_CHECKING:
    from ..logmanager import LogManager

logger = logging.getLogger(__name__)


def _get_ace_components() -> tuple[type | None, type | None]:
    """Lazily import ACE components when needed.

    Returns:
        Tuple of (LessonEmbedder class, GptmeHybridMatcher class), or (None, None) if unavailable.
    """
    try:
        from ace.embedder import LessonEmbedder  # type: ignore[import-not-found]
        from ace.gptme_integration import (  # type: ignore[import-not-found]
            GptmeHybridMatcher,
        )

        return LessonEmbedder, GptmeHybridMatcher
    except ImportError:
        logger.debug("ACE not available - sentence-transformers not installed")
        return None, None


# Context-local storage for lesson index
_lesson_index_var: ContextVar[LessonIndex | None] = ContextVar(
    "lesson_index", default=None
)


@dataclass
class LessonSessionStats:
    """Statistics about lessons matched during a session."""

    total_matched: int = 0
    unique_lessons: set[str] = field(default_factory=set)
    lesson_titles: dict[str, str] = field(default_factory=dict)  # path -> title


# Context-local storage for session statistics
_session_stats_var: ContextVar[LessonSessionStats | None] = ContextVar(
    "lesson_session_stats", default=None
)


def _get_session_stats() -> LessonSessionStats:
    """Get context-local session stats, creating if needed."""
    stats = _session_stats_var.get()
    if stats is None:
        stats = LessonSessionStats()
        _session_stats_var.set(stats)
    return stats


def _reset_session_stats() -> None:
    """Reset session statistics for a new session."""
    _session_stats_var.set(LessonSessionStats())


def _get_lesson_index() -> LessonIndex:
    """Get context-local lesson index, creating it if needed."""
    index = _lesson_index_var.get()
    if index is None:
        index = LessonIndex()
        _lesson_index_var.set(index)
    return index


def _get_included_lessons_from_log(log: list[Message]) -> set[str]:
    """Extract lesson paths that have already been included in the conversation.

    Args:
        log: Conversation log

    Returns:
        Set of lesson paths (as strings) that have been included
    """
    included = set()

    for msg in log:
        if msg.role == "system" and "# Relevant Lessons" in msg.content:
            # Extract lesson paths from formatted lessons
            # Format: *Path: /some/path/lesson.md*
            lines = msg.content.split("\n")
            for line in lines:
                if line.startswith("*Path: ") and line.endswith("*"):
                    # Extract path between "*Path: " and final "*"
                    path_str = line[7:-1]  # Remove "*Path: " prefix and "*" suffix
                    included.add(path_str)

    return included


def _extract_recent_tools(log: list[Message], limit: int = 10) -> list[str]:
    """Extract tools used in recent messages.

    Args:
        log: Conversation log
        limit: Number of recent messages to check

    Returns:
        List of unique tool names used
    """
    tools = []

    # Check recent messages for tool use
    for msg in reversed(log[-limit:]):
        # Check for tool use in assistant messages
        if msg.role == "assistant":
            # Extract tool names from ToolUse/ToolResult patterns
            for block in msg.get_codeblocks():
                if block.lang and block.lang not in ("text", "markdown"):
                    # Extract just the tool name (first word) from lang
                    # e.g., "patch file.py" -> "patch"
                    tool_name = block.lang.split()[0]
                    tools.append(tool_name)

    # Return unique tools, preserving order
    seen = set()
    unique_tools = []
    for tool in tools:
        if tool not in seen:
            seen.add(tool)
            unique_tools.append(tool)

    return unique_tools


def _extract_message_content(log: list[Message], limit: int = 10) -> str:
    """Extract message content from recent user and assistant messages.

    Args:
        log: Conversation log
        limit: Number of recent messages to check

    Returns:
        Combined message content string
    """
    messages = [
        msg.content
        for msg in reversed(log[-limit:])
        if msg.role in ("user", "assistant")
    ]

    # Combine messages (most recent first, so reverse to get chronological)
    combined = " ".join(reversed(messages))

    logger.debug(
        f"Extracted content from {len(messages)} messages "
        f"(content length: {len(combined)} chars)"
    )

    return combined


def handle_lesson_command(ctx: CommandContext) -> Generator[Message, None, None]:
    """Handle /lesson command."""
    # Delegate to the command handler
    yield from lesson(ctx)


def auto_include_lessons_hook(
    manager: "LogManager",
) -> Generator[Message | StopPropagation, None, None]:
    """Hook to automatically include relevant lessons in context.

    Extracts keywords from both user and assistant messages to trigger lessons.

    Args:
        manager: Conversation manager with log and workspace

    Returns:
        Generator of messages to prepend (lessons as system message)
    """
    # Get configuration
    config = get_config()
    auto_include = config.get_env_bool("GPTME_LESSONS_AUTO_INCLUDE", True)

    if not auto_include:
        logger.debug("Auto-inclusion disabled")
        return

    # Get hybrid matching configuration
    use_hybrid = config.get_env_bool("GPTME_LESSONS_USE_HYBRID", False)

    # Session-wide limit (higher default, applies across entire session)
    try:
        max_lessons = int(config.get_env("GPTME_LESSONS_MAX_SESSION") or "20")
    except (ValueError, TypeError):
        max_lessons = 20

    # Get session stats and check if we've hit the limit
    stats = _get_session_stats()

    # Initialize stats from log if empty (e.g., when resuming a conversation)
    if not stats.unique_lessons:
        included_in_log = _get_included_lessons_from_log(manager.log.messages)
        if included_in_log:
            stats.unique_lessons.update(included_in_log)
            stats.total_matched = len(included_in_log)
            logger.debug(
                f"Initialized session stats from log: {len(included_in_log)} lessons"
            )

    if len(stats.unique_lessons) >= max_lessons:
        logger.debug(
            f"Session lesson limit reached ({len(stats.unique_lessons)}/{max_lessons})"
        )
        return

    # Get messages from log
    messages = manager.log.messages

    # Get lessons already included
    included_lessons = _get_included_lessons_from_log(messages)

    # Extract message content from recent user and assistant messages
    message_content = _extract_message_content(messages)
    tools = _extract_recent_tools(messages)

    if not message_content and not tools:
        logger.debug("No message content or tools to match, skipping lesson inclusion")
        return

    # Create match context
    context = MatchContext(
        message=message_content,
        tools_used=tools,
    )

    # Get lesson index and find matching lessons
    try:
        index = _get_lesson_index()

        # Track whether we're using hybrid matching (for session_id support)
        using_hybrid = False

        # Choose matcher based on configuration
        if use_hybrid:
            # Only import ACE when explicitly requested
            LessonEmbedder, GptmeHybridMatcher = _get_ace_components()

            if LessonEmbedder is not None and GptmeHybridMatcher is not None:
                logger.info("Using ACE hybrid lesson matching")
                # Initialize embedder with lesson directories from LessonIndex
                from pathlib import Path

                lesson_dirs = index.lesson_dirs

                # Use first lesson directory for embedder (typically workspace lessons)
                # Embeddings stored in .gptme/embeddings/lessons/
                embeddings_dir = Path.cwd() / ".gptme" / "embeddings" / "lessons"

                try:
                    embedder = LessonEmbedder(
                        lessons_dir=lesson_dirs[0]
                        if lesson_dirs
                        else Path.cwd() / "lessons",
                        embeddings_dir=embeddings_dir,
                    )
                    logger.info(
                        f"Initialized ACE embedder with lessons_dir={lesson_dirs[0] if lesson_dirs else 'lessons'}"
                    )
                    matcher = GptmeHybridMatcher(embedder=embedder)
                    using_hybrid = True
                except Exception as e:
                    logger.warning(
                        f"Failed to initialize embedder: {e}. Falling back to keyword matching."
                    )
                    # Fall back to keyword-only (not hybrid with None embedder)
                    matcher = LessonMatcher()
            else:
                logger.warning(
                    "Hybrid matching requested but ACE not available "
                    "(install sentence-transformers), falling back to keyword-only"
                )
                matcher = LessonMatcher()
        else:
            logger.debug("Using keyword-only lesson matcher")
            matcher = LessonMatcher()

        # Generate session_id from chat_id for tracking (only for hybrid matcher)
        session_id = manager.chat_id

        # Call matcher with appropriate parameters
        # Only GptmeHybridMatcher supports session_id parameter
        if using_hybrid:
            match_results = matcher.match(index.lessons, context, session_id=session_id)
        else:
            match_results = matcher.match(index.lessons, context)

        # Filter out already included lessons (MatchResult has .lesson attribute)
        new_matches = [
            match
            for match in match_results
            if str(match.lesson.path) not in included_lessons
            and str(match.lesson.path) not in stats.unique_lessons
        ]

        # Limit to remaining session budget
        remaining_budget = max_lessons - len(stats.unique_lessons)
        if len(new_matches) > remaining_budget:
            logger.debug(
                f"Limiting lessons from {len(new_matches)} to {remaining_budget} "
                f"(session: {len(stats.unique_lessons)}/{max_lessons})"
            )
            new_matches = new_matches[:remaining_budget]

        if not new_matches:
            logger.debug("No new lessons to include")
            return

        # Format lessons as system message
        content_parts = ["# Relevant Lessons\n"]
        for match in new_matches:
            lesson = match.lesson
            content_parts.append(f"\n## {lesson.title}\n")
            content_parts.append(f"\n*Path: {lesson.path}*\n")
            content_parts.append(f"\n*Category: {lesson.category}*\n")
            content_parts.append(
                f"\n*Matched by: {len(match.matched_by)} keyword(s)*\n"
            )
            content_parts.append(f"\n{lesson.body}\n")

        lesson_msg = Message(
            role="system",
            content="".join(content_parts),
            hide=True,  # Hide from user-facing output
        )

        # Update session statistics
        for match in new_matches:
            path_str = str(match.lesson.path)
            stats.unique_lessons.add(path_str)
            stats.lesson_titles[path_str] = match.lesson.title
        stats.total_matched += len(new_matches)

        titles = [str(match.lesson.title) for match in new_matches]
        titles_list = "\n".join(f"- {title}" for title in titles)
        logger.info(f"Auto-included {len(new_matches)} lessons:\n{titles_list}")

        yield lesson_msg

    except Exception as e:
        logger.warning(f"Error during lesson auto-inclusion: {e}")
        return


def session_end_lessons_hook(
    manager: "LogManager", **kwargs
) -> Generator[Message | StopPropagation, None, None]:
    """Hook to print lesson statistics at end of session.

    Args:
        manager: Conversation manager with log and workspace
        **kwargs: Additional arguments (e.g., logdir)

    Yields:
        Nothing (just logs statistics)
    """
    from ..util import console

    stats = _session_stats_var.get()
    if stats is None or stats.total_matched == 0:
        return

    # Print summary to console
    console.print(
        f"[dim]Lessons: {len(stats.unique_lessons)} unique lessons included "
        f"({stats.total_matched} total matches)[/dim]"
    )

    # Reset stats for next session
    _reset_session_stats()

    # Don't yield any messages - just log
    yield from ()


# Tool specification (for /tools command)
tool = ToolSpec(
    name="lessons",
    desc="Lesson system for structured guidance",
    instructions="""
Use lessons to learn and remember skills/tools/workflows, improve your performance, and avoid known failure modes.

How lessons help you:
- Automatically included when relevant keywords or tools match
- Extracted from both user and assistant messages in the conversation
- Session-wide limit (default 20) prevents context bloat

Leverage lessons for self-improvement:
- Pay attention to lessons included in context
- Apply patterns and avoid anti-patterns
- Reference lessons when making decisions
- Learn from past failures documented in lessons
""".strip(),
    examples="",
    functions=[],
    hooks={
        "auto_include_lessons": (
            HookType.STEP_PRE.value,
            auto_include_lessons_hook,
            5,  # Medium priority
        ),
        "session_end_lessons": (
            HookType.SESSION_END.value,
            session_end_lessons_hook,
            5,  # Medium priority
        ),
    },
    commands={
        "lesson": handle_lesson_command,
    },
)
