"""MCP Server for ZK Framework Documentation."""

from fastmcp import FastMCP
import time
import os
import threading
from typing import Optional

from .logger import setup_logging, logger
from .doc_indexer import DocIndexer
from .git_manager import GitDocumentationManager
from .indexing_state import IndexingState


mcp = FastMCP(name="zk-doc-server")

# Module-level doc indexer instance
_indexer = None
_indexer_lock = threading.Lock()


def _is_indexing_in_progress() -> bool:
    """
    Checks if documentation indexing is currently in progress.
    This function checks the persisted indexing state from IndexingState.

    Returns:
        True if indexing is in progress, False otherwise.
    """
    with _indexer_lock:
        if _indexer is not None:
            return _indexer.is_indexing_in_progress()
    return False


def _get_indexer():
    """Get or create the document indexer instance.

    Thread-safe getter that returns None if indexing is not yet complete.
    """
    global _indexer

    with _indexer_lock:
        if _indexer is None:
            _indexer = DocIndexer.from_env()

        # Return None if indexing is still in progress
        if _indexer.is_indexing_in_progress():
            return None

        return _indexer


def _extract_title_from_content(content: str) -> str:
    """Extract the first heading from content as title."""
    lines = content.split('\n')
    for line in lines:
        if line.startswith('#'):
            # Remove markdown heading markers
            return line.lstrip('#').strip()
    # If no heading found, use first 50 chars
    return content[:50] + "..." if len(content) > 50 else content


def _distance_to_relevance(distance: float) -> float:
    """Convert ChromaDB distance to relevance score (0-1)."""
    # ChromaDB distances are typically 0-2, where 0 is most similar
    # Convert to 0-1 scale where 1 is most relevant
    return max(0.0, 1.0 - distance)


def search_zk_docs_impl(
    query: str,
    limit: int = 5,
    min_relevance: float = 0.0,
    category: Optional[str] = None
) -> dict:
    """Search ZK documentation for relevant content - core implementation.

    This function contains the actual search logic and is exposed as a testable
    function. It performs semantic search on indexed ZK documentation using ChromaDB.

    The separation between search_zk_docs_impl() and search_zk_docs() exists because:
    - search_zk_docs_impl() contains the business logic and is testable directly
    - search_zk_docs() is decorated with @mcp.tool() to expose it as an MCP tool
    - FastMCP's @mcp.tool() decorator wraps the function, making direct testing difficult
    - By splitting them, we can test the implementation without relying on MCP internals

    Args:
        query: Search query string (required)
        limit: Maximum number of results to return (default: 5, range: 1-20)
        min_relevance: Minimum relevance score threshold (default: 0.0, range: 0-1)
        category: Optional filter for document categories (e.g., "tutorial", "reference", "guide")

    Returns:
        Dictionary with search results including:
        - indexing_status: Current status of documentation indexing ("in_progress", "complete", "error")
        - results: List of matching documents with scores
        - total_found: Number of results returned
        - query_time_ms: Query execution time in milliseconds
        - message: Optional user-friendly message for in_progress status
    """
    start_time = time.time()

    # Parameter validation
    if not query or not isinstance(query, str):
        return {
            "indexing_status": "error",
            "error": "Invalid parameter: 'query' is required and cannot be empty."
        }

    if not isinstance(limit, int) or limit < 1 or limit > 20:
        return {
            "indexing_status": "error",
            "error": "Invalid parameter: 'limit' must be between 1 and 20."
        }

    if not isinstance(min_relevance, (int, float)) or min_relevance < 0 or min_relevance > 1:
        return {
            "indexing_status": "error",
            "error": "Invalid parameter: 'min_relevance' must be between 0 and 1."
        }

    # Validate category if provided
    if category is not None and not isinstance(category, str):
        return {
            "indexing_status": "error",
            "error": "Invalid parameter: 'category' must be a string."
        }

    # Early check for indexing status
    if _is_indexing_in_progress():
        return {
            "indexing_status": "in_progress",
            "message": "The documentation is currently being indexed. This may take up to 10 minutes. Please try your search again in a few moments. Do not restart the server as this will reset the indexing process.",
            "results": [],
            "total_found": 0,
            "query_time_ms": int((time.time() - start_time) * 1000)
        }

    try:
        # Get the indexer instance (non-blocking)
        indexer = _get_indexer()

        if indexer is None:
            # If the indexer is None at this point, it means indexing is not complete,
            # and a search cannot be performed. This is treated as an error state
            # because _is_indexing_in_progress() should have caught the 'in_progress' state.
            return {
                "indexing_status": "error",
                "results": [],
                "total_found": 0,
                "query_time_ms": int((time.time() - start_time) * 1000),
                "error": "Documentation indexer is not available. Please check server logs for details."
            }

        # Perform the search with ChromaDB
        search_results = indexer.search(query, n_results=limit)

        # Format results according to spec
        formatted_results = []
        total_found = 0

        if search_results.get('results'):
            for result in search_results['results']:
                # Convert distance to relevance score
                relevance_score = _distance_to_relevance(result['distance'])

                # Filter by minimum relevance
                if relevance_score < min_relevance:
                    continue

                # Extract metadata
                metadata = result.get('metadata', {})
                file_path = metadata.get('source', '')

                # Filter by category if provided
                if category:
                    # Simple category extraction from file path
                    # e.g., "/path/to/tutorial/file.md" -> "tutorial"
                    path_parts = file_path.split('/')
                    doc_category = path_parts[-2] if len(path_parts) > 1 else ""
                    if category.lower() != doc_category.lower():
                        continue

                # Extract or derive title from content
                content = result.get('document', '')
                title = _extract_title_from_content(content)

                formatted_results.append({
                    "content": content,
                    "file_path": file_path,
                    "title": title,
                    "category": category or "",
                    "relevance_score": round(relevance_score, 4)
                })
                total_found += 1

        query_time_ms = int((time.time() - start_time) * 1000)

        return {
            "indexing_status": "complete",
            "results": formatted_results,
            "total_found": total_found,
            "query_time_ms": query_time_ms
        }

    except ImportError as e:
        return {
            "indexing_status": "error",
            "error": "Search dependencies not available",
            "details": str(e)
        }
    except FileNotFoundError as e:
        return {
            "indexing_status": "error",
            "error": "Documentation path not found",
            "details": str(e)
        }
    except Exception as e:
        return {
            "indexing_status": "error",
            "error": "Internal server error during document search",
            "details": str(e)
        }


