"""Job change signal collector — detects title/company changes for campaign contacts.

Periodically re-scans campaign contact profiles, comparing current title/company
against the stored `profile_json` snapshot. If changes are detected, saves a
SIGNAL_JOB_CHANGE signal — the highest-converting signal type (3X conversion rate).

Runs every 4 hours via the scheduler, scanning up to 50 contacts per run.
"""

from __future__ import annotations

import json
import logging
import time
from typing import Any

logger = logging.getLogger(__name__)

# Max profiles to fetch per run (rate limit safety)
JOB_CHANGE_BATCH_SIZE = 50

# Min days between re-scans for the same contact
RESCAN_INTERVAL_DAYS = 7


async def collect_job_changes() -> str:
    """Scan campaign contacts for job title/company changes.

    For each active campaign:
    1. Fetch contacts not scanned in the last RESCAN_INTERVAL_DAYS
    2. Re-fetch their LinkedIn profile via get_profile()
    3. Compare current title/company vs stored profile_json
    4. If changed → save SIGNAL_JOB_CHANGE with old/new details
    5. Update contact's profile_json snapshot + last_scanned_at

    Returns summary string.
    """
    from ..constants import (
        SIGNAL_JOB_CHANGE,
        SIGNAL_TTL_JOB_CHANGE,
    )
    from ..db.queries import get_contacts_for_campaign, list_campaigns
    from ..db.schema import get_db
    from ..db.signal_queries import (
        save_signal,
        signal_exists,
        upsert_signal_account,
    )
    from ..linkedin import UnipileError, get_account_id, get_linkedin_client

    account_id = get_account_id()
    if not account_id:
        return "No LinkedIn account connected."

    # Get active campaigns
    campaigns = list_campaigns(status="active")
    if not campaigns:
        return "No active campaigns."

    try:
        client = get_linkedin_client()
    except UnipileError as e:
        return f"LinkedIn client error: {e}"

    now = int(time.time())
    rescan_cutoff = now - (RESCAN_INTERVAL_DAYS * 86400)

    total_scanned = 0
    total_changes = 0
    errors = 0

    try:
        for camp in campaigns:
            if total_scanned >= JOB_CHANGE_BATCH_SIZE:
                break

            cid = camp["id"]
            contacts = get_contacts_for_campaign(cid)
            if not contacts:
                continue

            for contact in contacts:
                if total_scanned >= JOB_CHANGE_BATCH_SIZE:
                    break

                contact_id = contact["id"]
                linkedin_id = contact.get("linkedin_id", "")
                linkedin_url = contact.get("linkedin_url", "")

                # Skip contacts without LinkedIn identifier
                identifier = linkedin_id or _extract_public_id(linkedin_url)
                if not identifier:
                    continue

                # Skip recently scanned contacts
                last_scanned = contact.get("last_scanned_at") or 0
                if last_scanned > rescan_cutoff:
                    continue

                # Parse existing profile snapshot
                old_profile = _parse_profile_json(contact.get("profile_json", ""))
                old_title = old_profile.get("title", "") or contact.get("title", "")
                old_company = old_profile.get("company", "") or contact.get("company", "")

                try:
                    # Fetch current profile from LinkedIn
                    profile = await client.get_profile(account_id, identifier)
                    total_scanned += 1

                    if not profile or isinstance(profile, list):
                        # API error (422 sentinel or empty)
                        continue

                    # Extract current title/company
                    new_title = (
                        profile.get("headline", "")
                        or profile.get("title", "")
                        or ""
                    )
                    new_company = (
                        profile.get("company", "")
                        or _extract_company_from_experience(profile)
                        or ""
                    )
                    new_name = (
                        profile.get("name", "")
                        or profile.get("full_name", "")
                        or contact.get("name", "")
                    )

                    # Detect changes
                    title_changed = _has_significant_change(old_title, new_title)
                    company_changed = _has_significant_change(old_company, new_company)

                    if title_changed or company_changed:
                        # Check dedup: don't re-signal if already detected recently
                        if not signal_exists(
                            SIGNAL_JOB_CHANGE,
                            linkedin_id=linkedin_id or identifier,
                            lookback_seconds=RESCAN_INTERVAL_DAYS * 86400,
                        ):
                            metadata = {
                                "old_title": old_title,
                                "new_title": new_title,
                                "old_company": old_company,
                                "new_company": new_company,
                                "title_changed": title_changed,
                                "company_changed": company_changed,
                                "campaign_id": cid,
                                "contact_id": contact_id,
                            }

                            # Build content summary
                            changes = []
                            if title_changed:
                                changes.append(f"Title: {old_title} → {new_title}")
                            if company_changed:
                                changes.append(f"Company: {old_company} → {new_company}")
                            content = f"{new_name} changed roles: {'; '.join(changes)}"

                            save_signal(
                                signal_type=SIGNAL_JOB_CHANGE,
                                source="profile_scan",
                                prospect_id=contact_id,
                                prospect_name=new_name or None,
                                prospect_title=new_title or None,
                                linkedin_id=linkedin_id or identifier,
                                campaign_id=cid,
                                content=content[:1000],
                                metadata_json=json.dumps(metadata),
                                expires_at=now + SIGNAL_TTL_JOB_CHANGE,
                            )
                            total_changes += 1

                            # Update signal account
                            upsert_signal_account(
                                linkedin_id=linkedin_id or identifier,
                                prospect_name=new_name or None,
                                company=new_company or None,
                            )

                            logger.info(
                                "Job change detected for %s: %s",
                                new_name,
                                "; ".join(changes),
                            )

                    # Update profile snapshot and last_scanned_at
                    _update_contact_profile(
                        contact_id=contact_id,
                        profile_json=json.dumps(profile),
                        timestamp=now,
                    )

                except UnipileError as e:
                    logger.warning(
                        "Profile fetch failed for %s: %s", identifier, e
                    )
                    errors += 1
                except Exception as e:
                    logger.warning(
                        "Job change scan error for %s: %s", identifier, e
                    )
                    errors += 1

    finally:
        await client.close()

    summary = f"Scanned {total_scanned} profiles, detected {total_changes} job changes"
    if errors:
        summary += f", {errors} errors"
    return summary


