"""FastMCP app setup, shared state, helper functions, and resources."""

import os
import uuid

from fastmcp import FastMCP

from memory_mcp.config import (
    check_stop_hook_configured,
    find_bootstrap_files,
    get_hook_install_instructions,
    get_settings,
)
from memory_mcp.helpers import (
    build_ranking_factors as _build_ranking_factors,
)
from memory_mcp.helpers import (
    cluster_memories_for_display as _cluster_memories_for_display,
)
from memory_mcp.helpers import (
    format_memories_for_llm as _format_memories_for_llm,
)
from memory_mcp.helpers import (
    get_promotion_suggestions as _get_promotion_suggestions,
)
from memory_mcp.helpers import (
    get_similarity_confidence as _get_similarity_confidence,
)
from memory_mcp.logging import (
    configure_logging,
    get_logger,
)
from memory_mcp.models import DisplayCluster, Memory
from memory_mcp.project import detect_project, get_current_project_id
from memory_mcp.storage import MemoryType, RecallMode, Storage

log = get_logger("server")

# ========== Shared State ==========
settings = get_settings()
configure_logging(level=settings.log_level, log_format=settings.log_format)
storage = Storage(settings)
mcp = FastMCP("memory-mcp")

log.info("Memory MCP server initialized")

# Warn if Stop hook not configured (pattern mining won't work without it)
if settings.warn_missing_hook and settings.mining_enabled:
    if not check_stop_hook_configured():
        log.warning("Stop hook not configured - pattern mining will not auto-log outputs")
        log.warning(get_hook_install_instructions())


# ========== Helper Function Wrappers ==========


def build_ranking_factors(mode: RecallMode | None, prefix: str = "") -> str:
    """Build ranking factors string using module storage."""
    mode_config = storage.get_recall_mode_config(mode or RecallMode.BALANCED)
    mode_name = mode.value if mode else "balanced"
    return _build_ranking_factors(
        mode_name,
        mode_config.similarity_weight,
        mode_config.recency_weight,
        mode_config.access_weight,
        prefix,
    )


def get_promotion_suggestions(memories: list[Memory], max_suggestions: int = 2) -> list[dict]:
    """Get promotion suggestions using module settings."""
    return _get_promotion_suggestions(memories, settings.promotion_threshold, max_suggestions)


def format_memories_for_llm(memories: list[Memory]):
    """Format memories for LLM using module settings."""
    return _format_memories_for_llm(
        memories,
        settings.high_confidence_threshold,
        settings.default_confidence_threshold,
    )


def get_similarity_confidence(similarity: float | None) -> str:
    """Map similarity score to confidence label using module settings."""
    return _get_similarity_confidence(
        similarity,
        settings.high_confidence_threshold,
        settings.default_confidence_threshold,
    )


def get_auto_project_id() -> str | None:
    """Get the current project ID if project awareness is enabled."""
    if not settings.project_awareness_enabled:
        return None
    return get_current_project_id()


# ========== Session State ==========

_current_session_id: str | None = None


def get_current_session_id() -> str:
    """Get or create the current server session ID.

    Creates a session on first call, reusing it for all subsequent calls.
    This enables zero-config session tracking - memories are automatically
    associated with the current session without requiring explicit session_id.
    """
    global _current_session_id
    if _current_session_id is None:
        _current_session_id = str(uuid.uuid4())
        project_path = os.getcwd()
        storage.create_or_get_session(_current_session_id, project_path=project_path)
        log.info("Auto-created session: {} for project: {}", _current_session_id, project_path)
    return _current_session_id


# ========== Hot Cache Formatting ==========

# Track whether we've attempted auto-bootstrap this session (per working directory)
_auto_bootstrap_attempted: set[str] = set()


def _try_auto_bootstrap() -> bool:
    """Attempt to auto-bootstrap from current directory if hot cache is empty.

    Returns True if bootstrap was attempted and created memories.
    Only runs once per working directory per session.
    """
    from pathlib import Path

    cwd = os.getcwd()

    # Only attempt once per directory per session
    if cwd in _auto_bootstrap_attempted:
        return False
    _auto_bootstrap_attempted.add(cwd)

    file_paths = find_bootstrap_files(Path(cwd))
    if not file_paths:
        log.debug("Auto-bootstrap: no documentation files found in {}", cwd)
        return False

    log.info("Auto-bootstrap: found {} documentation files in {}", len(file_paths), cwd)

    result = storage.bootstrap_from_files(
        file_paths=file_paths,
        memory_type=MemoryType.PROJECT,
        promote_to_hot=True,
        tags=["auto-bootstrap"],
    )

    if result["memories_created"] > 0:
        log.info(
            "Auto-bootstrap: created {} memories from {} files",
            result["memories_created"],
            result["files_processed"],
        )
        return True

    return False


def _format_memory_list(memories: list[Memory], header: str) -> str:
    """Format a list of memories as a resource string.

    Args:
        memories: List of Memory objects to format
        header: Header line for the resource (e.g., "[MEMORY: Hot Cache]")

    Returns:
        Formatted string with header and memory items
    """
    max_chars = settings.hot_cache_display_max_chars
    lines = [header]

    for m in memories:
        content = m.content[:max_chars] + "..." if len(m.content) > max_chars else m.content
        tags_str = f" [{', '.join(m.tags[:3])}]" if m.tags else ""
        lines.append(f"- {content}{tags_str}")

    return "\n".join(lines)