@mcp.tool()
def search_zk_docs(
    query: str,
    limit: int = 5,
    min_relevance: float = 0.0,
    category: Optional[str] = None
) -> dict:
    """Search ZK documentation for relevant content.

    This is the MCP tool entry point for searching. It delegates to search_zk_docs_impl()
    which contains the actual implementation. See search_zk_docs_impl() docstring for
    explanation of why they are separated.

    Args:
        query: Search query string (required)
        limit: Maximum number of results to return (default: 5, range: 1-20)
        min_relevance: Minimum relevance score threshold (default: 0.0, range: 0-1)
        category: Optional filter for document categories (e.g., "tutorial", "reference", "guide")

    Returns:
        Dictionary with search results including relevance scores
    """
    return search_zk_docs_impl(query, limit, min_relevance, category)



def _prepare_doc_source(indexer: DocIndexer) -> Optional[str]:
    """
    Prepares the documentation source (Git or local).

    Args:
        indexer: The DocIndexer instance.

    Returns:
        The current commit hash (None if not using Git or if Git setup failed).
    """
    use_git = os.getenv("ZK_DOC_USE_GIT", "true").lower() == "true"

    if not use_git:
        logger.info("ZK_DOC_USE_GIT is false. Using local documentation.")
        return None

    git_manager = GitDocumentationManager.from_env()
    if not git_manager.ensure_repo_exists():
        logger.error("Failed to clone or find the repository. Aborting indexing.")
        return None

    indexer.doc_path = git_manager.repo_path
    logger.info(f"Indexer is configured to use documentation from: {indexer.doc_path}")

    git_manager.pull_latest()
    current_commit = git_manager.get_last_commit_hash()
    return current_commit


def _decide_and_perform_indexing(indexer: DocIndexer, current_commit: Optional[str]):
    """
    Orchestrates the indexing process by calling DocIndexer.index_docs().

    DocIndexer is responsible for determining indexing mode and executing it.
    This function delegates all indexing decisions to DocIndexer and handles
    post-indexing logging.

    Args:
        indexer: The DocIndexer instance.
        current_commit: The current commit hash from the documentation source (None if not using Git).
    """
    use_git = os.getenv("ZK_DOC_USE_GIT", "true").lower() == "true"
    commit_to_index = current_commit if use_git else "local_docs"

    # DocIndexer.index_docs() handles all mode determination internally
    indexer.index_docs(commit_to_index)

    # Log completion
    if use_git:
        logger.info(f"Indexing complete for commit {current_commit[:7]}.")
    else:
        logger.info("Indexing complete for local documentation.")


