"""BackendClient — calls HeyLead Backend API proxy instead of Unipile directly.

Same method signatures as UnipileClient so all 5 MCP tools work unchanged.
The backend handles Unipile auth, account scoping, and API key security.
"""

from __future__ import annotations

import asyncio
import logging
from typing import Any

import httpx

from ..constants import UNIPILE_POLL_INTERVAL_SECONDS, UNIPILE_POLL_TIMEOUT_SECONDS
from .unipile import UnipileAuthError, UnipileError

logger = logging.getLogger(__name__)

_TIMEOUT = httpx.Timeout(30.0, connect=30.0, read=60.0, write=60.0)


def _wrap_connection_error(e: Exception, base_url: str) -> UnipileError:
    """Wrap low-level httpx errors with user-friendly messages."""
    if isinstance(e, httpx.ConnectError):
        return UnipileError(
            f"Cannot connect to HeyLead backend at {base_url}. "
            "Is the URL correct? Check your internet connection."
        )
    if isinstance(e, httpx.ConnectTimeout):
        return UnipileError(
            f"Connection to {base_url} timed out. "
            "The backend may be starting up — try again in 30 seconds."
        )
    if isinstance(e, httpx.ReadTimeout):
        return UnipileError(
            f"Request to {base_url} timed out waiting for response. "
            "Try again — the backend may be under heavy load."
        )
    if isinstance(e, httpx.TimeoutException):
        return UnipileError(f"Request to {base_url} timed out. Try again.")
    return UnipileError(f"Backend request failed: {e}")


