"""MCP server for acsearch.info ancient coin auction archive.

Provides tools for searching auction records, retrieving lot details,
and researching price history — all through acsearch.info's Premium database.

Authentication:
    Set ACSEARCH_USERNAME and ACSEARCH_PASSWORD environment variables
    for your Premium account. Cookies are cached to minimize logins.

Usage:
    uv run acsearch-mcp                    # HTTP mode (default)
    uv run acsearch-mcp --transport stdio  # stdio mode (for Hermes/Claude)
"""

import functools
import json
import os
import traceback
from dataclasses import asdict
from typing import Any

from fastmcp import FastMCP

from .client import AcsearchClient
from .parser import parse_search_results, parse_lot_detail

# Category mapping
CATEGORIES = {
    "all": 0,
    "coins": "1-2",
    "ancient": 1,
    "modern": 2,
    "banknotes": 3,
    "orders": 4,
    "literature": 8,
    "antiquities": 9,
}


def _get_client() -> AcsearchClient:
    """Get or create an authenticated client."""
    username = os.environ.get("ACSEARCH_USERNAME", "")
    password = os.environ.get("ACSEARCH_PASSWORD", "")
    if not username or not password:
        raise ValueError(
            "ACSEARCH_USERNAME and ACSEARCH_PASSWORD environment variables "
            "must be set for your Premium account."
        )
    return AcsearchClient(username=username, password=password)


def _mcp_tool_error_handler(fn):
    """Decorator: catch exceptions and return user-friendly error messages."""

    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except ValueError as e:
            return f"Configuration error: {e}"
        except Exception as e:
            tb = traceback.format_exc()
            # Show the last few lines of the traceback for relevant context
            tb_lines = tb.strip().split("\n")
            short_tb = "\n".join(tb_lines[-6:]) if len(tb_lines) > 6 else tb
            return f"Error: {type(e).__name__}: {e}\n\nTraceback:\n{short_tb}"

    return wrapper


mcp = FastMCP(
    "acsearch",
    instructions=(
        "acsearch.info auction archive for ancient coins. "
        "Use search() to find auction records with Boolean queries "
        "(supports *, \"...\", -exclude, (...) grouping). "
        "Use get_lot() for full details of a specific lot. "
        "Use price_history() to analyze realized prices over time. "
        "Requires a Premium account (€85/year)."
    ),
)


def _serialize(obj: Any) -> str:
    """Serialize dataclass objects to JSON."""
    if hasattr(obj, "__dataclass_fields__"):
        return json.dumps(asdict(obj), indent=2, ensure_ascii=False)
    return json.dumps(obj, indent=2, ensure_ascii=False, default=str)


@mcp.tool()
@_mcp_tool_error_handler
def search(
    query: str,
    category: str = "ancient",
    page: int = 1,
    currency: str = "usd",
    order: int = 0,
) -> str:
    """Search acsearch.info auction archive for coins.

    Uses Boolean full-text search with operators:
      *  = wildcard (auto-appended to words of 3+ chars)
      "..." = exact phrase match
      -word = exclude word
      (...) = group sub-queries

    Args:
        query: Search terms with optional operators. Examples:
            "Philip II tetradrachm"
            "Alexander* stater -bronze"
            "cistophorus Ephes*"
        category: One of: all, coins, ancient (default), modern,
            banknotes, orders, literature, antiquities
        page: Page number (1-indexed, 100 results per page)
        currency: Price currency: usd (default), eur, chf, gbp
        order: Sort order: 0=relevance (default), 1=date desc,
            2=date asc, 3=price desc, 4=price asc

    Returns:
        JSON with total_results, page, results_per_page,
        time_seconds, and results array. Each result has:
        id, title, description, auction date, price (realized),
        image thumbnail URL, and a link to the full detail page.
    """
    client = _get_client()
    try:
        cat_id = CATEGORIES.get(category.lower(), 1)
        html = client.search(
            query=query,
            category=cat_id,
            page=page,
            currency=currency,
            order=order,
        )
        response = parse_search_results(html, query=query, page=page)
        return _serialize(response)
    finally:
        client.close()


@mcp.tool()
@_mcp_tool_error_handler
def get_lot(lot_id: str) -> str:
    """Get full details of a specific auction lot by its acsearch record ID.

    Args:
        lot_id: The acsearch record ID (e.g. "2529269"). Visible in
            search results as the 'id' field.

    Returns:
        JSON with full lot details: id, title, description,
        auction info, price, coin specs (weight, diameter, metal,
        denomination), images, bibliographic references (Le Rider,
        SNG, etc.), and provenance history.
    """
    client = _get_client()
    try:
        html = client.get_lot(lot_id)
        lot = parse_lot_detail(html)
        if lot is None:
            return f"No lot found with id={lot_id}"
        return _serialize(lot)
    finally:
        client.close()


