"""
SiteHub LinkedIn ICP Tracker — Zoho CRM API Client

One-way push of ICP contacts to Zoho CRM Contacts/Accounts modules.
Handles OAuth 2.0 token refresh, duplicate detection, rate limiting,
and field mapping.

Environment variables:
    ZOHO_CLIENT_ID       — OAuth Client ID from Zoho API Console
    ZOHO_CLIENT_SECRET   — OAuth Client Secret
    ZOHO_REFRESH_TOKEN   — Long-lived refresh token
    ZOHO_API_DOMAIN      — defaults to https://www.zohoapis.eu (EU)
    ZOHO_ACCOUNTS_URL    — defaults to https://accounts.zoho.eu (EU)
"""

import os
import time
import logging

import requests

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

ZOHO_CLIENT_ID = os.environ.get("ZOHO_CLIENT_ID", "")
ZOHO_CLIENT_SECRET = os.environ.get("ZOHO_CLIENT_SECRET", "")
ZOHO_REFRESH_TOKEN = os.environ.get("ZOHO_REFRESH_TOKEN", "")
ZOHO_API_DOMAIN = os.environ.get("ZOHO_API_DOMAIN", "https://www.zohoapis.eu")
ZOHO_ACCOUNTS_URL = os.environ.get("ZOHO_ACCOUNTS_URL", "https://accounts.zoho.eu")

# In-memory token cache
_access_token: str | None = None
_token_expires_at: float = 0.0

# Rate limiting
_last_request_time: float = 0.0
_MIN_REQUEST_INTERVAL = 0.7  # seconds between API calls


# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------

class ZohoError(Exception):
    """Base Zoho error."""


class ZohoAuthError(ZohoError):
    """Authentication / token refresh failure."""


class ZohoAPIError(ZohoError):
    """General API error (4xx/5xx)."""


class ZohoRateLimitError(ZohoError):
    """429 rate limit exceeded."""


# ---------------------------------------------------------------------------
# Auth helpers
# ---------------------------------------------------------------------------

def is_configured() -> bool:
    """Return True if all required Zoho env vars are set."""
    return bool(ZOHO_CLIENT_ID and ZOHO_CLIENT_SECRET and ZOHO_REFRESH_TOKEN)


def _refresh_access_token() -> str:
    """Exchange the refresh token for a new access token."""
    global _access_token, _token_expires_at

    url = f"{ZOHO_ACCOUNTS_URL}/oauth/v2/token"
    resp = requests.post(url, params={
        "grant_type": "refresh_token",
        "client_id": ZOHO_CLIENT_ID,
        "client_secret": ZOHO_CLIENT_SECRET,
        "refresh_token": ZOHO_REFRESH_TOKEN,
    }, timeout=15)

    if resp.status_code != 200:
        raise ZohoAuthError(f"Token refresh failed ({resp.status_code}): {resp.text[:300]}")

    data = resp.json()
    if "access_token" not in data:
        raise ZohoAuthError(f"No access_token in response: {data}")

    _access_token = data["access_token"]
    # Expire 5 minutes early as safety margin
    _token_expires_at = time.time() + data.get("expires_in", 3600) - 300
    logger.info("Zoho access token refreshed")
    return _access_token


def _get_access_token() -> str:
    """Return a valid access token, refreshing if needed."""
    if _access_token and time.time() < _token_expires_at:
        return _access_token
    return _refresh_access_token()


# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------

def _rate_limit():
    """Enforce minimum interval between API calls."""
    global _last_request_time
    elapsed = time.time() - _last_request_time
    if elapsed < _MIN_REQUEST_INTERVAL:
        time.sleep(_MIN_REQUEST_INTERVAL - elapsed)
    _last_request_time = time.time()