def _extract_public_id(linkedin_url: str) -> str:
    """Extract the public identifier from a LinkedIn URL.

    e.g., 'https://www.linkedin.com/in/johndoe' → 'johndoe'
    """
    if not linkedin_url:
        return ""
    url = linkedin_url.rstrip("/")
    if "/in/" in url:
        return url.split("/in/")[-1].split("?")[0]
    return ""


def _parse_profile_json(raw: str) -> dict[str, Any]:
    """Safely parse profile_json field."""
    if not raw:
        return {}
    try:
        return json.loads(raw)
    except (json.JSONDecodeError, TypeError):
        return {}


def _extract_company_from_experience(profile: dict[str, Any]) -> str:
    """Extract current company from profile experience section."""
    experiences = profile.get("experiences", [])
    if not experiences:
        experiences = profile.get("experience", [])
    if experiences and isinstance(experiences, list):
        # First experience is usually current
        first = experiences[0]
        if isinstance(first, dict):
            return first.get("company_name", "") or first.get("company", "")
    return ""


def _has_significant_change(old: str, new: str) -> bool:
    """Check if two values represent a significant change.

    Ignores case differences, minor whitespace changes, and empty → empty.
    """
    if not old and not new:
        return False
    old_norm = (old or "").strip().lower()
    new_norm = (new or "").strip().lower()
    if old_norm == new_norm:
        return False
    # Treat empty old as "we had no data" → not a real change
    if not old_norm:
        return False
    return True


def _update_contact_profile(
    contact_id: str,
    profile_json: str,
    timestamp: int,
) -> None:
    """Update a contact's profile_json and last_scanned_at directly via SQL.

    We bypass update_contact() since profile_json is not in _VALID_CONTACT_COLS.
    """
    from ..db.schema import get_db

    db = get_db()
    try:
        db.execute(
            "UPDATE contacts SET profile_json = ?, last_scanned_at = ? WHERE id = ?",
            (profile_json, timestamp, contact_id),
        )
        db.commit()
    finally:
        db.close()
