"""Affinity CRM tools for ecs-mcp."""

import os
from typing import Any

import httpx

from ecs_mcp import mcp

AFFINITY_BASE_URL = "https://api.affinity.co"


# ---------------------------------------------------------------------------
# Affinity API client helpers
# ---------------------------------------------------------------------------

def _get_api_key() -> str:
    key = os.environ.get("AFFINITY_API_KEY", "").strip()
    if not key:
        raise ValueError(
            "AFFINITY_API_KEY environment variable is not set. "
            "Add it to your Claude Desktop config under 'env'."
        )
    return key


def _client() -> httpx.AsyncClient:
    return httpx.AsyncClient(
        base_url=AFFINITY_BASE_URL,
        auth=("", _get_api_key()),
        headers={"Content-Type": "application/json"},
        timeout=30.0,
    )


async def _find_list_by_name(client: httpx.AsyncClient, list_name: str) -> dict[str, Any] | None:
    response = await client.get("/lists")
    response.raise_for_status()
    for lst in response.json():
        if lst.get("name", "").lower() == list_name.lower():
            return lst
    return None


def _normalize(name: str) -> str:
    """Lowercase and collapse punctuation/whitespace for loose name comparison."""
    return " ".join("".join(c if c.isalnum() else ' ' for c in name.lower()).split())


def _names_match(a: str, b: str) -> bool:
    na, nb = _normalize(a), _normalize(b)
    return na == nb or na in nb or nb in na


async def _find_org_in_list(
    client: httpx.AsyncClient,
    list_id: int,
    list_type: int,
    org_name: str,
) -> tuple[dict[str, Any] | None, list[str]]:
    """Search for a duplicate organization within the target list (tenant-safe).

    Fetches existing list entries, resolves their organization names, and
    matches against org_name. All results are guaranteed to belong to our tenant.
    Returns (matched_org, []) — empty warnings list kept for API compatibility.
    """
    # Collect all organization IDs already in the list (paginated)
    org_ids: set[int] = set()
    page_token: str | None = None
    while True:
        params: dict[str, Any] = {"page_size": 500}
        if page_token:
            params["page_token"] = page_token
        response = await client.get(f"/lists/{list_id}/list-entries", params=params)
        response.raise_for_status()
        data = response.json()
        entries = data if isinstance(data, list) else data.get("list_entries", [])
        for entry in entries:
            entity = entry.get("entity") or {}
            if list_type == LIST_TYPE_OPPORTUNITY:
                oid = entity.get("organization_id")
            else:
                oid = entity.get("id") or entry.get("entity_id")
            if oid:
                org_ids.add(int(oid))
        page_token = data.get("next_page_token") if isinstance(data, dict) else None
        if not page_token:
            break

    # Fetch each org and collect full objects for matching
    orgs_in_list: list[dict[str, Any]] = []
    for oid in org_ids:
        response = await client.get(f"/organizations/{oid}")
        if response.status_code == 200:
            orgs_in_list.append(response.json())

    # Exact match first
    for org in orgs_in_list:
        if org.get("name", "").lower() == org_name.lower():
            return org, []

    # Near match (handles typos like hyphens, punctuation differences)
    for org in orgs_in_list:
        if _names_match(org.get("name", ""), org_name):
            return org, []

    return None, []


async def _create_organization(
    client: httpx.AsyncClient, org_name: str, domain: str = ""
) -> dict[str, Any]:
    if not domain:
        # domain is required by the Affinity API — derive a best-effort slug
        domain = "".join(c for c in org_name.lower() if c.isalnum()) + ".com"
    response = await client.post(
        "/organizations",
        json={"name": org_name, "domain": domain},
    )
    response.raise_for_status()
    return response.json()


LIST_TYPE_OPPORTUNITY = 8


async def _create_list_entry(
    client: httpx.AsyncClient, list_id: int, entity_id: int
) -> dict[str, Any]:
    response = await client.post(
        f"/lists/{list_id}/list-entries",
        json={"entity_id": entity_id},
    )
    response.raise_for_status()
    return response.json()