def index_documentation():
    """
    Manages the documentation indexing process by orchestrating the preparation of the
    doc source and the decision to index.

    This function is designed to run in a background thread to avoid blocking server startup.
    """
    global _indexer

    try:
        with _indexer_lock:
            _indexer = DocIndexer.from_env()

        current_commit = _prepare_doc_source(_indexer)
        _decide_and_perform_indexing(_indexer, current_commit)

        info = _indexer.get_index_info()
        logger.info(f"Current index info: {info}")
        logger.info("Documentation indexing completed successfully.")

    except ImportError as e:
        logger.error(f"Search dependencies not available: {e}")
        logger.error("To enable search, install: pip install zk-doc-mcp-server[search]")
    except FileNotFoundError:
        logger.error(f"Documentation path not found: {_indexer.doc_path if _indexer else 'not initialized'}")
        logger.error("Skipping documentation indexing.")
    except Exception as e:
        logger.error(f"Error during documentation indexing: {e}")
        import traceback
        logger.error(traceback.format_exc())


def run_init_command(force: bool = False) -> int:
    """
    Initialize documentation repository and pre-index it.

    This function clones/syncs the documentation repository and builds an index
    before starting the server, providing immediate query availability.

    Indexing mode is automatically selected based on state:
    - Force mode: If --force flag is set (delete and rebuild from scratch)
    - Full indexing: If no index exists or indexing was previously interrupted
    - Incremental update: If index exists and files have changed since last index
    - Skipped: If index is already up-to-date with current documentation

    Args:
        force: Force re-cloning of repository and re-indexing

    Returns:
        0 on success, 1 on failure
    """
    setup_logging()
    logger.info("Initializing ZK Documentation...")

    try:
        # Step 1: Prepare repository
        logger.info("Setting up documentation repository...")
        use_git = os.getenv("ZK_DOC_USE_GIT", "true").lower() == "true"

        if use_git:
            git_manager = GitDocumentationManager.from_env()

            if force:
                logger.info("Force flag set: will re-clone repository and rebuild index")

            if not git_manager.ensure_repo_exists():
                logger.error("Failed to clone or find the repository.")
                return 1

            logger.info(f"✓ Repository available at: {git_manager.repo_path}")

            # Sync to latest version
            git_manager.pull_latest()
            current_commit = git_manager.get_last_commit_hash()
            logger.info(f"✓ Repository is up to date (commit: {current_commit[:7]})")
        else:
            logger.info("ZK_DOC_USE_GIT is false. Using local documentation.")
            current_commit = None

        # Step 2: Create indexer with force_reindex flag
        logger.info("Checking indexing state...")
        indexer = DocIndexer(
            doc_path=os.getenv("ZK_DOC_SRC_DIR", str(__import__('pathlib').Path.home() / ".zk-doc-mcp" / "repo")),
            persist_dir=os.getenv("ZK_DOC_VECTOR_DB_DIR", str(__import__('pathlib').Path.home() / ".zk-doc-mcp" / "chromadb")),
            force_reindex=force
        )

        # Step 3: Let DocIndexer determine and execute the indexing mode
        commit_to_index = current_commit if use_git else "local_docs"
        indexer.index_docs(commit_to_index)

        # Step 4: Report results
        info = indexer.get_index_info()
        logger.info(f"✓ Indexing complete. Index info: {info}")

        if use_git:
            logger.info(f"✓ Indexed for commit {current_commit[:7]}")
        else:
            logger.info("✓ Indexed local documentation")

        logger.info("✓ Initialization complete!")
        return 0

    except ImportError as e:
        logger.error(f"Search dependencies not available: {e}")
        logger.error("To enable search, install: pip install zk-doc-mcp-server[search]")
        return 1
    except FileNotFoundError as e:
        logger.error(f"Documentation path not found: {e}")
        return 1
    except Exception as e:
        logger.error(f"Error during initialization: {e}")
        import traceback
        logger.error(traceback.format_exc())
        return 1



