"""Connection sync service — persist 1st-degree LinkedIn connections locally.

Fetches connections from Unipile's relations API and stores them in the local
SQLite `connections` table so we can do fast set-based dedup without hitting
the API every time. Also provides helpers to check/mark individual connections.

Key functions:
- sync_connections(): Full sync from Unipile → local DB
- get_local_connection_ids(): Fast set of provider_id + public_id from local DB
- is_first_degree(): Quick single-row check
- mark_connected(): Record a newly connected prospect
- get_sync_age(): Seconds since last sync
- audit_campaign_connections(): Batch audit for existing campaigns
"""

from __future__ import annotations

import logging
import time
import uuid
from typing import Any

from ..db.schema import get_db

logger = logging.getLogger(__name__)

# Re-sync if local data is older than this (1 hour)
SYNC_STALE_SECONDS = 3600


async def sync_connections(
    client: Any,
    account_id: str,
    max_relations: int = 2000,
) -> int:
    """Fetch 1st-degree connections from Unipile and upsert into local DB.

    Tries the relations API first; if it fails/returns empty, falls back to
    extracting connections from inbox chats (each chat attendee = 1st degree).

    Args:
        client: LinkedIn client (BackendClient or UnipileClient).
        account_id: Unipile account ID.
        max_relations: Max relations to fetch (paginated).

    Returns:
        Number of connections synced.
    """
    relations: list[dict[str, Any]] = []

    # Strategy 1: Relations API (full data with names)
    try:
        relations = await client.get_relations(account_id, limit=max_relations)
    except Exception as e:
        logger.warning("Relations API failed, will try inbox fallback: %s", e)

    # Strategy 2: Inbox chats fallback (provider_ids only, no names)
    if not relations:
        logger.info("Falling back to inbox chats for connection sync")
        relations = await _sync_from_inbox(client, account_id, max_chats=max_relations)

    if not relations:
        logger.debug("No connections found for account %s", account_id)
        return 0

    db = get_db()
    now = int(time.time())
    synced = 0

    try:
        for rel in relations:
            provider_id = (rel.get("provider_id") or "").strip()
            if not provider_id:
                continue

            public_id = (rel.get("public_id") or "").strip()
            name = (rel.get("name") or "").strip()
            headline = (rel.get("headline") or "").strip()

            # Upsert: INSERT OR REPLACE on the UNIQUE(account_id, provider_id) constraint
            db.execute(
                """INSERT INTO connections (id, account_id, provider_id, public_id, name, headline, synced_at)
                   VALUES (?, ?, ?, ?, ?, ?, ?)
                   ON CONFLICT(account_id, provider_id) DO UPDATE SET
                       public_id = excluded.public_id,
                       name = excluded.name,
                       headline = excluded.headline,
                       synced_at = excluded.synced_at""",
                (str(uuid.uuid4()), account_id, provider_id, public_id, name, headline, now),
            )
            synced += 1

        db.commit()
        logger.info("Connection sync: %d connections synced for account %s", synced, account_id)
    except Exception as e:
        logger.warning("Connection sync failed (DB): %s", e)
    finally:
        db.close()

    # Update global_contacts.network_degree for synced connections
    _update_global_contacts_degree(relations)

    return synced


