"""Inbound lead qualification engine.

Qualifies inbound LinkedIn signals (connection requests, unsolicited DMs,
post comments) against the user's active ICPs to determine lead potential.
"""

from __future__ import annotations

import json
import logging
import re
from dataclasses import asdict, dataclass
from typing import Any

from .llm import LLMClient

logger = logging.getLogger(__name__)


@dataclass
class InboundQualification:
    """Result of qualifying an inbound signal."""

    intent: str  # 'buying_signal', 'networking', 'job_seeking', 'spam', 'partnership', 'unknown'
    matched_icp_id: str | None
    confidence: float  # 0.0-1.0
    recommended_action: str  # 'engage_immediately', 'ask_purpose', 'accept_and_monitor', 'ignore'
    reasoning: str

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


# ──────────────────────────────────────────────
# Fast path: rule-based spam/recruiter detection
# ──────────────────────────────────────────────

_SPAM_HEADLINE_KEYWORDS = [
    "recruiter", "talent acquisition", "staffing", "headhunter",
    "recruiting", "hiring manager",
]

_JOB_SEEKING_KEYWORDS = [
    "looking for opportunities", "open to work", "seeking new role",
    "job seeker", "actively looking", "#opentowork",
]

_SPAM_MESSAGE_PATTERNS = [
    r"we have.*?opportunity",
    r"perfect candidate",
    r"immediate opening",
    r"your resume",
    r"job (opening|position|vacancy)",
    r"apply now",
    r"we are hiring",
]


def _classify_fast(
    headline: str,
    content: str,
) -> InboundQualification | None:
    """Rule-based fast classification for obvious cases."""
    headline_lower = (headline or "").lower()
    content_lower = (content or "").lower()

    # Check recruiter/staffing headline
    for kw in _SPAM_HEADLINE_KEYWORDS:
        if kw in headline_lower:
            return InboundQualification(
                intent="job_seeking",
                matched_icp_id=None,
                confidence=0.85,
                recommended_action="ignore",
                reasoning=f"Headline contains recruiter keyword: '{kw}'",
            )

    # Check job-seeking signals
    for kw in _JOB_SEEKING_KEYWORDS:
        if kw in headline_lower or kw in content_lower:
            return InboundQualification(
                intent="job_seeking",
                matched_icp_id=None,
                confidence=0.8,
                recommended_action="ignore",
                reasoning=f"Job-seeking signal detected: '{kw}'",
            )

    # Check spam message patterns
    for pattern in _SPAM_MESSAGE_PATTERNS:
        if content_lower and re.search(pattern, content_lower):
            return InboundQualification(
                intent="spam",
                matched_icp_id=None,
                confidence=0.8,
                recommended_action="ignore",
                reasoning=f"Spam pattern detected in message",
            )

    return None


# ──────────────────────────────────────────────
# Structured ICP keyword matching
# ──────────────────────────────────────────────

def _compute_keyword_overlap(
    profile: dict[str, Any],
    icp: dict[str, Any],
) -> float:
    """Compute a simple keyword overlap score between a profile and an ICP.

    Returns 0.0-1.0 score based on how many ICP keywords appear in the
    sender's headline, title, or company.
    """
    # Build the profile text to search
    profile_text = " ".join([
        (profile.get("headline") or ""),
        (profile.get("title") or ""),
        (profile.get("company") or ""),
        (profile.get("name") or ""),
    ]).lower()

    if not profile_text.strip():
        return 0.0

    # Collect ICP match terms
    match_terms: list[str] = []

    # Keywords from ICP
    keywords = icp.get("keywords") or []
    match_terms.extend(keywords)

    # Job titles
    job_titles = icp.get("job_titles") or {}
    for title in (job_titles.get("include") or []):
        if isinstance(title, dict):
            match_terms.append(title.get("name", ""))
        else:
            match_terms.append(str(title))

    # Industries
    industries = icp.get("industries") or {}
    for ind in (industries.get("include") or []):
        if isinstance(ind, dict):
            match_terms.append(ind.get("name", ""))
        else:
            match_terms.append(str(ind))

    if not match_terms:
        return 0.0

    # Count matches
    matches = sum(1 for term in match_terms if term.lower() in profile_text)
    return min(1.0, matches / max(len(match_terms), 1))