async def _create_opportunity(
    client: httpx.AsyncClient,
    name: str,
    list_id: int,
    org_id: int,
    person_ids: list[int] | None = None,
) -> dict[str, Any]:
    body: dict[str, Any] = {
        "name": name,
        "list_id": list_id,
        "organization_id": org_id,
    }
    if person_ids:
        body["person_ids"] = person_ids
    response = await client.post("/opportunities", json=body)
    response.raise_for_status()
    return response.json()


async def _find_person_by_email(
    client: httpx.AsyncClient, email: str
) -> dict[str, Any] | None:
    response = await client.get("/persons", params={"term": email})
    response.raise_for_status()
    for person in response.json().get("persons", []):
        if email.lower() in [e.lower() for e in person.get("emails", [])]:
            return person
    return None


async def _create_person(
    client: httpx.AsyncClient,
    full_name: str,
    email: str,
    org_id: int,
) -> dict[str, Any]:
    parts = full_name.strip().split(" ", 1)
    first_name = parts[0]
    last_name = parts[1] if len(parts) > 1 else ""
    body: dict[str, Any] = {
        "first_name": first_name,
        "last_name": last_name,
        "organization_ids": [org_id],
    }
    if email:
        body["emails"] = [email]
    response = await client.post("/persons", json=body)
    response.raise_for_status()
    return response.json()


async def _create_note(
    client: httpx.AsyncClient,
    content: str,
    org_id: int,
    person_id: int | None = None,
) -> dict[str, Any]:
    body: dict[str, Any] = {
        "content": content,
        "organization_ids": [org_id],
    }
    if person_id is not None:
        body["person_ids"] = [person_id]
    response = await client.post("/notes", json=body)
    response.raise_for_status()
    return response.json()


# ---------------------------------------------------------------------------
# MCP tools
# ---------------------------------------------------------------------------

async def _search_opportunities_in_affinity(
    client: httpx.AsyncClient,
    term: str,
    list_id: int | None,
) -> list[dict[str, Any]]:
    """Search opportunities by term; optionally filter to a specific list.

    GET /opportunities?term= is tenant-scoped (safe) but does not support
    list_id filtering, so we fetch individual records to check list_entries.
    """
    params: dict[str, Any] = {"term": term, "page_size": 500}
    response = await client.get("/opportunities", params=params)
    response.raise_for_status()
    data = response.json()
    opportunities: list[dict[str, Any]] = (
        data if isinstance(data, list) else data.get("opportunities", [])
    )

    if not list_id or not opportunities:
        return opportunities

    # Filter to only opportunities that have a list_entry for list_id.
    # Requires fetching each opportunity individually (list_entries not in search results).
    filtered: list[dict[str, Any]] = []
    for opp in opportunities:
        detail_resp = await client.get(f"/opportunities/{opp['id']}")
        if detail_resp.status_code == 200:
            detail = detail_resp.json()
            if any(le.get("list_id") == list_id for le in detail.get("list_entries", [])):
                filtered.append(detail)
    return filtered


@mcp.tool()
async def search_opportunities(
    term: str,
    list_id: int | None = None,
) -> str:
    """Search for existing opportunities in Affinity CRM by name or keyword.

    Use this before create_deal to check for duplicates. Searches by partial
    opportunity name match. When list_id is provided, only opportunities that
    belong to that list are returned.

    Args:
        term: Search term — deal name, project code name, or descriptive keywords.
        list_id: Affinity list ID to restrict results (e.g. 339920 for ALL DEALS ECS).
    """
    try:
        async with _client() as client:
            results = await _search_opportunities_in_affinity(client, term, list_id)
    except httpx.HTTPStatusError as e:
        return (
            f"Affinity API error {e.response.status_code} "
            f"on {e.request.method} {e.request.url}\n"
            f"Response body: {e.response.text}"
        )

    if not results:
        suffix = f" in list {list_id}" if list_id else ""
        return f"No opportunities found matching '{term}'{suffix}."

    lines = [f"Found {len(results)} opportunity(ies) matching '{term}':"]
    for opp in results:
        entries = opp.get("list_entries") or []
        list_ids = ", ".join(str(le.get("list_id")) for le in entries) if entries else "unknown"
        lines.append(
            f"  - \"{opp.get('name', '?')}\" "
            f"(id={opp['id']}, org_id={opp.get('organization_id')}, lists=[{list_ids}])"
        )
    return "\n".join(lines)