async def _sync_from_inbox(
    client: Any,
    account_id: str,
    max_chats: int = 500,
) -> list[dict[str, Any]]:
    """Extract 1st-degree connections from inbox chats as a fallback.

    Each LinkedIn chat has an attendee_provider_id which is a 1st-degree connection.
    This works when the relations API times out (common for accounts with many connections).
    Fetches raw chat data directly from the backend/Unipile chats endpoint.
    """
    relations: list[dict[str, Any]] = []
    seen_pids: set[str] = set()

    try:
        # Use raw chats endpoint to get attendee_provider_id
        # (the normalized get_chats() loses this field)
        all_chats: list[dict[str, Any]] = []
        cursor: str | None = None
        page_size = min(max_chats, 50)
        pages = 0
        max_pages = (max_chats // page_size) + 1

        while pages < max_pages:
            chats_page, next_cursor = await _fetch_raw_chats(
                client, page_size, cursor,
            )
            if not chats_page:
                break
            all_chats.extend(chats_page)
            cursor = next_cursor
            pages += 1
            if not cursor or len(all_chats) >= max_chats:
                break

        for chat in all_chats:
            if not isinstance(chat, dict):
                continue

            # Extract attendee provider_id
            att_pid = chat.get("attendee_provider_id", "")
            if not att_pid or att_pid in seen_pids:
                continue
            seen_pids.add(att_pid)

            relations.append({
                "provider_id": str(att_pid),
                "name": "",
                "headline": "",
                "public_id": "",
            })

        logger.info(
            "Inbox fallback: found %d unique connections from %d chats",
            len(relations), len(all_chats),
        )
    except Exception as e:
        logger.warning("Inbox fallback failed: %s", e)

    return relations


async def _fetch_raw_chats(
    client: Any,
    limit: int = 50,
    cursor: str | None = None,
) -> tuple[list[dict[str, Any]], str | None]:
    """Fetch raw chat objects from the backend/Unipile chats endpoint.

    Returns (items, next_cursor). Items have attendee_provider_id for connections.
    Uses backend config directly for reliability (avoids client internal access issues).
    """
    import httpx

    try:
        # Get backend config directly — more reliable than accessing client internals
        from ..config import get_backend_config
        base_url, jwt_token = get_backend_config()
        if not base_url or not jwt_token:
            # Try client internals as fallback
            base_url = getattr(client, "base_url", "")
            jwt_token = getattr(client, "jwt_token", "")
            if not base_url or not jwt_token:
                logger.debug("No backend config or client credentials for raw chats fetch")
                return [], None

        url = f"{base_url.rstrip('/')}/api/v1/chats?limit={limit}"
        if cursor:
            url += f"&cursor={cursor}"

        headers = {
            "Authorization": f"Bearer {jwt_token}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        }

        async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as http:
            resp = await http.get(url, headers=headers)

        if resp.status_code != 200:
            logger.warning("Raw chats fetch returned %d: %s", resp.status_code, resp.text[:200])
            return [], None

        data = resp.json()
        items = data.get("items", []) if isinstance(data, dict) else []
        next_cursor = data.get("cursor") if isinstance(data, dict) else None

        return items, next_cursor
    except Exception as e:
        logger.warning("Raw chats fetch failed: %s", e)
        return [], None


def _update_global_contacts_degree(relations: list[dict[str, Any]]) -> None:
    """Set network_degree=1 on global_contacts for confirmed 1st-degree connections."""
    db = get_db()
    try:
        updated = 0
        for rel in relations:
            provider_id = (rel.get("provider_id") or "").strip()
            public_id = (rel.get("public_id") or "").strip()
            if not provider_id and not public_id:
                continue

            # Try matching by provider_id first (stored in profile_json → provider_id),
            # then by public_id (stored as linkedin_id in global_contacts)
            for identifier in [provider_id, public_id]:
                if not identifier:
                    continue
                cursor = db.execute(
                    """UPDATE global_contacts SET network_degree = 1, updated_at = ?
                       WHERE (linkedin_id = ? OR linkedin_id = ?) AND (network_degree IS NULL OR network_degree != 1)""",
                    (int(time.time()), identifier, identifier.lower()),
                )
                if cursor.rowcount > 0:
                    updated += cursor.rowcount

        if updated > 0:
            db.commit()
            logger.info("Updated network_degree=1 for %d global contacts", updated)
    except Exception as e:
        logger.debug("Global contacts degree update skipped: %s", e)
    finally:
        db.close()


def get_local_connection_ids(account_id: str) -> set[str]:
    """Fast set-based lookup of all locally stored 1st-degree connection identifiers.

    Returns a set of lowercase provider_ids + public_ids + linkedin URLs.
    """
    db = get_db()
    try:
        rows = db.execute(
            """SELECT provider_id, public_id FROM connections WHERE account_id = ?""",
            (account_id,),
        ).fetchall()
    except Exception:
        return set()
    finally:
        db.close()

    ids: set[str] = set()
    for row in rows:
        pid = row[0] or ""
        pub = row[1] or ""
        if pid:
            ids.add(pid.lower().strip())
        if pub:
            ids.add(pub.lower().strip())
            ids.add(f"https://www.linkedin.com/in/{pub.lower().strip()}")
    return ids


def is_first_degree(account_id: str, provider_id: str) -> bool:
    """Quick check if a specific prospect is a 1st-degree connection in local DB."""
    if not provider_id:
        return False
    db = get_db()
    try:
        row = db.execute(
            "SELECT 1 FROM connections WHERE account_id = ? AND provider_id = ? LIMIT 1",
            (account_id, provider_id.strip()),
        ).fetchone()
        return row is not None
    except Exception:
        return False
    finally:
        db.close()


def is_first_degree_by_public_id(account_id: str, public_id: str) -> bool:
    """Quick check if a specific prospect is a 1st-degree connection by public_id."""
    if not public_id:
        return False
    db = get_db()
    try:
        row = db.execute(
            "SELECT 1 FROM connections WHERE account_id = ? AND LOWER(public_id) = LOWER(?) LIMIT 1",
            (account_id, public_id.strip()),
        ).fetchone()
        return row is not None
    except Exception:
        return False
    finally:
        db.close()


def mark_connected(
    account_id: str,
    provider_id: str,
    name: str = "",
    public_id: str = "",
    headline: str = "",
) -> None:
    """Record a newly connected prospect in local connections table.

    Called when we discover a prospect is already connected (via profile check or 409).
    """
    if not provider_id:
        return
    db = get_db()
    try:
        db.execute(
            """INSERT INTO connections (id, account_id, provider_id, public_id, name, headline, synced_at)
               VALUES (?, ?, ?, ?, ?, ?, ?)
               ON CONFLICT(account_id, provider_id) DO UPDATE SET
                   name = COALESCE(NULLIF(excluded.name, ''), connections.name),
                   public_id = COALESCE(NULLIF(excluded.public_id, ''), connections.public_id),
                   headline = COALESCE(NULLIF(excluded.headline, ''), connections.headline),
                   synced_at = excluded.synced_at""",
            (str(uuid.uuid4()), account_id, provider_id.strip(), public_id, name, headline, int(time.time())),
        )
        db.commit()
    except Exception as e:
        logger.debug("mark_connected failed: %s", e)
    finally:
        db.close()


def get_sync_age(account_id: str) -> int | None:
    """Return seconds since last connection sync, or None if never synced."""
    db = get_db()
    try:
        row = db.execute(
            "SELECT MAX(synced_at) FROM connections WHERE account_id = ?",
            (account_id,),
        ).fetchone()
        if row and row[0]:
            return int(time.time()) - int(row[0])
        return None
    except Exception:
        return None
    finally:
        db.close()


async def ensure_synced(client: Any, account_id: str) -> set[str]:
    """Ensure connections are synced (auto-sync if stale), then return local IDs.

    This is the main entry point for dedup: checks sync age and refreshes if needed,
    then returns the fast local set.
    """
    age = get_sync_age(account_id)
    if age is None or age > SYNC_STALE_SECONDS:
        logger.info("Connection sync stale (age=%s) — re-syncing for account %s", age, account_id)
        await sync_connections(client, account_id)
    return get_local_connection_ids(account_id)


async def audit_campaign_connections(
    client: Any,
    account_id: str,
    campaign_id: str,
) -> dict[str, Any]:
    """Audit a campaign's pending outreaches against current 1st-degree connections.

    1. Ensure connections are synced
    2. Get all pending outreaches for the campaign
    3. Cross-reference against local connections
    4. For matches: update outreach to 'connected'
    5. Return audit report

    Returns:
        {"total_pending": int, "already_connected": int, "updated": list[str]}
    """
    from ..db.schema import get_db as _get_db

    # Step 1: Ensure fresh connections
    await ensure_synced(client, account_id)
    connection_ids = get_local_connection_ids(account_id)

    # Step 2: Get pending outreaches
    db = _get_db()
    try:
        rows = db.execute(
            """SELECT o.id, o.contact_id, c.linkedin_id, c.name, c.linkedin_url,
                      c.profile_json
               FROM outreaches o
               JOIN contacts c ON o.contact_id = c.id
               WHERE o.campaign_id = ? AND o.status = 'pending'""",
            (campaign_id,),
        ).fetchall()
    finally:
        db.close()

    report = {
        "total_pending": len(rows),
        "already_connected": 0,
        "updated": [],
    }

    if not rows:
        return report

    # Step 3 & 4: Cross-reference and update
    import json

    for row in rows:
        outreach_id = row[0]
        linkedin_id = (row[2] or "").lower().strip()
        name = row[3] or ""
        linkedin_url = (row[4] or "").lower().strip()
        profile_json_str = row[5] or "{}"

        # Extract provider_id from profile_json
        provider_id = ""
        try:
            profile = json.loads(profile_json_str)
            provider_id = (profile.get("provider_id") or "").lower().strip()
        except Exception:
            pass

        # Build identifier set for this prospect
        identifiers = {x for x in (linkedin_id, provider_id, linkedin_url) if x}
        if linkedin_id:
            identifiers.add(f"https://www.linkedin.com/in/{linkedin_id}")

        # Check against local connections
        if identifiers & connection_ids:
            db = _get_db()
            try:
                db.execute(
                    "UPDATE outreaches SET status = 'connected' WHERE id = ?",
                    (outreach_id,),
                )
                db.commit()
            finally:
                db.close()

            report["already_connected"] += 1
            report["updated"].append(name or outreach_id)
            logger.info("Audit: %s is already 1st-degree connected — marked as 'connected'", name)

    return report


def get_connection_count(account_id: str) -> int:
    """Return the number of locally stored connections for an account."""
    db = get_db()
    try:
        row = db.execute(
            "SELECT COUNT(*) FROM connections WHERE account_id = ?",
            (account_id,),
        ).fetchone()
        return row[0] if row else 0
    except Exception:
        return 0
    finally:
        db.close()