# ──────────────────────────────────────────────
# LLM qualification
# ──────────────────────────────────────────────

_QUALIFY_SYSTEM = """You are an expert B2B sales analyst. Given an inbound LinkedIn signal (connection request, DM, or post comment), determine if the sender could be a potential customer/lead.

You must output a valid JSON object with these exact fields:
- "intent": one of "buying_signal", "networking", "job_seeking", "spam", "partnership", "unknown"
- "matched_icp_id": the ID of the best-matching ICP, or null if no match
- "confidence": float between 0.0 and 1.0
- "recommended_action": one of "engage_immediately", "ask_purpose", "accept_and_monitor", "ignore"
- "reasoning": brief explanation (1-2 sentences)

Intent definitions:
- buying_signal: Shows signs of being a potential buyer (title matches ICP, mentions pain points, asks about product/service)
- networking: General professional networking (industry peer, mutual connections, no buying intent)
- job_seeking: Looking for a job or is a recruiter
- spam: Irrelevant, mass messaging, or promotional
- partnership: Interested in collaboration, not buying
- unknown: Can't determine intent from available info

Recommended action logic:
- engage_immediately: High-confidence buying signal (confidence >= 0.7)
- ask_purpose: Looks promising but unclear intent (confidence 0.4-0.7)
- accept_and_monitor: Low confidence match, worth keeping in network
- ignore: Spam, recruiters, or clearly irrelevant

No markdown. No code fences. Output ONLY the JSON object."""

_QUALIFY_PROMPT = """Qualify this inbound LinkedIn signal:

Signal type: {signal_type}

Sender profile:
- Name: {name}
- Headline: {headline}
- Company: {company}

{content_section}

Active ICPs (Ideal Customer Profiles) to match against:
{icp_section}

Classify intent, match to an ICP if relevant, and recommend an action."""


def _build_icp_section(icps: list[dict[str, Any]]) -> str:
    """Build a concise ICP summary for the LLM prompt."""
    if not icps:
        return "No active ICPs defined. Qualify based on general B2B sales signals."

    sections = []
    for icp_data in icps:
        icp_json_str = icp_data.get("icp_json", "{}")
        try:
            parsed = json.loads(icp_json_str) if isinstance(icp_json_str, str) else icp_json_str
        except (json.JSONDecodeError, TypeError):
            parsed = {}

        # Extract individual ICPs from the result
        icps_list = parsed.get("icps", [parsed]) if parsed else [{}]
        for single_icp in icps_list[:2]:  # Max 2 per result to keep prompt short
            icp_id = icp_data.get("id", "")
            name = single_icp.get("name", icp_data.get("name", "Unknown"))
            desc = single_icp.get("description", "")[:150]
            pain_points = ", ".join((single_icp.get("pain_points") or [])[:3])
            keywords = ", ".join((single_icp.get("keywords") or [])[:5])

            section = f"  ICP '{name}' (id={icp_id}):"
            if desc:
                section += f"\n    Description: {desc}"
            if pain_points:
                section += f"\n    Pain points: {pain_points}"
            if keywords:
                section += f"\n    Keywords: {keywords}"
            sections.append(section)

    return "\n".join(sections) if sections else "No ICP details available."


def _parse_qualification_response(raw: str, icps: list[dict]) -> InboundQualification:
    """Parse the LLM JSON response into an InboundQualification."""
    # Strip markdown fences if present
    text = raw.strip()
    if text.startswith("```"):
        text = re.sub(r"^```\w*\n?", "", text)
        text = re.sub(r"\n?```$", "", text)

    try:
        data = json.loads(text)
    except json.JSONDecodeError:
        # Try to extract JSON from response
        match = re.search(r'\{[^{}]*"intent"[^{}]*\}', text, re.DOTALL)
        if match:
            try:
                data = json.loads(match.group())
            except json.JSONDecodeError:
                return InboundQualification(
                    intent="unknown",
                    matched_icp_id=None,
                    confidence=0.3,
                    recommended_action="ask_purpose",
                    reasoning="Could not parse qualification response",
                )
        else:
            return InboundQualification(
                intent="unknown",
                matched_icp_id=None,
                confidence=0.3,
                recommended_action="ask_purpose",
                reasoning="Could not parse qualification response",
            )

    valid_intents = {"buying_signal", "networking", "job_seeking", "spam", "partnership", "unknown"}
    valid_actions = {"engage_immediately", "ask_purpose", "accept_and_monitor", "ignore"}

    intent = data.get("intent", "unknown")
    if intent not in valid_intents:
        intent = "unknown"

    action = data.get("recommended_action", "ask_purpose")
    if action not in valid_actions:
        action = "ask_purpose"

    confidence = float(data.get("confidence", 0.3))
    confidence = max(0.0, min(1.0, confidence))

    matched_icp_id = data.get("matched_icp_id")
    # Validate that matched ICP actually exists
    if matched_icp_id:
        icp_ids = {icp.get("id") for icp in icps}
        if matched_icp_id not in icp_ids:
            matched_icp_id = None

    return InboundQualification(
        intent=intent,
        matched_icp_id=matched_icp_id,
        confidence=confidence,
        recommended_action=action,
        reasoning=data.get("reasoning", ""),
    )