class BackendClient:
    """Async HTTP client that calls HeyLead Backend API proxy.

    Provides the same interface as UnipileClient so tools don't need to know
    whether they're talking to Unipile directly or via the backend.
    """

    def __init__(self, backend_url: str, jwt_token: str) -> None:
        self.base_url = backend_url.rstrip("/")
        self.jwt_token = jwt_token
        self._client = httpx.AsyncClient(timeout=_TIMEOUT)

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.jwt_token}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        }

    async def close(self) -> None:
        await self._client.aclose()

    # ── Auth / Account Management ──

    async def create_hosted_auth_link(self) -> str:
        """Create a LinkedIn auth link via the backend proxy."""
        url = f"{self.base_url}/api/v1/auth/connect"
        try:
            resp = await self._client.post(url, headers=self._headers())
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            raise _wrap_connection_error(e, self.base_url)
        if resp.status_code == 401:
            raise UnipileAuthError("Backend JWT expired or invalid. Run setup_profile again.")
        if resp.status_code == 409:
            data = resp.json()
            raise UnipileError(data.get("detail", "Account already connected"))
        resp.raise_for_status()
        result = resp.json()
        auth_url = result.get("url")
        if not auth_url:
            raise UnipileError(f"Backend response missing 'url': {result}")
        return auth_url

    async def list_accounts(self) -> list[dict[str, Any]]:
        """List accounts via the backend proxy (filtered to user's account)."""
        url = f"{self.base_url}/api/v1/accounts"
        try:
            resp = await self._client.get(url, headers=self._headers())
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            raise _wrap_connection_error(e, self.base_url)
        if resp.status_code == 401:
            raise UnipileAuthError("Backend JWT expired or invalid.")
        resp.raise_for_status()
        data = resp.json()
        return data.get("accounts", [])

    async def find_linkedin_account(self) -> tuple[str | None, str]:
        """Find a LinkedIn account from the backend."""
        try:
            accounts = await self.list_accounts()
        except Exception as e:
            return None, f"Failed to list accounts: {e}"

        if not accounts:
            return None, "No accounts found."

        for acc in accounts:
            provider = acc.get("provider") or acc.get("provider_type") or acc.get("type") or ""
            if "LINKEDIN" in str(provider).upper():
                account_id = (
                    acc.get("id") or acc.get("account_id")
                    or acc.get("accountId") or acc.get("uuid")
                )
                if account_id:
                    return str(account_id), "Found LinkedIn account"
        return None, "No LinkedIn accounts found."

    async def verify_account(self, account_id: str) -> tuple[bool, str]:
        """Check if a Unipile account is still connected via the backend."""
        try:
            url = f"{self.base_url}/api/v1/accounts/{account_id}"
            resp = await self._client.get(url, headers=self._headers())
            if resp.status_code == 403:
                return False, "Access denied: not your account"
            if resp.status_code == 404:
                return False, "Account not found"
            if resp.status_code == 401:
                return False, "Backend JWT expired"
            resp.raise_for_status()
            data = resp.json()
            status = data.get("status") or data.get("state") or data.get("connection_status") or ""
            if isinstance(status, str) and status.lower() in ("disconnected", "error", "failed", "expired"):
                return False, f"Account status: {status}"
            return True, "Connected"
        except Exception as e:
            return False, f"Verification error: {e}"

    async def bind_account(self, account_id: str) -> None:
        """Bind a Unipile account_id to the authenticated user on the backend."""
        url = f"{self.base_url}/api/v1/accounts/{account_id}/bind"
        resp = await self._client.post(url, headers=self._headers())
        if resp.status_code == 409:
            pass  # Already bound — that's fine
        elif resp.status_code != 200:
            detail = resp.text[:200]
            raise UnipileError(f"Failed to bind account: {resp.status_code} — {detail}")

    async def get_existing_account_ids(self) -> set[str]:
        """Snapshot all current account IDs via the backend."""
        try:
            accounts = await self.list_accounts()
        except Exception:
            return set()
        ids: set[str] = set()
        for acc in accounts:
            aid = acc.get("id") or acc.get("account_id") or acc.get("accountId") or acc.get("uuid")
            if aid:
                ids.add(str(aid))
        return ids

    async def poll_for_new_account(
        self,
        known_ids: set[str],
        timeout_seconds: int = UNIPILE_POLL_TIMEOUT_SECONDS,
        interval: int = UNIPILE_POLL_INTERVAL_SECONDS,
    ) -> tuple[str | None, str]:
        """Poll for a NEW LinkedIn account that wasn't in known_ids."""
        elapsed = 0
        while elapsed < timeout_seconds:
            try:
                accounts = await self.list_accounts()
            except Exception:
                await asyncio.sleep(interval)
                elapsed += interval
                continue

            for acc in accounts:
                aid = acc.get("id") or acc.get("account_id") or acc.get("accountId") or acc.get("uuid")
                if not aid or str(aid) in known_ids:
                    continue
                provider = acc.get("provider") or acc.get("provider_type") or acc.get("type") or ""
                if "LINKEDIN" not in str(provider).upper():
                    continue
                account_id = str(aid)
                connected, status = await self.verify_account(account_id)
                if connected:
                    # Bind this account to the user on the backend
                    await self.bind_account(account_id)
                    return account_id, "LinkedIn account connected!"

            await asyncio.sleep(interval)
            elapsed += interval

        return None, f"Timed out after {timeout_seconds}s waiting for LinkedIn connection."

    # ── Profile ──

    async def get_own_profile(self, account_id: str) -> dict[str, Any]:
        """Fetch the user's LinkedIn profile via the backend proxy.

        The backend returns raw Unipile data; we normalize here (same as UnipileClient).
        """
        url = f"{self.base_url}/api/v1/profile"
        try:
            resp = await self._client.get(url, headers=self._headers())
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            raise _wrap_connection_error(e, self.base_url)
        if resp.status_code in (401, 403):
            raise UnipileAuthError()
        if resp.status_code == 400:
            raise UnipileError("No LinkedIn account connected on the backend.")
        resp.raise_for_status()
        data = resp.json()
        if isinstance(data, list) and data:
            data = data[0]

        # Normalize — same logic as UnipileClient.get_own_profile
        first_name = data.get("first_name") or data.get("firstName") or ""
        last_name = data.get("last_name") or data.get("lastName") or ""
        headline = data.get("headline") or data.get("occupation") or ""
        public_id = data.get("public_identifier") or data.get("publicIdentifier") or ""
        provider_id = data.get("provider_id") or data.get("id") or ""

        title = headline
        company = ""
        if " at " in headline:
            parts = headline.rsplit(" at ", 1)
            title = parts[0]
            company = parts[1]

        experience = data.get("experience") or data.get("positions") or []
        if isinstance(experience, list) and experience:
            current = experience[0]
            if isinstance(current, dict):
                title = current.get("title") or current.get("role") or title
                company = current.get("company") or current.get("company_name") or company

        location = data.get("location") or ""
        if isinstance(location, dict):
            location = location.get("name") or location.get("default") or str(location)

        skills_raw = data.get("skills") or []
        skills: list[str] = []
        if isinstance(skills_raw, list):
            for s in skills_raw:
                if isinstance(s, str):
                    skills.append(s)
                elif isinstance(s, dict):
                    skills.append(s.get("name") or s.get("skill") or str(s))

        return {
            "name": f"{first_name} {last_name}".strip(),
            "first_name": first_name,
            "last_name": last_name,
            "headline": headline,
            "title": title,
            "company": company,
            "location": location,
            "summary": data.get("summary") or data.get("about") or "",
            "industry": data.get("industry") or "",
            "public_id": public_id,
            "provider_id": str(provider_id),
            "profile_url": f"https://www.linkedin.com/in/{public_id}" if public_id else "",
            "connections": data.get("connections_count") or data.get("network_info", {}).get("connections_count", 0),
            "skills": skills,
            "experience": experience if isinstance(experience, list) else [],
            "posts": [],
        }

    async def get_posts(
        self, account_id: str, provider_id: str = "", limit: int = 10,
    ) -> list[dict[str, str]]:
        """Fetch recent LinkedIn posts via the backend proxy."""
        params = {"limit": str(limit)}
        if provider_id:
            params["provider_id"] = provider_id

        url = f"{self.base_url}/api/v1/posts"
        try:
            resp = await self._client.get(url, params=params, headers=self._headers())
            if resp.status_code in (404, 400):
                return []
            resp.raise_for_status()
            data = resp.json()

            items = []
            if isinstance(data, list):
                items = data
            elif isinstance(data, dict):
                items = data.get("items") or data.get("data") or data.get("posts") or []

            posts: list[dict[str, str]] = []
            for item in items[:limit]:
                text = ""
                if isinstance(item, dict):
                    text = item.get("text") or item.get("body") or item.get("content") or ""
                elif isinstance(item, str):
                    text = item
                if text:
                    urn = ""
                    if isinstance(item, dict):
                        urn = item.get("id") or item.get("urn") or item.get("social_id") or ""
                    posts.append({"text": str(text), "urn": str(urn)})
            return posts
        except Exception as e:
            logger.warning(f"Failed to fetch posts via backend: {e}")
            return []

    # ── Search ──

    async def search_people(
        self,
        account_id: str,
        keywords: str = "",
        title: str = "",
        location: str = "",
        count: int = 25,
    ) -> list[dict[str, Any]]:
        """Search LinkedIn for people via the backend proxy."""
        search_query = keywords
        if title and title.lower() not in keywords.lower():
            search_query = f"{title} {keywords}"

        url = f"{self.base_url}/api/v1/search"
        payload = {
            "keywords": search_query,
            "category": "people",
            "limit": min(count, 50),
        }

        try:
            try:
                resp = await self._client.post(url, json=payload, headers=self._headers())
            except (httpx.ConnectError, httpx.TimeoutException) as e:
                raise _wrap_connection_error(e, self.base_url)
            if resp.status_code in (401, 403):
                raise UnipileAuthError()
            if resp.status_code == 400:
                raise UnipileError("No LinkedIn account connected on the backend.")
            resp.raise_for_status()
            data = resp.json()

            items = data.get("items") or data.get("results") or data.get("data") or []
            if isinstance(data, list):
                items = data

            results: list[dict[str, Any]] = []
            for item in items:
                if not isinstance(item, dict):
                    continue
                name_parts = []
                if item.get("first_name") or item.get("firstName"):
                    name_parts.append(item.get("first_name") or item.get("firstName") or "")
                if item.get("last_name") or item.get("lastName"):
                    name_parts.append(item.get("last_name") or item.get("lastName") or "")
                name = " ".join(name_parts).strip() or item.get("name") or ""
                if not name:
                    continue

                headline_val = item.get("headline") or ""
                pub_id = item.get("public_identifier") or item.get("publicIdentifier") or ""
                prov_id = item.get("provider_id") or item.get("id") or item.get("member_urn") or ""

                parsed_title = headline_val
                parsed_company = ""
                if " at " in headline_val:
                    parts = headline_val.rsplit(" at ", 1)
                    parsed_title = parts[0]
                    parsed_company = parts[1]

                loc = item.get("location") or ""
                if isinstance(loc, dict):
                    loc = loc.get("name") or loc.get("default") or ""

                profile_url = item.get("profile_url") or item.get("public_profile_url") or ""
                if not profile_url and pub_id:
                    profile_url = f"https://www.linkedin.com/in/{pub_id}"

                results.append({
                    "name": name,
                    "title": parsed_title,
                    "company": parsed_company,
                    "headline": headline_val,
                    "location": str(loc),
                    "linkedin_url": profile_url,
                    "public_id": pub_id,
                    "provider_id": str(prov_id),
                })
            return results
        except UnipileAuthError:
            raise
        except Exception as e:
            logger.warning(f"Backend search error: {e}")
            return []

    # ── Messaging ──

    async def send_invitation(
        self,
        account_id: str,
        provider_id: str,
        message: str = "",
    ) -> dict[str, Any]:
        """Send a LinkedIn connection invitation via the backend proxy."""
        result: dict[str, Any] = {"success": False, "error": "", "blocked": False, "auth_error": False}

        url = f"{self.base_url}/api/v1/invite"
        payload: dict[str, Any] = {"provider_id": provider_id}
        if message:
            payload["message"] = message[:200]

        try:
            try:
                resp = await self._client.post(url, json=payload, headers=self._headers())
            except (httpx.ConnectError, httpx.TimeoutException) as e:
                result["error"] = str(_wrap_connection_error(e, self.base_url))
                result["blocked"] = True
                return result
            if resp.status_code == 401:
                result["error"] = "Backend JWT expired."
                result["auth_error"] = True
                result["blocked"] = True
                return result
            if resp.status_code == 400:
                result["error"] = "No LinkedIn account connected."
                result["blocked"] = True
                return result

            resp.raise_for_status()
            data = resp.json()
            status_code = data.get("status_code", 200)

            if status_code in (200, 201):
                result["success"] = True
            elif status_code == 429:
                result["error"] = "Rate limited by LinkedIn. Will retry later."
                result["blocked"] = True
            elif status_code in (401, 403):
                result["error"] = "LinkedIn account disconnected."
                result["auth_error"] = True
                result["blocked"] = True
            elif status_code == 409:
                result["error"] = "Already connected or invitation pending."
            else:
                result["error"] = f"Unipile returned {status_code}"

        except httpx.TimeoutException:
            result["error"] = "Request timed out."
        except Exception as e:
            result["error"] = f"Send failed: {e}"

        return result

    async def get_chats(
        self,
        account_id: str,
        limit: int = 20,
    ) -> list[dict[str, Any]]:
        """Fetch recent LinkedIn messages via the backend proxy."""
        url = f"{self.base_url}/api/v1/chats"
        params = {"limit": str(limit)}

        try:
            try:
                resp = await self._client.get(url, params=params, headers=self._headers())
            except (httpx.ConnectError, httpx.TimeoutException) as e:
                raise _wrap_connection_error(e, self.base_url)
            if resp.status_code in (401, 403):
                raise UnipileAuthError()
            if resp.status_code == 400:
                raise UnipileError("No LinkedIn account connected.")
            resp.raise_for_status()
            data = resp.json()

            items = data.get("items") or data.get("chats") or data.get("data") or []
            if isinstance(data, list):
                items = data

            # Normalize — same logic as UnipileClient.get_chats
            messages: list[dict[str, Any]] = []
            for chat in items:
                if not isinstance(chat, dict):
                    continue
                chat_id = chat.get("id") or chat.get("chat_id") or ""
                chat_messages = chat.get("messages") or chat.get("last_messages") or []
                if isinstance(chat_messages, list) and chat_messages:
                    last_msg = chat_messages[-1] if isinstance(chat_messages[-1], dict) else {}
                elif isinstance(chat_messages, dict):
                    last_msg = chat_messages
                else:
                    last_msg = {}

                text = last_msg.get("text") or last_msg.get("body") or last_msg.get("content") or ""
                if not text:
                    continue

                sender_id = last_msg.get("sender_id") or last_msg.get("sender", {}).get("provider_id", "")
                sender_name = last_msg.get("sender_name") or last_msg.get("sender", {}).get("display_name", "")

                if not sender_name:
                    attendees = chat.get("attendees") or []
                    for att in attendees:
                        if isinstance(att, dict):
                            att_id = att.get("provider_id") or att.get("id") or ""
                            if att_id and att_id == sender_id:
                                sender_name = att.get("display_name") or att.get("name") or ""
                                break
                            elif not sender_name:
                                sender_name = att.get("display_name") or att.get("name") or ""

                ts_raw = last_msg.get("timestamp") or last_msg.get("created_at") or last_msg.get("date") or ""
                timestamp = 0
                if isinstance(ts_raw, (int, float)):
                    timestamp = int(ts_raw)
                elif isinstance(ts_raw, str) and ts_raw:
                    try:
                        from datetime import datetime
                        timestamp = int(datetime.fromisoformat(ts_raw.replace("Z", "+00:00")).timestamp())
                    except (ValueError, TypeError):
                        pass

                messages.append({
                    "sender_name": sender_name,
                    "sender_id": str(sender_id),
                    "text": text,
                    "timestamp": timestamp,
                    "conversation_urn": str(chat_id),
                })
            return messages
        except UnipileAuthError:
            raise
        except Exception as e:
            logger.warning(f"Failed to fetch chats via backend: {e}")
            return []
