# THIS FUNCTIONALITY REMAINS EXPERIMENTAL

import os
import json
import logging
import asyncio
from typing import Optional

import httpx
from mcp.server.fastmcp import FastMCP


API_URL = os.environ.get("REVYOPS_API_URL", "https://app.revyops.com").rstrip("/")

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("revyops-mcp")


_clients: dict[str, dict] = {}
_default_client: dict | None = None
_initialized = False
_init_lock: asyncio.Lock | None = None


def _parse_api_keys() -> list[str]:
    raw = os.environ.get("REVYOPS_API_KEYS", "")
    if not raw:
        # Legacy single-key format
        single = os.environ.get("REVYOPS_API_KEY", "")
        return [single] if single else []
    return [k.strip() for k in raw.replace("\n", ",").split(",") if k.strip()]


async def _discover_client(api_key: str) -> dict | None:
    """Call /api/public/whoami to resolve an API key to client info."""
    try:
        async with httpx.AsyncClient(timeout=15) as client:
            resp = await client.get(
                f"{API_URL}/api/public/whoami",
                headers={"X-API-KEY": api_key},
            )
            if resp.status_code == 200:
                data = resp.json()
                return {
                    "name": data.get("client_name", data.get("client_id", "Unknown")),
                    "client_id": data["client_id"],
                    "api_key": api_key,
                }
            else:
                logger.warning("whoami failed (%s) for key ...%s", resp.status_code, api_key[-4:])
    except Exception as e:
        logger.warning("Could not reach %s/api/public/whoami: %s", API_URL, e)
    return None


async def _init_clients():
    """Discover all clients from API keys on first tool call.

    Uses an asyncio.Lock so concurrent first calls wait rather than race.
    Only sets _initialized after at least one client is resolved, so a
    transient network failure on startup allows retry on the next call.
    """
    global _initialized, _default_client, _init_lock

    if _initialized:
        return

    # Lazy-create lock inside the running event loop (safe on Python 3.10+)
    if _init_lock is None:
        _init_lock = asyncio.Lock()

    async with _init_lock:
        # Double-check after acquiring lock
        if _initialized:
            return

        keys = _parse_api_keys()
        if not keys:
            logger.warning("No API keys configured. Set REVYOPS_API_KEYS env var.")
            return

        # Legacy fallback: if REVYOPS_CLIENT_ID is set, use it directly
        legacy_id = os.environ.get("REVYOPS_CLIENT_ID", "")
        if legacy_id and len(keys) == 1:
            entry = {"name": "Default", "client_id": legacy_id, "api_key": keys[0]}
            _clients["default"] = entry
            _default_client = entry
            _initialized = True
            logger.info("Loaded legacy client: %s", legacy_id)
            return

        # Auto-discover all clients
        tasks = [_discover_client(key) for key in keys]
        results = await asyncio.gather(*tasks)
        for result in results:
            if result:
                # Use client_id as registry key to avoid name collisions
                registry_key = result["client_id"]
                _clients[registry_key] = result
                logger.info("Discovered client: %s (%s)", result["name"], result["client_id"])

        if _clients:
            _default_client = next(iter(_clients.values()))
            _initialized = True
        else:
            # Don't set _initialized — allow retry on next call
            logger.warning("Client discovery failed for all keys. Will retry on next tool call.")


def _resolve_client(client_name: Optional[str] = None) -> dict:
    """Resolve a client name to its config. Case-insensitive, partial matching.

    When only one client is configured, it is returned automatically.
    When multiple clients exist, client_name is required.
    """
    if not _clients:
        raise ValueError(
            "No clients available. Check your REVYOPS_API_KEYS env var."
        )

    if not client_name:
        if len(_clients) == 1:
            return _default_client
        available = ", ".join(c["name"] for c in _clients.values())
        raise ValueError(
            f"Multiple clients available — specify client_name. Options: {available}"
        )

    key = client_name.lower()

    # Exact match by client_id (registry key)
    if key in _clients:
        return _clients[key]

    # Match by name or partial name/client_id
    for registry_key, entry in _clients.items():
        name_lower = entry["name"].lower()
        if key == name_lower or key in name_lower or key in registry_key:
            return entry

    available = ", ".join(c["name"] for c in _clients.values())
    raise ValueError(f"Client '{client_name}' not found. Available: {available}")