async def qualify_inbound(
    profile: dict[str, Any],
    content: str | None,
    signal_type: str,
    active_icps: list[dict[str, Any]],
) -> InboundQualification:
    """Qualify an inbound signal against ICPs.

    Tries fast rule-based classification first, then structured keyword
    matching, and falls back to LLM for ambiguous cases.

    Args:
        profile: Sender info with name, headline, company, title.
        content: Their message, comment text, or invite note (may be empty).
        signal_type: 'invitation', 'message', or 'comment'.
        active_icps: List of ICP dicts from list_icps(status='active').

    Returns:
        InboundQualification with intent, ICP match, confidence, and action.
    """
    headline = profile.get("headline", "") or profile.get("title", "")
    content_str = content or ""

    # 1. Fast path: rule-based spam/recruiter detection
    fast_result = _classify_fast(headline, content_str)
    if fast_result:
        logger.info("Inbound qualified (fast): %s → %s", profile.get("name"), fast_result.intent)
        return fast_result

    # 2. Structured keyword matching (boost for LLM or standalone)
    best_overlap = 0.0
    best_icp_id = None
    for icp in active_icps:
        icp_json_str = icp.get("icp_json", "{}")
        try:
            parsed = json.loads(icp_json_str) if isinstance(icp_json_str, str) else icp_json_str
        except (json.JSONDecodeError, TypeError):
            continue
        for single_icp in (parsed.get("icps", [parsed]) if parsed else []):
            overlap = _compute_keyword_overlap(profile, single_icp)
            if overlap > best_overlap:
                best_overlap = overlap
                best_icp_id = icp.get("id")

    # 3. LLM qualification
    # Route through backend if in backend mode
    from ..config import has_local_llm_key, is_backend_mode
    if is_backend_mode() and not has_local_llm_key():
        from ..linkedin import get_linkedin_client
        backend_client = get_linkedin_client()
        try:
            return await backend_client.qualify_inbound(
                profile=profile,
                content=content_str,
                signal_type=signal_type,
                icp_summaries=[
                    {
                        "id": icp.get("id"),
                        "name": icp.get("name"),
                        "icp_json": icp.get("icp_json", "{}"),
                    }
                    for icp in active_icps
                ],
            )
        except Exception as e:
            logger.warning("Backend qualify_inbound failed: %s, using keyword match", e)
            # Fall through to keyword-only result
        finally:
            await backend_client.close()

    # Try local LLM
    try:
        client = LLMClient()
        content_section = f'Message/Comment: "{content_str[:500]}"' if content_str else "No message content (silent connection request)."
        icp_section = _build_icp_section(active_icps)

        prompt = _QUALIFY_PROMPT.format(
            signal_type=signal_type,
            name=profile.get("name", "Unknown"),
            headline=headline,
            company=profile.get("company", "Unknown"),
            content_section=content_section,
            icp_section=icp_section,
        )

        raw = await client.generate(prompt, system=_QUALIFY_SYSTEM, temperature=0.2, max_tokens=500)
        result = _parse_qualification_response(raw, active_icps)
        logger.info("Inbound qualified (LLM): %s → %s (%.0f%%)", profile.get("name"), result.intent, result.confidence * 100)
        return result

    except Exception as e:
        logger.warning("LLM qualify_inbound failed: %s, using keyword overlap", e)

    # 4. Fallback: keyword overlap only
    if best_overlap >= 0.3:
        return InboundQualification(
            intent="unknown",
            matched_icp_id=best_icp_id,
            confidence=best_overlap,
            recommended_action="ask_purpose" if best_overlap >= 0.4 else "accept_and_monitor",
            reasoning=f"Keyword overlap with ICP: {best_overlap:.0%}",
        )

    return InboundQualification(
        intent="unknown",
        matched_icp_id=None,
        confidence=0.2,
        recommended_action="accept_and_monitor",
        reasoning="No strong signals detected — worth monitoring.",
    )