def _api_request(method: str, path: str, json_body: dict | None = None,
                 params: dict | None = None, _retry: bool = True) -> dict:
    """Central Zoho CRM API request handler.

    Handles auth headers, rate limiting, 401 retry, and error mapping.
    """
    _rate_limit()
    token = _get_access_token()
    url = f"{ZOHO_API_DOMAIN}/crm/v6/{path}"
    headers = {
        "Authorization": f"Zoho-oauthtoken {token}",
    }

    resp = requests.request(method, url, headers=headers,
                            json=json_body, params=params, timeout=30)

    # Token expired — refresh and retry once
    if resp.status_code == 401 and _retry:
        _refresh_access_token()
        return _api_request(method, path, json_body, params, _retry=False)

    if resp.status_code == 429:
        raise ZohoRateLimitError("Zoho API rate limit exceeded")

    if resp.status_code >= 400:
        raise ZohoAPIError(
            f"Zoho API {method} {path} returned {resp.status_code}: "
            f"{resp.text[:500]}"
        )

    # Some endpoints return 204 No Content
    if resp.status_code == 204:
        return {}

    return resp.json()


# ---------------------------------------------------------------------------
# Name utilities
# ---------------------------------------------------------------------------

def split_name(full_name: str) -> tuple[str, str]:
    """Split a full name into (first_name, last_name).

    Zoho requires Last_Name. If only one word, last_name gets the name
    and first_name is empty.
    """
    parts = full_name.strip().split()
    if not parts:
        return ("", "-")
    if len(parts) == 1:
        return ("", parts[0])
    return (parts[0], " ".join(parts[1:]))


# ---------------------------------------------------------------------------
# Search / lookup
# ---------------------------------------------------------------------------

def search_zoho_contact(full_name: str) -> dict | None:
    """Search Zoho CRM for an existing Contact by name.

    Returns the first matching record dict (with 'id') or None.
    """
    try:
        data = _api_request("GET", "Contacts/search", params={
            "criteria": f"(Full_Name:equals:{full_name})",
        })
    except ZohoAPIError as e:
        if "204" in str(e) or "No Content" in str(e):
            return None
        raise

    records = data.get("data", [])
    if records:
        return records[0]

    # Fallback: search by split name
    first, last = split_name(full_name)
    if first:
        try:
            data = _api_request("GET", "Contacts/search", params={
                "criteria": f"((Last_Name:equals:{last})and(First_Name:equals:{first}))",
            })
        except ZohoAPIError:
            return None
        records = data.get("data", [])
        if records:
            return records[0]

    return None


def search_zoho_account(company_name: str) -> dict | None:
    """Search Zoho CRM for an existing Account by name.

    Returns the first matching record dict (with 'id') or None.
    """
    try:
        data = _api_request("GET", "Accounts/search", params={
            "criteria": f"(Account_Name:equals:{company_name})",
        })
    except ZohoAPIError:
        return None

    records = data.get("data", [])
    return records[0] if records else None


# ---------------------------------------------------------------------------
# Create / upsert
# ---------------------------------------------------------------------------

def create_zoho_account(company_name: str, tier: str = "") -> str:
    """Create a new Account in Zoho CRM. Returns the new Account id."""
    tier_label = f"Tier {tier} ICP" if tier and tier != "Not ICP" else tier
    payload = {
        "data": [{
            "Account_Name": company_name,
            "Tag": [{"name": "LinkedIn ICP"}],
            "Description": f"Imported from SiteHub ICP Tracker. {tier_label}".strip(),
        }]
    }
    data = _api_request("POST", "Accounts", json_body=payload)
    records = data.get("data", [])
    if not records or records[0].get("code") != "SUCCESS":
        detail = records[0] if records else data
        raise ZohoAPIError(f"Failed to create Account '{company_name}': {detail}")
    return records[0]["details"]["id"]


def find_or_create_account(company_name: str, tier: str = "") -> str | None:
    """Find an existing Account or create a new one. Returns Account id.

    Returns None if company_name is blank.
    """
    if not company_name or not company_name.strip():
        return None

    existing = search_zoho_account(company_name)
    if existing:
        logger.info("Found existing Zoho Account '%s' (id=%s)", company_name, existing["id"])
        return existing["id"]

    new_id = create_zoho_account(company_name, tier)
    logger.info("Created new Zoho Account '%s' (id=%s)", company_name, new_id)
    return new_id


# ---------------------------------------------------------------------------
# Contact payload builder
# ---------------------------------------------------------------------------