_http_client: httpx.AsyncClient | None = None


def _get_http_client() -> httpx.AsyncClient:
    global _http_client
    if _http_client is None:
        _http_client = httpx.AsyncClient(timeout=60)
    return _http_client


def _headers(client: dict) -> dict:
    return {"X-API-KEY": client["api_key"], "Content-Type": "application/json"}


async def _request(method: str, client: dict, path: str, **kwargs) -> dict:
    """Shared HTTP helper with user-friendly error messages."""
    url = f"{API_URL}{path}"
    try:
        resp = await getattr(_get_http_client(), method)(
            url, headers=_headers(client), **kwargs,
        )
        resp.raise_for_status()
        return resp.json()
    except httpx.HTTPStatusError as e:
        status = e.response.status_code
        if status == 401:
            msg = "Authentication failed. Your API key may be invalid or expired."
        elif status == 403:
            msg = "Access denied. Your API key may not have permission for this action."
        elif status == 404:
            msg = f"Not found: {path}"
        elif status >= 500:
            msg = "RevyOps server error. Please try again in a moment."
        else:
            msg = f"Request failed ({status}): {e.response.text[:200]}"
        raise ValueError(msg) from None
    except httpx.ConnectError:
        raise ValueError(f"Cannot connect to {API_URL}. Check your network or REVYOPS_API_URL.") from None
    except httpx.TimeoutException:
        raise ValueError("Request timed out. The server may be under heavy load.") from None


async def _get(client: dict, path: str, params: dict | None = None) -> dict:
    return await _request("get", client, path, params=params)


async def _post(client: dict, path: str, payload: dict | None = None) -> dict:
    return await _request("post", client, path, json=payload or {})


async def _patch(client: dict, path: str, payload: dict) -> dict:
    return await _request("patch", client, path, json=payload)


mcp = FastMCP(
    "RevyOps",
    instructions=(
        "Access RevyOps campaign reporting, contacts, companies, and email data. "
        "Multiple client workspaces may be available. Use list_clients to see them, "
        "then pass the client name to any tool. If only one client is configured, "
        "it is used automatically."
    ),
)



@mcp.tool()
async def list_clients() -> str:
    """List all available RevyOps client workspaces.

    Call this first to see which clients you can access.
    Use the returned names when calling other tools.
    """
    await _init_clients()
    if not _clients:
        return json.dumps({"error": "No clients configured. Set REVYOPS_API_KEYS."})

    clients = []
    for entry in _clients.values():
        clients.append({
            "name": entry["name"],
            "client_id": entry["client_id"],
            "is_default": entry is _default_client,
        })
    return json.dumps({"clients": clients, "total": len(clients)}, indent=2)



