"""Unipile HTTP client for HeyLead — all LinkedIn operations go through here.

Handles all LinkedIn operations for HeyLead's scope:
- Hosted auth link creation (setup flow)
- Account listing / polling / verification
- Profile fetching
- People search
- Invitation sending
- Chat/message fetching

Uses direct httpx calls — no Unipile SDK dependency.
"""

from __future__ import annotations

import asyncio
import json
import logging
import re
from datetime import datetime, timedelta, timezone
from typing import Any

import httpx

from .. import config
from ..constants import UNIPILE_POLL_INTERVAL_SECONDS, UNIPILE_POLL_TIMEOUT_SECONDS

logger = logging.getLogger(__name__)

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


# ──────────────────────────────────────────────
# Exceptions
# ──────────────────────────────────────────────

class UnipileError(Exception):
    """Base error for Unipile API failures."""


class UnipileAuthError(UnipileError):
    """Raised when the Unipile account is disconnected or auth fails."""

    def __init__(self, message: str = "") -> None:
        self.message = message or (
            "🔑 LinkedIn account disconnected.\n\n"
            "Run setup_profile again to reconnect your LinkedIn account."
        )
        super().__init__(self.message)


# ──────────────────────────────────────────────
# Client
# ──────────────────────────────────────────────

class UnipileClient:
    """Async Unipile HTTP client for LinkedIn operations.

    Config loaded from ~/.heylead/config.json:
        unipile_api_url: e.g. "https://apiXX.unipile.com:XXXXX"
        unipile_api_key: X-API-KEY header value
    """

    def __init__(self, api_url: str, api_key: str) -> None:
        # Clean URL: remove non-printable chars, ensure https://
        raw = re.sub(r"[^\x20-\x7E]", "", api_url.strip()).rstrip("/")
        if not raw.startswith("http://") and not raw.startswith("https://"):
            raw = f"https://{raw}"
        self.base_url = raw
        self.api_key = api_key.strip()
        self._client = httpx.AsyncClient(timeout=_TIMEOUT)

    def _headers(self) -> dict[str, str]:
        return {
            "accept": "application/json",
            "content-type": "application/json",
            "X-API-KEY": self.api_key,
        }

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

    # ── Auth / Account Management ──

    async def create_hosted_auth_link(self) -> str:
        """Create a hosted auth link for LinkedIn OAuth.

        Returns the URL the user should open in their browser.
        """
        # expiresOn: 24h from now, ISO 8601 with exactly 3ms digits
        future = datetime.now(timezone.utc) + timedelta(hours=24)
        expires_on = future.strftime("%Y-%m-%dT%H:%M:%S") + f".{future.microsecond // 1000:03d}Z"

        payload: dict[str, Any] = {
            "expiresOn": expires_on,
            "api_url": self.base_url,
            "type": "create",
            "providers": ["LINKEDIN"],
        }

        url = f"{self.base_url}/api/v1/hosted/accounts/link"
        try:
            resp = await self._client.post(url, json=payload, headers=self._headers())
            resp.raise_for_status()
            result = resp.json()
            auth_url = result.get("url")
            if not auth_url:
                raise UnipileError(f"Unipile response missing 'url': {result}")
            return auth_url
        except httpx.HTTPStatusError as e:
            detail = ""
            try:
                detail = str(e.response.json())
            except Exception:
                detail = e.response.text[:300]
            raise UnipileError(f"Failed to create auth link: {e.response.status_code} — {detail}") from e

    async def list_accounts(self) -> list[dict[str, Any]]:
        """List all accounts from Unipile."""
        url = f"{self.base_url}/api/v1/accounts"
        resp = await self._client.get(url, headers=self._headers())
        resp.raise_for_status()
        data = resp.json()

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

    async def find_linkedin_account(self) -> tuple[str | None, str]:
        """Find a LinkedIn account among connected Unipile accounts.

        Returns (account_id, message).
        """
        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 in Unipile."

        def _is_linkedin(acc: dict) -> bool:
            provider = acc.get("provider") or acc.get("provider_type") or acc.get("type") or ""
            return "LINKEDIN" in str(provider).upper()

        linkedin_accounts = [a for a in accounts if _is_linkedin(a)]
        if not linkedin_accounts:
            providers = [a.get("provider") for a in accounts]
            return None, f"No LinkedIn accounts found. Providers: {providers}"

        account = linkedin_accounts[0]
        account_id = (
            account.get("id")
            or account.get("account_id")
            or account.get("accountId")
            or account.get("uuid")
        )
        if account_id:
            return str(account_id), "Found LinkedIn account"
        return None, "LinkedIn account found but no ID field"

    async def verify_account(self, account_id: str) -> tuple[bool, str]:
        """Check if a Unipile account is still connected.

        Returns (is_connected, message).
        """
        try:
            url = f"{self.base_url}/api/v1/accounts/{account_id}"
            resp = await self._client.get(url, headers=self._headers())
            if resp.status_code == 404:
                return False, "Account not found in Unipile"
            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}"

            provider = (data.get("provider") or "").upper()
            if provider and "LINKEDIN" not in provider:
                return False, f"Account is not LinkedIn (provider: {provider})"

            return True, "Connected"
        except Exception as e:
            return False, f"Verification error: {e}"

    async def poll_for_account(
        self,
        timeout_seconds: int = UNIPILE_POLL_TIMEOUT_SECONDS,
        interval: int = UNIPILE_POLL_INTERVAL_SECONDS,
    ) -> tuple[str | None, str]:
        """Poll for a LinkedIn account to appear after OAuth.

        Loops every `interval` seconds until timeout.
        Returns (account_id, message).
        """
        elapsed = 0
        while elapsed < timeout_seconds:
            account_id, msg = await self.find_linkedin_account()
            if account_id:
                # Verify it's actually connected
                connected, status = await self.verify_account(account_id)
                if connected:
                    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."

    async def get_existing_account_ids(self) -> set[str]:
        """Snapshot all current account IDs (used to detect new accounts)."""
        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.

        This ensures we only pick up the account the current user just connected,
        not pre-existing accounts from other users on the same Unipile workspace.

        Returns (account_id, message).
        """
        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
                # Found a new LinkedIn account
                account_id = str(aid)
                connected, status = await self.verify_account(account_id)
                if connected:
                    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 authenticated user's LinkedIn profile.

        Returns a normalized profile dict matching HeyLead's format.
        """
        url = f"{self.base_url}/api/v1/users/me?account_id={account_id}"
        resp = await self._client.get(url, headers=self._headers())

        if resp.status_code in (401, 403):
            raise UnipileAuthError()
        resp.raise_for_status()

        data = resp.json()
        if isinstance(data, list) and data:
            data = data[0]

        # Normalize to HeyLead profile format
        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 ""

        # Parse title + company from headline ("Title at Company")
        title = headline
        company = ""
        if " at " in headline:
            parts = headline.rsplit(" at ", 1)
            title = parts[0]
            company = parts[1]

        # Try to get from experience if available
        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

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

        # Extract skills
        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": [],  # Populated separately via get_posts()
        }

    async def get_posts(
        self, account_id: str, provider_id: str = "", limit: int = 10,
    ) -> list[dict[str, str]]:
        """Fetch recent LinkedIn posts for the user.

        Returns list of {"text": "...", "urn": "..."}.
        """
        identifier = provider_id or account_id
        url = f"{self.base_url}/api/v1/users/{identifier}/posts?account_id={account_id}&limit={limit}"

        try:
            resp = await self._client.get(url, headers=self._headers())
            if resp.status_code in (404, 400):
                logger.info(f"Posts endpoint returned {resp.status_code}, trying fallback")
                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: {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 matching criteria via Unipile.

        Returns normalized prospect dicts matching HeyLead's format.
        """
        search_query = keywords
        if title and title.lower() not in keywords.lower():
            search_query = f"{title} {keywords}"

        url = f"{self.base_url}/api/v1/linkedin/search?account_id={account_id}"
        payload: dict[str, Any] = {
            "api": "classic",
            "category": "people",
            "keywords": search_query,
            "limit": min(count, 50),
        }

        try:
            resp = await self._client.post(url, json=payload, headers=self._headers())
            if resp.status_code in (401, 403):
                raise UnipileAuthError()
            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 = 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 ""

                # Parse title + company from headline
                parsed_title = headline
                parsed_company = ""
                if " at " in headline:
                    parts = headline.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,
                    "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"Unipile 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 Unipile.

        Returns {"success": bool, "error": str, "blocked": bool, "auth_error": bool}.
        """
        result: dict[str, Any] = {"success": False, "error": "", "blocked": False, "auth_error": False}

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

        try:
            resp = await self._client.post(url, json=payload, headers=self._headers())

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

        except httpx.TimeoutException:
            result["error"] = "Request timed out. Check your internet connection."
        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/chats via Unipile.

        Returns normalized message list matching HeyLead's format:
        [{"sender_name", "sender_id", "text", "timestamp", "conversation_urn"}]
        """
        url = f"{self.base_url}/api/v1/chats?account_id={account_id}&limit={limit}"

        try:
            resp = await self._client.get(url, headers=self._headers())
            if resp.status_code in (401, 403):
                raise UnipileAuthError()
            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

            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 ""

                # Get the last message
                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

                # Identify the sender
                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 no sender info in message, try attendees
                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 ""

                # Parse timestamp
                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 as dt
                        timestamp = int(dt.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: {e}")
            return []


# ──────────────────────────────────────────────
# Module-level helpers
# ──────────────────────────────────────────────

def get_unipile_client() -> UnipileClient:
    """Create a UnipileClient from config.json settings."""
    cfg = config.load_config()
    api_url = cfg.get("unipile_api_url", "")
    api_key = cfg.get("unipile_api_key", "")
    if not api_url or not api_key:
        raise UnipileError(
            "Unipile not configured.\n\n"
            "Run setup_profile to configure your Unipile API key and URL."
        )
    return UnipileClient(api_url, api_key)


def get_account_id() -> str | None:
    """Load the stored Unipile account_id from settings DB."""
    from ..db.queries import get_setting
    return get_setting("unipile_account_id", None)