def _format_clustered_memory_list(
    clusters: list[DisplayCluster],
    unclustered: list[Memory],
    header: str,
) -> str:
    """Format clustered memories as a resource string.

    Semantic clustering groups related memories together to reduce
    cognitive load (RePo research-inspired).

    Args:
        clusters: List of DisplayCluster objects
        unclustered: Memories that didn't fit in any cluster
        header: Header line for the resource

    Returns:
        Formatted string with headers for each cluster
    """
    max_chars = settings.hot_cache_display_max_chars
    lines = [header]

    for cluster in clusters:
        lines.append(f"\n## {cluster.label} ({cluster.size} items)")
        for m in cluster.members:
            content = m.content[:max_chars] + "..." if len(m.content) > max_chars else m.content
            tags_str = f" [{', '.join(m.tags[:3])}]" if m.tags else ""
            lines.append(f"  - {content}{tags_str}")

    if unclustered:
        lines.append("\n## Other")
        for m in unclustered:
            content = m.content[:max_chars] + "..." if len(m.content) > max_chars else m.content
            tags_str = f" [{', '.join(m.tags[:3])}]" if m.tags else ""
            lines.append(f"  - {content}{tags_str}")

    return "\n".join(lines)


# ========== MCP Resources ==========


@mcp.resource("memory://hot-cache")
def hot_cache_resource() -> str:
    """Auto-injectable system context with high-confidence patterns.

    Configure Claude Code to include this resource in system prompts
    for instant recall of frequently-used knowledge.

    If the hot cache is empty and documentation files exist in the current
    directory, auto-bootstraps from README.md, CLAUDE.md, etc.

    Records hit/miss metrics for observability (see hot_cache_status).

    Project-aware: If project awareness is enabled, filters to current project
    plus global memories.

    Semantic clustering: Groups related memories together to reduce
    cognitive load (enabled by default, configure via settings).
    """
    # Get current project for project-aware hot cache
    project_id = get_auto_project_id()
    hot_memories = storage.get_hot_memories(project_id=project_id)

    if not hot_memories and _try_auto_bootstrap():
        hot_memories = storage.get_hot_memories(project_id=project_id)

    if not hot_memories:
        storage.record_hot_cache_miss()
        return "[MEMORY: Hot cache empty - no frequently-accessed patterns yet]"

    storage.record_hot_cache_hit()
    header = "[MEMORY: Hot Cache - High-confidence patterns]"
    if project_id:
        header = f"[MEMORY: Hot Cache ({project_id}) - High-confidence patterns]"

    # Apply semantic clustering if enabled and enough memories
    if settings.clustering_display_enabled and len(hot_memories) >= 4:
        memory_ids = [m.id for m in hot_memories]
        embeddings = storage.get_embeddings_for_memories(memory_ids)

        if embeddings:
            clusters, unclustered = _cluster_memories_for_display(
                memories=hot_memories,
                embeddings=embeddings,
                threshold=settings.clustering_display_threshold,
                min_cluster_size=settings.clustering_min_size,
                max_clusters=settings.clustering_max_clusters,
            )

            # Only use clustered format if we got meaningful clusters
            if clusters:
                return _format_clustered_memory_list(clusters, unclustered, header)

    # Fall back to flat list
    return _format_memory_list(hot_memories, header)


@mcp.resource("memory://working-set")
def working_set_resource() -> str:
    """Session-aware active memory context (Engram-inspired).

    Provides a compact working set of contextually relevant memories:
    1. Recently recalled memories (that were actually used)
    2. Predicted next memories (from access pattern learning)
    3. Top salience hot items (to fill remaining slots)

    Smaller and more focused than hot-cache - designed for active work context.
    Semantic clustering groups related items together.
    """
    if not settings.working_set_enabled:
        return "[MEMORY: Working set disabled]"

    working_set = storage.get_working_set()

    if not working_set:
        return "[MEMORY: Working set empty - no recent activity]"

    header = "[MEMORY: Working Set - Active context]"

    # Apply semantic clustering if enabled and enough memories
    if settings.clustering_display_enabled and len(working_set) >= 4:
        memory_ids = [m.id for m in working_set]
        embeddings = storage.get_embeddings_for_memories(memory_ids)

        if embeddings:
            clusters, unclustered = _cluster_memories_for_display(
                memories=working_set,
                embeddings=embeddings,
                threshold=settings.clustering_display_threshold,
                min_cluster_size=settings.clustering_min_size,
                max_clusters=settings.clustering_max_clusters,
            )

            if clusters:
                return _format_clustered_memory_list(clusters, unclustered, header)

    return _format_memory_list(working_set, header)


@mcp.resource("memory://project-context")
def project_context_resource() -> str:
    """Project-specific memory context for the current git repository.

    Returns:
    - Current project ID (from git remote URL)
    - Project-specific hot cache memories
    - Recent project activity

    Use this to understand what memories are associated with the current project.
    """
    project = detect_project()

    if not project:
        return "[MEMORY: No git project detected - memories will be global]"

    # Get project-specific hot memories
    hot_memories = storage.get_hot_memories(project_id=project.id)

    lines = [
        f"[MEMORY: Project Context - {project.name}]",
        f"Project ID: {project.id}",
        f"Path: {project.path}",
    ]

    if project.remote_url:
        lines.append(f"Remote: {project.remote_url}")

    lines.append(f"Hot memories: {len(hot_memories)}")

    if hot_memories:
        lines.append("")
        lines.append("Recent hot memories:")
        max_chars = settings.hot_cache_display_max_chars
        for m in hot_memories[:5]:  # Show top 5
            content = m.content[:max_chars] + "..." if len(m.content) > max_chars else m.content
            lines.append(f"  - {content}")

    return "\n".join(lines)