@mcp.tool()
async def create_deal(
    list_name: str,
    organization_name: str,
    domain: str = "",
    contact_name: str = "",
    contact_email: str = "",
    note: str = "",
) -> str:
    """Add a company as a new deal entry on an Affinity CRM list.

    Finds or creates the organization, optionally creates the contact person,
    and attaches a note with opportunity details. Avoids duplicate organizations.

    Args:
        list_name: The exact name of the Affinity list (e.g. "ALL DEALS ECS").
        organization_name: The company name to add (e.g. "Acme Corp").
        domain: Company website domain (e.g. "acme.com"). Derived from name if omitted.
        contact_name: Full name of the contact person from the email.
        contact_email: Email address of the contact person.
        note: Opportunity details or context extracted from the email.
    """
    try:
        return await _create_deal(list_name, organization_name, domain, contact_name, contact_email, note)
    except httpx.HTTPStatusError as e:
        return (
            f"Affinity API error {e.response.status_code} "
            f"on {e.request.method} {e.request.url}\n"
            f"Response body: {e.response.text}"
        )


async def _create_deal(
    list_name: str,
    organization_name: str,
    domain: str,
    contact_name: str,
    contact_email: str,
    note: str,
) -> str:
    async with _client() as client:
        # Resolve the target list
        lst = await _find_list_by_name(client, list_name)
        if lst is None:
            return (
                f"Error: No Affinity list found with name '{list_name}'. "
                "Check the list name in Affinity and try again."
            )

        # Find or create the organization (scoped to this list — no cross-tenant risk)
        org, _ = await _find_org_in_list(client, lst["id"], lst.get("type", 0), organization_name)
        created_new_org = org is None
        if created_new_org:
            org = await _create_organization(client, organization_name, domain)

        org_id: int = org["id"]

        # Find or create the contact person (needed before adding to list for opportunities)
        person_id: int | None = None
        person_status = ""
        if contact_name or contact_email:
            existing_person = None
            if contact_email:
                existing_person = await _find_person_by_email(client, contact_email)
            if existing_person is None:
                person = await _create_person(client, contact_name, contact_email, org_id)
                person_id = person["id"]
                person_status = f"created new contact '{contact_name}' (id={person_id})"
            else:
                person_id = existing_person["id"]
                person_status = f"found existing contact '{existing_person.get('first_name', '')} {existing_person.get('last_name', '')}' (id={person_id})"

        # Add to list — opportunity lists require POST /opportunities; others use list entries
        if lst.get("type") == LIST_TYPE_OPPORTUNITY:
            entry = await _create_opportunity(
                client,
                name=org.get("name", organization_name),
                list_id=lst["id"],
                org_id=org_id,
                person_ids=[person_id] if person_id else None,
            )
        else:
            entry = await _create_list_entry(client, lst["id"], org_id)

        # Attach note
        note_status = ""
        if note:
            await _create_note(client, note, org_id, person_id)
            note_status = f"note added: {note[:80]}{'...' if len(note) > 80 else ''}"

        # Build status summary
        org_status = "created new organization" if created_new_org else "found existing organization"
        lines = [
            f"Successfully added '{org.get('name', organization_name)}' to '{lst['name']}'.",
            f"  - Organization: {org_status} (id={org_id})",
            f"  - List entry id: {entry.get('id', '?')}",
        ]
        if person_status:
            lines.append(f"  - Contact: {person_status}")
        if note_status:
            lines.append(f"  - Note: {note_status}")
        return "\n".join(lines)