@mcp.tool()
async def get_campaigns(
    client_name: Optional[str] = None,
    date_from: Optional[str] = None,
    date_to: Optional[str] = None,
    origin: Optional[str] = None,
    date_grouping: Optional[str] = None,
) -> str:
    """Get all campaigns with performance metrics for a client.

    Returns per-campaign: total emails sent, new/old prospects, replies,
    bounces, interested count, disposition breakdown, API stats (open rate,
    click rate, unsubscribes), and campaign start/end dates.

    Args:
        client_name: Client workspace name (from list_clients). Uses default if not set.
        date_from: Filter start date (YYYY-MM-DD).
        date_to: Filter end date (YYYY-MM-DD).
        origin: Filter by platform - "instantly", "smartlead", "emailbison", or "lemlist".
        date_grouping: "daily", "weekly", or "monthly" for timeseries data.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    filters = []
    if date_from:
        filters.append(f"email_timestamp__ge={date_from}")
    if date_to:
        filters.append(f"email_timestamp__le={date_to}")
    if origin:
        filters.append(f"origin__eq={origin}")

    payload = {}
    if filters:
        payload["filter_query_string"] = "&".join(filters)
    if date_grouping:
        payload["date_grouping"] = date_grouping

    result = await _post(client, f"/api/ui/heavy/{client['client_id']}/campaigns", payload)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_campaign_drilldown(
    campaign_id: str,
    origin: str,
    metric_type: str = "sends",
    client_name: Optional[str] = None,
    date_start: Optional[str] = None,
    date_end: Optional[str] = None,
    search: Optional[str] = None,
    group_by: str = "contacts",
    limit: int = 100,
) -> str:
    """Drill into a specific campaign to see individual contacts or companies.

    Args:
        campaign_id: The campaign identifier.
        origin: Platform - "instantly", "smartlead", "emailbison", or "lemlist".
        metric_type: What to show - "sends", "prospects", "replies", "interested", or "bounces".
        client_name: Client workspace name.
        date_start: Filter start date (YYYY-MM-DD).
        date_end: Filter end date (YYYY-MM-DD).
        search: Search by email, name, or company.
        group_by: Group results by "contacts" or "companies".
        limit: Max results (default 100, max 5000).
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {
        "batch": [{"metric_type": metric_type, "campaign_id": campaign_id, "origin": origin}],
        "group_by": group_by,
        "limit": min(limit, 5000),
    }
    if date_start:
        payload["date_start"] = date_start
    if date_end:
        payload["date_end"] = date_end
    if search:
        payload["search"] = search

    result = await _post(client, f"/api/ui/heavy/{client['client_id']}/campaign-drilldown", payload)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_email_thread(
    contact_id: str,
    client_name: Optional[str] = None,
    campaign_id: Optional[str] = None,
    origin: Optional[str] = None,
) -> str:
    """Get the full email thread between your team and a contact.

    Args:
        contact_id: The contact's ID in RevyOps.
        client_name: Client workspace name.
        campaign_id: Optional - filter to a specific campaign.
        origin: Optional - filter to a specific platform.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    params = {"contact_id": contact_id}
    if campaign_id:
        params["campaign_id"] = campaign_id
    if origin:
        params["origin"] = origin

    result = await _get(client, f"/api/ui/heavy/{client['client_id']}/campaign-drilldown/thread", params)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_deliverability(
    client_name: Optional[str] = None,
    date_start: Optional[str] = None,
    date_end: Optional[str] = None,
) -> str:
    """Get email deliverability metrics broken down by sending domain and inbox.

    Args:
        client_name: Client workspace name.
        date_start: Filter start date (YYYY-MM-DD).
        date_end: Filter end date (YYYY-MM-DD).
    """
    await _init_clients()
    client = _resolve_client(client_name)
    params = {}
    if date_start:
        params["campaign_date_start__ge"] = date_start
    if date_end:
        params["campaign_date_end__le"] = date_end

    result = await _get(client, f"/api/ui/heavy/{client['client_id']}/deliverability", params)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_campaign_stats(
    client_name: Optional[str] = None,
    date_start: Optional[str] = None,
    date_end: Optional[str] = None,
) -> str:
    """Get high-level campaign statistics for a client.

    Args:
        client_name: Client workspace name.
        date_start: Filter start date (YYYY-MM-DD).
        date_end: Filter end date (YYYY-MM-DD).
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {"client_ids": [client["client_id"]]}
    if date_start:
        payload["campaign_date_start"] = date_start
    if date_end:
        payload["campaign_date_end"] = date_end

    result = await _post(client, "/api/ui/campaigns-stats", payload)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_contacts_stats(
    client_name: Optional[str] = None,
    date_start: Optional[str] = None,
    date_end: Optional[str] = None,
) -> str:
    """Get contact-level aggregate statistics for a client.

    Args:
        client_name: Client workspace name.
        date_start: Filter start (ISO datetime).
        date_end: Filter end (ISO datetime).
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {"client_ids": [client["client_id"]]}
    if date_start or date_end:
        payload["date_range"] = {}
        if date_start:
            payload["date_range"]["start"] = date_start
        if date_end:
            payload["date_range"]["end"] = date_end

    result = await _post(client, "/api/ui/contacts-stats", payload)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_variable_report_variables(
    client_name: Optional[str] = None,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None,
) -> str:
    """Get available tracking variables and campaigns for A/B reporting.

    Args:
        client_name: Client workspace name.
        start_date: Filter start (ISO datetime).
        end_date: Filter end (ISO datetime).
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {"client_ids": [client["client_id"]]}
    if start_date:
        payload["start_date"] = start_date
    if end_date:
        payload["end_date"] = end_date

    result = await _post(client, "/api/ui/variable-report/variables", payload)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_variable_report(
    campaign_origin_pairs: list[list[str]],
    variable_fields: list[str],
    variable_values: list[list[str]],
    client_name: Optional[str] = None,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None,
) -> str:
    """Generate an A/B test style report grouped by tracking variables.

    Args:
        campaign_origin_pairs: List of [campaign_id, origin] pairs.
        variable_fields: Tracking field names to group by.
        variable_values: Allowed values for each field.
        client_name: Client workspace name.
        start_date: Filter start (ISO datetime).
        end_date: Filter end (ISO datetime).
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {
        "client_ids": [client["client_id"]],
        "campaign_origin_pairs": campaign_origin_pairs,
        "variable_fields": variable_fields,
        "variable_values": variable_values,
    }
    if start_date:
        payload["start_date"] = start_date
    if end_date:
        payload["end_date"] = end_date

    result = await _post(client, "/api/ui/variable-report/report", payload)
    return json.dumps(result, indent=2, default=str)



@mcp.tool()
async def add_campaign_note(
    campaign_id: str, origin: str, note: str, client_name: Optional[str] = None,
) -> str:
    """Add or update a note on a campaign.

    Args:
        campaign_id: The campaign identifier.
        origin: Platform - "instantly", "smartlead", "emailbison", or "lemlist".
        note: The note text to set.
        client_name: Client workspace name.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {"origin": origin, "old_value": "", "new_value": note, "force": True}
    result = await _post(client, f"/api/ui/{client['client_id']}/campaigns/{campaign_id}/note", payload)
    return json.dumps(result, default=str)