def build_contact_payload(contact: dict, account_id: str | None = None) -> dict:
    """Map ICP tracker contact dict to Zoho CRM Contact fields."""
    first, last = split_name(contact.get("name", ""))

    # Build description with metadata
    desc_parts = ["Source: SiteHub LinkedIn ICP Tracker"]
    if contact.get("match_reason"):
        desc_parts.append(f"Match Reason: {contact['match_reason']}")
    desc_parts.append("---")
    if contact.get("notes"):
        desc_parts.append(contact["notes"])
    description = "\n".join(desc_parts)

    payload: dict = {
        "Last_Name": last,
        "Title": (contact.get("title", "") or "")[:100],
        "Description": description,
        "Lead_Source": "LinkedIn",
        "Tag": [{"name": "LinkedIn ICP"}],
    }

    if first:
        payload["First_Name"] = first

    if account_id:
        payload["Account_Name"] = {"id": account_id}

    # Custom fields — interaction data
    if contact.get("total_interactions"):
        payload["LinkedIn_Interactions"] = contact["total_interactions"]
    if contact.get("first_interaction"):
        d = contact["first_interaction"]
        payload["LinkedIn_First_Interaction"] = d.isoformat() if hasattr(d, "isoformat") else str(d)
    if contact.get("latest_interaction"):
        d = contact["latest_interaction"]
        payload["LinkedIn_Latest_Interaction"] = d.isoformat() if hasattr(d, "isoformat") else str(d)

    return payload


# ---------------------------------------------------------------------------
# Main sync entry point
# ---------------------------------------------------------------------------

def push_contact_to_zoho(contact: dict) -> dict:
    """Push a single ICP contact to Zoho CRM (create or update).

    Returns dict with keys:
        action: 'create' | 'update' | 'skip'
        zoho_id: str (on success)
        reason: str (on skip/error)
    """
    if not is_configured():
        return {"action": "skip", "reason": "not_configured"}

    if contact.get("status") != "ICP":
        return {"action": "skip", "reason": "not_icp"}

    name = contact.get("name", "")
    if not name:
        return {"action": "skip", "reason": "no_name"}

    # Check if already linked via zoho_id
    existing_zoho_id = contact.get("zoho_id")

    # If we have a zoho_id, update directly
    if existing_zoho_id:
        account_id = find_or_create_account(
            contact.get("company", ""),
            contact.get("company_tier", ""),
        )
        payload = build_contact_payload(contact, account_id)
        data = _api_request("PUT", f"Contacts/{existing_zoho_id}",
                            json_body={"data": [payload]})
        records = data.get("data", [])
        if records and records[0].get("code") == "SUCCESS":
            logger.info("Updated Zoho Contact '%s' (id=%s)", name, existing_zoho_id)
            return {"action": "update", "zoho_id": existing_zoho_id}
        raise ZohoAPIError(f"Failed to update Contact '{name}': {records}")

    # Search for existing contact in Zoho (duplicate detection)
    existing = search_zoho_contact(name)
    if existing:
        zoho_id = existing["id"]
        account_id = find_or_create_account(
            contact.get("company", ""),
            contact.get("company_tier", ""),
        )
        payload = build_contact_payload(contact, account_id)
        data = _api_request("PUT", f"Contacts/{zoho_id}",
                            json_body={"data": [payload]})
        records = data.get("data", [])
        if records and records[0].get("code") == "SUCCESS":
            logger.info("Updated existing Zoho Contact '%s' (id=%s)", name, zoho_id)
            return {"action": "update", "zoho_id": zoho_id}
        raise ZohoAPIError(f"Failed to update Contact '{name}': {records}")

    # Create new contact
    account_id = find_or_create_account(
        contact.get("company", ""),
        contact.get("company_tier", ""),
    )
    payload = build_contact_payload(contact, account_id)
    data = _api_request("POST", "Contacts", json_body={"data": [payload]})
    records = data.get("data", [])
    if not records or records[0].get("code") != "SUCCESS":
        raise ZohoAPIError(f"Failed to create Contact '{name}': {records}")

    zoho_id = records[0]["details"]["id"]
    logger.info("Created new Zoho Contact '%s' (id=%s)", name, zoho_id)
    return {"action": "create", "zoho_id": zoho_id}
