"""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_hot_cache_concise as _format_hot_cache_concise,
)
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,
        trust_weight=settings.recall_trust_weight,
        helpfulness_weight=settings.recall_helpfulness_weight,
        hybrid_enabled=settings.hybrid_search_enabled,
        prefix=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.
    Disabled by default (auto_bootstrap=False) since markdown files are often
    redundant with what's already in the codebase/context.
    """
    from pathlib import Path

    # Check if auto-bootstrap is enabled (disabled by default)
    if not settings.auto_bootstrap:
        return False

    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, include_ids: bool = False) -> 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]")
        include_ids: If True, include memory IDs for feedback tracking

    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 ""
        id_prefix = f"[id:{m.id}] " if include_ids else ""
        lines.append(f"- {id_prefix}{content}{tags_str}")

    return "\n".join(lines)


def _format_clustered_memory_list(
    clusters: list[DisplayCluster],
    unclustered: list[Memory],
    header: str,
    include_ids: bool = False,
) -> 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
        include_ids: If True, include memory IDs for feedback tracking

    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 ""
            id_prefix = f"[id:{m.id}] " if include_ids else ""
            lines.append(f"  - {id_prefix}{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 ""
            id_prefix = f"[id:{m.id}] " if include_ids else ""
            lines.append(f"  - {id_prefix}{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).
    Logs injections for feedback loop analysis (7-day retention).

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

    Concise format with category prefixes and metadata for quick scanning.
    """
    # 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()

    # Log injections for feedback loop tracking
    session_id = get_current_session_id()
    memory_ids = [m.id for m in hot_memories]
    storage.log_injections_batch(
        memory_ids=memory_ids,
        resource="hot-cache",
        session_id=session_id,
        project_id=project_id,
    )

    # Use concise format with category prefixes
    return _format_hot_cache_concise(
        memories=hot_memories,
        project_id=project_id,
        max_chars=settings.hot_cache_display_max_chars,
    )


@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.
    Logs injections for feedback loop analysis (7-day retention).
    """
    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]"

    # Log injections for feedback loop tracking
    project_id = get_auto_project_id()
    session_id = get_current_session_id()
    memory_ids = [m.id for m in working_set]
    storage.log_injections_batch(
        memory_ids=memory_ids,
        resource="working-set",
        session_id=session_id,
        project_id=project_id,
    )

    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)