@mcp.tool()
async def archive_campaign(
    campaign_id: str, origin: str, archived: bool = True, client_name: Optional[str] = None,
) -> str:
    """Archive or unarchive a campaign.

    Args:
        campaign_id: The campaign identifier.
        origin: Platform - "instantly", "smartlead", "emailbison", or "lemlist".
        archived: True to archive, False to unarchive.
        client_name: Client workspace name.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {"origin": origin, "archived": archived}
    result = await _post(client, f"/api/ui/{client['client_id']}/campaigns/{campaign_id}/archive", payload)
    return json.dumps(result, default=str)



@mcp.tool()
async def search_contacts(
    client_name: Optional[str] = None, email: Optional[str] = None,
    first_name: Optional[str] = None, last_name: Optional[str] = None,
    domain: Optional[str] = None, linkedin_url: Optional[str] = None,
) -> str:
    """Search for contacts by email, name, domain, or LinkedIn URL.

    Args:
        client_name: Client workspace name.
        email: Filter by email address.
        first_name: Filter by first name.
        last_name: Filter by last name.
        domain: Filter by company domain.
        linkedin_url: Filter by LinkedIn URL.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    params = {}
    if email: params["email"] = email
    if first_name: params["firstname"] = first_name
    if last_name: params["lastname"] = last_name
    if domain: params["domain"] = domain
    if linkedin_url: params["linkedin_url"] = linkedin_url

    result = await _get(client, "/api/public/v2/contacts", params)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_contact(contact_id: str, client_name: Optional[str] = None) -> str:
    """Get full details for a specific contact including custom fields.

    Args:
        contact_id: The contact's ID.
        client_name: Client workspace name.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    result = await _get(client, f"/api/public/v2/contacts/{contact_id}")
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def update_contact(
    contact_id: str, client_name: Optional[str] = None,
    first_name: Optional[str] = None, last_name: Optional[str] = None,
    title: Optional[str] = None, phone: Optional[str] = None,
    status: Optional[str] = None, custom_fields: Optional[dict] = None,
) -> str:
    """Update a contact's fields.

    Args:
        contact_id: The contact's ID.
        client_name: Client workspace name.
        first_name: New first name.
        last_name: New last name.
        title: New job title.
        phone: New phone number.
        status: New contact status.
        custom_fields: Dict of custom field updates.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {}
    if first_name is not None: payload["first_name"] = first_name
    if last_name is not None: payload["last_name"] = last_name
    if title is not None: payload["title"] = title
    if phone is not None: payload["phone"] = phone
    if status is not None: payload["status"] = status
    if custom_fields: payload["custom_fields"] = custom_fields

    result = await _patch(client, f"/api/public/v2/contacts/{contact_id}", payload)
    return json.dumps(result, indent=2, default=str)



