"""Точка входа MCP-сервера с HTTP-транспортом (streamable-http).

VMK Data MCP Server предоставляет AI-агентам безопасный read-only доступ
к базе данных недвижимости vmk_data (PostgreSQL + pgvector).

Данные: ~35 полей объявлений (цена, площадь, район, метро, тип сделки, ...).
Поиск: семантический (vector 768d через Ollama) + полнотекстовый (FTS).
Язык: украинский (запросы переводятся агентом перед вызовом).
Зачем: AI-агент может искать квартиры/дома по описанию пользователя,
фильтровать по бюджету и району, показывать детали объявлений.
"""

import json
import logging
from contextlib import asynccontextmanager

import asyncpg
import httpx
from mcp.server import FastMCP
from starlette.requests import Request
from starlette.responses import FileResponse, Response

from vmk_data_mcp.config import settings
from vmk_data_mcp.db import close_pool, init_pool
from vmk_data_mcp.embedder import close_client
from vmk_data_mcp.images import resolve_image_path
from vmk_data_mcp.models import (
    GetListingInput,
    MetadataFilters,
    PaginationParams,
    SearchMetadataInput,
    SearchSimilarInput,
)
from vmk_data_mcp.tools import (
    describe_schema,
    get_listing_by_id,
    search_by_metadata,
    search_similar_listings,
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def _error_json(message: str) -> str:
    """Возвращает стандартизированный JSON с ошибкой."""
    return json.dumps({"error": message}, indent=2, ensure_ascii=False)


@asynccontextmanager
async def app_lifespan(server):
    """Жизненный цикл приложения — инициализация и закрытие ресурсов."""
    logger.info("Initializing DB pool ...")
    await init_pool()
    logger.info("DB pool ready.")
    yield
    logger.info("Shutting down ...")
    await close_pool()
    await close_client()
    logger.info("Shutdown complete.")


SERVER_INSTRUCTIONS = """\
# VMK Data MCP Server — инструкция для AI-агента

## Назначение
Этот сервер предоставляет инструменты для поиска объявлений о недвижимости
(квартиры, дома, аренда, продажа) из базы данных `vmk_data`.

## Язык
Все текстовые запросы (`query`) должны быть на **украинском языке**.
AI-агент переводит запрос пользователя перед вызовом инструмента.

## Два инструмента поиска — выбирай правильно

### search_similar_listings — векторный (семантический)
Используй, когда пользователь описывает **желания, атмосферу, качества**:
- "уютная квартира с ремонтом у метро"
- "светлая студия в центрі міста"
- "простора 3-кімнатна з балконом"

Запрос на украинском. Конкретно, без стоп-слов.
✓ Хорошо: "2-кімнатна квартира біля метро з ремонтом"
✗ Плохо:  "дай квартиру недорого"

### search_by_metadata — полнотекстовый (FTS)
Используй, когда пользователь называет **конкретные ключевые слова**:
- "Печерський район Арсенальна метро"
- "вул. Шевченка Львів оренда"
- "новобудова моноліт Солом'янський"

Запрос на украинском. Имена собственные и термины.
✓ Хорошо: "Печерський район Арсенальна метро"
✗ Плохо:  "хочу квартиру" (слишком общее — используй vector)

## Фильтры (общие для обоих инструментов)
- Обязательно указывай `currency` при `min_price`/`max_price`.
- `city`, `district`, `metro_station` — поиск по подстроке (ILIKE), регистр не важен.
- `rooms_count`: 0 = студия.
- Если результатов мало — убери 1–2 фильтра (обычно district или metro_station).

## Сортировка (только search_by_metadata)
- `relevance` — по релевантности FTS (по умолчанию)
- `price_asc`/`price_desc` — по цене
- `date_desc` — новые сверху
- `area_desc` — по площади

## Fallback при 0 результатов
1. Попробуй другой инструмент (vector → FTS или FTS → vector).
2. Убери 1–2 жёстких фильтра.
3. Упрости query — убери разговорные слова.
4. Проверь, что query на украинском.

## Пагинация
- `total` > `limit` → есть следующая страница.
- Следующая: `offset += limit`. Предыдущая: `offset -= limit` (≥ 0).
- `limit` от 1 до 100.

## Изображения
- Каждый результат содержит поле `images` — список фото с прямыми ссылками.
- Ссылки ведут на этот же MCP-сервер (`/images/<property_id>/<hash>.jpg`).
- В поиске отдаётся до 5 фото на объявление; `get_listing_by_id` возвращает все.

## describe_schema
Возвращает полное описание таблицы, гайды, примеры запросов и лучшие практики.
Вызывай, если не уверен какой инструмент выбрать или какие фильтры применить.
"""


mcp = FastMCP(
    settings.mcp_server_name,
    host="0.0.0.0",
    port=settings.mcp_port,
    lifespan=app_lifespan,
    instructions=SERVER_INSTRUCTIONS,
)

# ── Хостинг изображений ───────────────────────────────────────────────


@mcp.custom_route("/images/{image_path:path}", methods=["GET"])
async def serve_image(request: Request) -> Response:
    """Раздаёт файлы изображений из хранилища data_collector.

    Путь в URL соответствует относительному пути `local_path` из таблицы
    `property_images`, например `/images/1/<hash>.jpg`.
    """
    image_path = request.path_params.get("image_path", "")
    file_path = resolve_image_path(image_path)

    if file_path is None or not file_path.is_file():
        return Response("Image not found", status_code=404, media_type="text/plain")

    return FileResponse(file_path)


# ── Prompts (гайды для AI-агента) ───────────────────────────────────


@mcp.prompt()
def search_guide() -> str:
    """📘 Гайд: как эффективно искать объявления через VMK Data MCP."""
    return SERVER_INSTRUCTIONS


# ── Регистрация инструментов ────────────────────────────────────────


@mcp.tool()
async def search_similar_listings_tool(
    query: str,
    filters: MetadataFilters | None = None,
    pagination: PaginationParams | None = None,
) -> str:
    """🔍 Векторный (семантический) поиск объявлений по смыслу.

    Используй, когда пользователь описывает **желания, атмосферу, качества**
    («уютная квартира с ремонтом», «светлая студия у метро»).
    Не используй для точных ключевых слов (район, метро) — для этого
    используй `search_by_metadata`.

    **Важно:** запрос `query` должен быть на **украинском** языке.
    Формулируй конкретно, без разговорных стоп-слов.

    **Фильтры:** объект `MetadataFilters` — city, district, deal_type,
    rooms_count, min/max_price (с обязательной currency), building_type и др.
    `city`/`district`/`metro_station` — поиск по подстроке, регистр не важен.
    `rooms_count`: 0 = студия.

    **Пагинация:** объект `PaginationParams` с `limit` (1–100, умолч. 20) и
    `offset` (≥ 0). Следующая страница: `offset += limit`.

    **Fallback при 0 результатов:** сервер вернёт подсказку с рекомендациями
    (попробовать FTS, убрать фильтры, упростить query).
    """
    try:
        args = SearchSimilarInput(
            query=query,
            filters=filters or MetadataFilters(),
            pagination=pagination or PaginationParams(),
        )
        return await search_similar_listings(args)
    except httpx.HTTPError as e:
        logger.warning("Ollama error: %s", e)
        return _error_json(f"Сервис эмбеддингов недоступен: {e}")
    except (asyncpg.PostgresError, ValueError) as e:
        logger.warning("DB/validation error: %s", e)
        return _error_json(f"Ошибка при поиске: {e}")
    except Exception as e:
        logger.exception("Unexpected error in search_similar_listings")
        return _error_json(f"Неожиданная ошибка: {e}")


@mcp.tool()
async def search_by_metadata_tool(
    query: str,
    filters: MetadataFilters | None = None,
    pagination: PaginationParams | None = None,
    sort_by: str = "relevance",
) -> str:
    """📋 Полнотекстовый поиск + фильтры по метаданным.

    Используй, когда пользователь называет **конкретные ключевые слова**
    («Печерський район», «Арсенальна метро», «вул. Шевченка»).
    Работает через готовую FTS-колонку `search_vector` (украинский конфиг) + GIN-индекс.

    **Важно:** запрос `query` должен быть на **украинском** языке.
    Используй имена собственные и термины, а не разговорные описания.

    **Фильтры:** объект `MetadataFilters` — те же поля, что и для vector-поиска.
    Обязательно указывай `currency` при `min_price`/`max_price`.

    **Сортировка:**
    - `relevance` — по релевантности FTS (по умолчанию)
    - `price_asc`/`price_desc` — по цене
    - `date_desc` — новые сверху
    - `area_desc` — по площади

    **Пагинация:** объект `PaginationParams`.

    **Fallback при 0 результатов:** сервер вернёт подсказку с рекомендациями
    (попробовать vector search, убрать фильтры).
    """
    try:
        args = SearchMetadataInput(
            query=query,
            filters=filters or MetadataFilters(),
            pagination=pagination or PaginationParams(),
            sort_by=sort_by,
        )
        return await search_by_metadata(args)
    except (asyncpg.PostgresError, ValueError) as e:
        logger.warning("DB/validation error: %s", e)
        return _error_json(f"Ошибка при поиске: {e}")
    except Exception as e:
        logger.exception("Unexpected error in search_by_metadata")
        return _error_json(f"Неожиданная ошибка: {e}")


@mcp.tool()
async def get_listing_by_id_tool(listing_id: int) -> str:
    """📄 Получить полную карточку объявления по ID.

    Возвращает все пользовательские поля объявления.
    Если объявление не найдено — вернёт ошибку с подсказкой
    (проверить ID, возможен статус archived/removed).
    """
    try:
        args = GetListingInput(listing_id=listing_id)
        return await get_listing_by_id(args)
    except (asyncpg.PostgresError, ValueError) as e:
        logger.warning("DB/validation error: %s", e)
        return _error_json(f"Ошибка при получении объявления: {e}")
    except Exception as e:
        logger.exception("Unexpected error in get_listing_by_id")
        return _error_json(f"Неожиданная ошибка: {e}")


@mcp.tool()
async def describe_schema_tool() -> str:
    """ℹ️ Описание схемы данных и гайд по поиску.

    Возвращает:
    - описание таблицы `property_listings` (поля, типы, enum-значения)
    - гайд когда использовать vector/FTS
    - примеры хороших и плохих запросов
    - лучшие комбинации фильтров
    - стратегию fallback и пагинации

    Вызывай, если не уверен какой инструмент выбрать или какие фильтры применить.
    """
    try:
        return await describe_schema()
    except Exception as e:
        logger.exception("Unexpected error in describe_schema")
        return _error_json(f"Неожиданная ошибка: {e}")


if __name__ == "__main__":
    mcp.run(transport="streamable-http")