# ──────────────────────────────────────────────
# Discovery DM Generation
# ──────────────────────────────────────────────

_DISCOVERY_SYSTEM = """You write warm, natural LinkedIn DMs that start a genuine conversation. Your goal is to understand WHY someone connected or commented — without being pushy or salesy.

Rules:
- Sound human and conversational, not scripted
- Match the sender's voice and tone
- Reference the signal (their invite message, comment, or just the connection)
- Ask an open-ended question about their motivation or needs
- Keep it under 300 characters
- Never use salesy language, emojis, or exclamation marks
- Never pitch — just be curious and friendly

Output a JSON object with "message" and "reasoning" fields. No markdown or code fences."""

_DISCOVERY_PROMPT = """Generate a discovery DM for a new LinkedIn connection.

My voice: {voice_desc}

Signal type: {signal_type}
Their name: {name}
Their headline: {headline}
{content_section}
{qualification_section}

Write a brief, warm message that acknowledges their signal and asks about their purpose/needs."""


async def generate_discovery_question(
    sender_profile: dict[str, Any],
    signal_type: str,
    content: str | None,
    voice: dict[str, Any],
    qualification: InboundQualification | None = None,
) -> dict[str, str]:
    """Generate a warm discovery DM to ask about the sender's purpose.

    Returns dict with 'message' and 'reasoning' keys.
    """
    voice_desc = f"Tone: {voice.get('tone', 'professional')}, style: {voice.get('sentence_length', 'concise')}"

    content_section = f'They said: "{(content or "")[:200]}"' if content else "No message — silent connection."

    qual_section = ""
    if qualification:
        qual_section = f"Qualification: {qualification.intent} ({qualification.confidence:.0%} confidence)"
        if qualification.reasoning:
            qual_section += f"\nContext: {qualification.reasoning}"

    prompt = _DISCOVERY_PROMPT.format(
        voice_desc=voice_desc,
        signal_type=signal_type,
        name=sender_profile.get("name", "there"),
        headline=sender_profile.get("headline", ""),
        content_section=content_section,
        qualification_section=qual_section,
    )

    # Route through backend if in backend mode
    from ..config import has_local_llm_key, is_backend_mode
    if is_backend_mode() and not has_local_llm_key():
        from ..linkedin import get_linkedin_client
        backend_client = get_linkedin_client()
        try:
            return await backend_client.generate_discovery_dm(
                profile=sender_profile,
                signal_type=signal_type,
                content=content or "",
                voice=voice,
                qualification=qualification.to_dict() if qualification else {},
            )
        except Exception as e:
            logger.warning("Backend generate_discovery_dm failed: %s", e)
        finally:
            await backend_client.close()

    # Local LLM
    try:
        client = LLMClient()
        raw = await client.generate(prompt, system=_DISCOVERY_SYSTEM, temperature=0.7, max_tokens=300)
        text = raw.strip()
        if text.startswith("```"):
            text = re.sub(r"^```\w*\n?", "", text)
            text = re.sub(r"\n?```$", "", text)
        try:
            data = json.loads(text)
            return {
                "message": data.get("message", text),
                "reasoning": data.get("reasoning", ""),
            }
        except json.JSONDecodeError:
            return {"message": text[:300], "reasoning": ""}
    except Exception as e:
        logger.warning("Discovery DM generation failed: %s", e)
        name = sender_profile.get("name", "").split()[0] if sender_profile.get("name") else "there"
        return {
            "message": f"Hey {name}, thanks for connecting! What prompted you to reach out?",
            "reasoning": "Fallback generic discovery question",
        }