@mcp.tool()
async def search_companies(
    client_name: Optional[str] = None, domain: Optional[str] = None, name: Optional[str] = None,
) -> str:
    """Search for companies by domain or name.

    Args:
        client_name: Client workspace name.
        domain: Filter by company domain.
        name: Filter by company name.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    params = {}
    if domain: params["domain"] = domain
    if name: params["name"] = name

    result = await _get(client, "/api/public/v2/companies", params)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def get_company(company_id: str, client_name: Optional[str] = None) -> str:
    """Get full details for a specific company including custom fields.

    Args:
        company_id: The company's ID.
        client_name: Client workspace name.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    result = await _get(client, f"/api/public/v2/companies/{company_id}")
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def update_company(
    company_id: str, client_name: Optional[str] = None,
    name: Optional[str] = None, status: Optional[str] = None,
    custom_fields: Optional[dict] = None,
) -> str:
    """Update a company's fields.

    Args:
        company_id: The company's ID.
        client_name: Client workspace name.
        name: New company name.
        status: New company status.
        custom_fields: Dict of custom field updates.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {}
    if name is not None: payload["name"] = name
    if status is not None: payload["status"] = status
    if custom_fields: payload["custom_fields"] = custom_fields

    result = await _patch(client, f"/api/public/v2/companies/{company_id}", payload)
    return json.dumps(result, indent=2, default=str)



@mcp.tool()
async def search_emails(
    client_name: Optional[str] = None, campaign_id: Optional[str] = None,
    contact_email: Optional[str] = None, email_type: Optional[str] = None,
    limit: int = 50,
) -> str:
    """Search email records by campaign, contact, or type.

    Args:
        client_name: Client workspace name.
        campaign_id: Filter by campaign.
        contact_email: Filter by contact email address.
        email_type: Filter by type - "INBOUND", "OUTBOUND", or "BOUNCED".
        limit: Max results (default 50).
    """
    await _init_clients()
    client = _resolve_client(client_name)
    params = {"limit": limit}
    if campaign_id: params["campaign_id"] = campaign_id
    if contact_email: params["contact"] = contact_email
    if email_type: params["email_type"] = email_type

    result = await _get(client, "/api/public/emails", params)
    return json.dumps(result, indent=2, default=str)


@mcp.tool()
async def mark_email_interested(
    email_id: str, interested: bool = True, client_name: Optional[str] = None,
) -> str:
    """Mark an email as interested or not interested.

    Args:
        email_id: The email record ID.
        interested: True to mark interested, False to unmark.
        client_name: Client workspace name.
    """
    await _init_clients()
    client = _resolve_client(client_name)
    payload = {"email_id": email_id, "interested": interested}
    result = await _post(client, "/api/public/emails/set-interested", payload)
    return json.dumps(result, default=str)



@mcp.resource("revyops://help")
async def help_resource() -> str:
    """Overview of RevyOps and available data."""
    return """
# RevyOps - Outbound Campaign Analytics

RevyOps aggregates email campaign data from Instantly, SmartLead,
EmailBison, and Lemlist into unified reporting.

## Quick Start
- Use list_clients to see your available workspaces
- Use get_campaigns to see campaign performance
- Use get_campaign_drilldown to see individual contacts in a campaign
- Use get_deliverability to check sending domain health

## Key Concepts
- Campaign: An outbound email sequence on one platform (origin)
- Origin: The sending platform (instantly, smartlead, emailbison, lemlist)
- Interested: A reply flagged as showing buying intent
- Prospect: "New" = first-time contact, "Old" = re-engaged
"""


def main():
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