@mcp.tool()
@_mcp_tool_error_handler
def price_history(
    query: str,
    category: str = "ancient",
    years: int = 20,
    currency: str = "usd",
) -> str:
    """Search for auction results and summarize realized prices over time.

    Searches the archive and groups results by year, extracting realized
    prices where available. Useful for market research and valuation.

    Args:
        query: Search terms (same Boolean syntax as search())
        category: One of: all, coins, ancient (default), modern, etc.
        years: How many years back to analyze (default: 20).
            Older results are filtered out.
        currency: Price currency: usd (default), eur, chf, gbp

    Returns:
        JSON summary with total results, results with prices,
        and per-year breakdown of average/min/max prices where available.
    """
    client = _get_client()
    try:
        cat_id = CATEGORIES.get(category.lower(), 1)

        # Search first page to get total count and initial results
        html = client.search(query=query, category=cat_id, currency=currency, order=1)
        response = parse_search_results(html, query=query, page=1)

        # Collect results from multiple pages (up to 3 pages = 300 results)
        all_results = list(response.results)
        total_pages = min(
            (response.total_results + 99) // 100,
            3,  # Cap at 3 pages to avoid excessive requests
        )

        for p in range(2, total_pages + 1):
            html = client.search(
                query=query, category=cat_id, page=p,
                currency=currency, order=1,
            )
            page_resp = parse_search_results(html, query=query, page=p)
            all_results.extend(page_resp.results)

        # Filter by date range
        from datetime import datetime
        cutoff_year = datetime.now().year - years

        year_data: dict[int, list[float]] = {}
        results_with_price = 0

        for r in all_results:
            try:
                year = int(r.auction.date.split(".")[-1])
            except (ValueError, AttributeError):
                continue

            if year < cutoff_year:
                continue

            # Parse realized price
            price_str = r.price.realized.strip()
            if price_str and price_str != "*":
                try:
                    # Strip currency symbols and thousand separators
                    price_num = float(
                        price_str
                        .replace("$", "").replace("€", "")
                        .replace("CHF", "").replace("£", "")
                        .replace("'", "").replace(",", "")
                        .strip()
                    )
                    if price_num > 0:
                        year_data.setdefault(year, []).append(price_num)
                        results_with_price += 1
                except ValueError:
                    pass

        # Build summary
        yearly_summary = {}
        for year in sorted(year_data.keys()):
            prices = year_data[year]
            yearly_summary[str(year)] = {
                "count": len(prices),
                "avg": round(sum(prices) / len(prices), 2),
                "min": round(min(prices), 2),
                "max": round(max(prices), 2),
            }

        return _serialize({
            "query": query,
            "total_results": response.total_results,
            "results_with_prices": results_with_price,
            "results_scanned": len(all_results),
            "currency": currency,
            "years_analyzed": years,
            "yearly_summary": yearly_summary,
            "note": (
                "Prices are realized hammer prices. "
                "Results without visible prices are excluded. "
                "Limited to first 300 results for performance."
            ),
        })
    finally:
        client.close()


@mcp.tool()
@_mcp_tool_error_handler
def find_comparables(
    query: str,
    category: str = "ancient",
    currency: str = "usd",
    max_results: int = 20,
) -> str:
    """Find comparable auction results for a coin — useful for market valuation.

    Searches with date-descending order to show most recent comps first.
    Extracts key fields: auction date, house, description, price, weight, diameter.

    Args:
        query: Coin description for comparables. Be specific:
            "Philip II tetradrachm Amphipolis Le Rider"
            "cistophorus Ephesus 2024"
            "Athens owl tetradrachm"
        category: One of: all, coins, ancient (default), modern, etc.
        currency: Price currency: usd (default), eur, chf, gbp
        max_results: Maximum number of results to return (default: 20)

    Returns:
        JSON array of comparable lots with id, title, description,
        date, price, and specs (weight, diameter, metal, denomination).
    """
    client = _get_client()
    try:
        cat_id = CATEGORIES.get(category.lower(), 1)

        # Use date-descending order for recent comps
        html = client.search(
            query=query, category=cat_id, currency=currency, order=1,
        )
        response = parse_search_results(html, query=query, page=1)

        # Trim to requested max
        results = response.results[:max_results]

        # For each result, fetch basic specs from the description text
        import re
        enriched = []
        for r in results:
            desc = r.description

            item = {
                "id": r.id,
                "title": r.title,
                "description": desc,
                "date": r.auction.date,
                "price": r.price.realized,
                "image": r.images[0].url if r.images else "",
                "url": r.url,
                "specs": {},
            }

            # Extract quick specs from description
            wt = re.search(r"(\d+\.?\d*)\s*g\b", desc)
            if wt:
                item["specs"]["weight"] = wt.group(0)

            dm = re.search(r"(\d+\.?\d*)\s*mm\b", desc)
            if dm:
                item["specs"]["diameter"] = dm.group(0)

            ax = re.search(r"(\d+)\s*h\b", desc)
            if ax:
                item["specs"]["die_axis"] = ax.group(0)

            for metal in ["AR", "AV", "AE", "EL", "BI"]:
                if re.search(rf"\b{metal}\b", desc):
                    item["specs"]["metal"] = metal
                    break

            enriched.append(item)

        return _serialize({
            "query": query,
            "total_available": response.total_results,
            "results_returned": len(enriched),
            "currency": currency,
            "comparables": enriched,
            "note": (
                "Results sorted by date (newest first). "
                "Prices are realized hammer prices (Premium account required). "
                "For full lot details including provenance and references, "
                "use get_lot() with the lot id."
            ),
        })
    finally:
        client.close()


def main():
    import argparse

    parser = argparse.ArgumentParser(description="acsearch MCP Server")
    parser.add_argument(
        "--transport",
        choices=["http", "stdio"],
        default="http",
        help="Transport protocol (default: http)",
    )
    parser.add_argument(
        "--host", default="127.0.0.1",
        help="Host to bind to (default: 127.0.0.1)",
    )
    parser.add_argument(
        "--port", type=int, default=8001,
        help="Port to bind to (default: 8001)",
    )
    args = parser.parse_args()

    if args.transport == "http":
        mcp.run(transport="http", host=args.host, port=args.port)
    else:
        mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