def show_settings_impl() -> dict:
    """Display all configuration settings - core implementation.

    This function contains the actual logic and is testable directly.
    It is wrapped by show_settings() which is exposed as an MCP tool.

    Returns:
        Dictionary with settings and summary statistics.
    """
    import os
    from pathlib import Path

    # Define all settings with their metadata
    settings_config = [
        # Phase 1: Core Settings
        {
            "key": "ZK_DOC_SRC_DIR",
            "description": "Documentation source directory (Git repo or local docs)",
            "default": str(Path.home() / ".zk-doc-mcp" / "repo"),
            "type": "string"
        },
        {
            "key": "ZK_DOC_VECTOR_DB_DIR",
            "description": "Vector database directory for storing embeddings and search indices",
            "default": str(Path.home() / ".zk-doc-mcp" / "chroma_db"),
            "type": "string"
        },
        {
            "key": "ZK_DOC_FORCE_REINDEX",
            "description": "Force re-indexing of documentation",
            "default": "false",
            "type": "boolean"
        },
        {
            "key": "ZK_DOC_LOG_LEVEL",
            "description": "Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
            "default": "INFO",
            "type": "enum",
            "enum_values": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
        },
        # Phase 2: Git Integration Settings
        {
            "key": "ZK_DOC_USE_GIT",
            "description": "Enable Git synchronization for documentation",
            "default": "true",
            "type": "boolean"
        },
        {
            "key": "ZK_DOC_CLONE_METHOD",
            "description": "Git clone method (https or ssh)",
            "default": "https",
            "type": "enum",
            "enum_values": ["https", "ssh"]
        },
        {
            "key": "ZK_DOC_REPO_URL",
            "description": "Repository URL",
            "default": "https://github.com/zkoss/zkdoc.git",
            "type": "string"
        },
        {
            "key": "ZK_DOC_GIT_BRANCH",
            "description": "Git branch to pull from",
            "default": "master",
            "type": "string"
        },
        # Phase 3: Feedback Settings
        {
            "key": "ZK_DOC_FEEDBACK_ENABLED",
            "description": "Enable feedback collection for search improvements",
            "default": "true",
            "type": "boolean"
        },
        {
            "key": "ZK_DOC_FEEDBACK_RETENTION_DAYS",
            "description": "Days to retain local feedback files",
            "default": "90",
            "type": "integer"
        },
        {
            "key": "ZK_DOC_FEEDBACK_GITHUB_REPO",
            "description": "GitHub repository for feedback issues (built-in)",
            "default": "zkoss/zkdoc",
            "type": "string"
        },
    ]

    # Build response settings
    settings = []
    configured_count = 0

    for config in settings_config:
        key = config["key"]
        default_value = config["default"]
        current_value = os.getenv(key, default_value)

        # Count configured settings (non-default)
        if current_value != default_value:
            configured_count += 1

        # Build setting entry
        setting = {
            "key": key,
            "description": config["description"],
            "default_value": default_value,
            "current_value": current_value,
            "type": config["type"]
        }

        # Add enum_values if present
        if "enum_values" in config:
            setting["enum_values"] = config["enum_values"]

        settings.append(setting)

    # Build summary
    summary = {
        "total_settings": len(settings_config),
        "configured_settings": configured_count,
        "default_settings": len(settings_config) - configured_count
    }

    return {
        "settings": settings,
        "summary": summary
    }


@mcp.tool()
def show_settings() -> dict:
    """Display all configuration settings for the ZK Documentation MCP Server.

    This tool enables users and administrators to inspect the current server
    configuration, including all environment variables and their effective values.
    It helps with debugging, configuration verification, and discoverability of
    available settings.

    Returns:
        Dictionary with settings and summary statistics.
    """
    return show_settings_impl()


@mcp.tool()
def submit_feedback(
    query: str,
    results: list,
    expected: str,
    comments: str = None
) -> dict:
    """Submit feedback about search results to improve documentation.

    When search results don't meet user expectations, this tool captures feedback
    that helps the documentation team understand gaps and improve content.

    Feedback is:
    - Always saved locally to ~/.zk-doc-mcp/feedback/
    - Automatically submitted as GitHub issue to https://github.com/zkoss/zkdoc/issues
    - Non-blocking (returns immediately while GitHub submission happens async)

    Args:
        query: The search query that produced unsatisfactory results
        results: List of search results returned (each with title, file_path, content)
        expected: What the user expected to find
        comments: Optional additional context about why results don't match

    Returns:
        Dictionary with submission status:
        - success: Whether feedback was saved
        - feedback_id: Unique identifier for this feedback
        - local_path: Where feedback was saved locally
        - github_issue_url: GitHub issue URL (if successfully created)
        - message: Status message
    """
    from .feedback_manager import get_feedback_manager

    feedback_manager = get_feedback_manager()
    return feedback_manager.submit_feedback(query, results, expected, comments)


def main():
    """Run the MCP server."""
    setup_logging()
    logger.info("Starting ZK Documentation MCP Server...")

    # Start indexing in a background thread to avoid blocking server startup
    indexing_thread = threading.Thread(
        target=index_documentation,
        name="IndexingThread",
        daemon=True  # Thread will exit when main program exits
    )
    indexing_thread.start()
    logger.info("Started background indexing thread. Server is ready to accept connections.")

    # Start the MCP server (this blocks)
    mcp.run()


if __name__ == "__main__":
    main()
