"""Reply generator — sentiment-aware LinkedIn DM replies.

Generates personalized replies to prospect messages based on sentiment:
- positive: Advance toward meeting/call (with optional booking link)
- question: Answer contextually, then soft CTA
- negative: Graceful close, leave door open
- neutral: Build rapport, ask clarifying question

Pipeline mirrors followup_generator.py:
1. Select sentiment-specific prompt
2. Generate reply via LLM (with voice + prospect intelligence)
3. Validate + Fix (caller handles via message_validator)

Backend routing: if in backend mode with no local LLM key,
proxies through the backend API (same pattern as followup_generator.py).
"""

from __future__ import annotations

import logging
from typing import Any

from .json_repair import parse_json
from .length_fixer import shorten_to_limit
from .llm import LLMClient
from .prompt_loader import (
    build_context_block,
    get_prompt_temperature,
    has_prompt,
    render_prompt,
)

logger = logging.getLogger(__name__)

REPLY_MAX_CHARS = 500
NEGATIVE_MAX_CHARS = 200  # Graceful closes should be very short

REPLY_SYSTEM = """You are an expert at writing LinkedIn DM replies. You craft genuine, personal responses that match the sender's voice and advance the relationship appropriately based on the prospect's sentiment.

Key principles:
- Sound like a real person, not a bot or salesperson
- DIRECTLY ADDRESS what the prospect said — never ignore their message
- Paraphrase what the prospect said — never mirror their exact words back at them
- Match the sender's voice and tone exactly
- Adapt your strategy based on the prospect's sentiment
- Keep replies concise (under 500 characters)
- Relate from experience when possible
- Keep tone understated — no buzzy adjectives, no generic flattery
- Never use salesy phrases like "leverage", "synergy", "exciting opportunity"
- Never try to overcome objections or be pushy
- Never use template openers: "Spot on.", "Love this.", "Agreed."
- Never use cliche phrases: "a lifesaver", "running the whole show"

You output a JSON object with two fields: "reasoning" and "message".
No markdown, no code fences, no explanation outside the JSON."""

REPLY_PROMPT_POSITIVE = """Write a LinkedIn DM reply to a prospect who expressed INTEREST.

## SENDER (you are writing AS this person)
Name: {sender_name}
Title: {sender_title}
Company: {sender_company}
Voice: {voice_tone}
Sentence style: {voice_sentence}
Vocabulary preferences: {voice_vocab}
No-go (NEVER use these): {voice_nogo}

## PROSPECT
Name: {prospect_name}
Title: {prospect_title}
Company: {prospect_company}
Headline: {prospect_headline}

## PROSPECT INTELLIGENCE (from analysis)
{prospect_intelligence}

## CAMPAIGN CONTEXT
Target: {campaign_target}
Relevance: {relevance_hook}

## CONVERSATION HISTORY (oldest first)
{conversation_history}

## THEIR LATEST MESSAGE (positive sentiment — they're interested)
"{reply_text}"

## BOOKING LINK
{booking_link_section}

## STRATEGY
The prospect expressed interest. Your goal:
1. Acknowledge their interest genuinely — show you're excited but not desperate
2. Propose a concrete next step (call, meeting, quick demo)
3. If a booking link is available, weave it in naturally
4. Keep it warm, personal, and action-oriented

## CONSTRAINTS
- Match the sender's voice exactly
- DIRECTLY reference what they said in their reply
- Keep under {max_chars} characters
- First name only for the prospect
- NO emojis unless the sender's voice uses them
- NO salesy language, no desperation
- Make scheduling easy — one clear CTA

## OUTPUT
Return ONLY valid JSON (no markdown):
{{
    "reasoning": {{
        "reply_hook": "what they said that you're responding to",
        "strategy": "how you're advancing toward a meeting",
        "cta_choice": "booking_link|suggest_time|ask_availability"
    }},
    "message": "the actual reply text"
}}"""

REPLY_PROMPT_QUESTION = """Write a LinkedIn DM reply to a prospect who asked a QUESTION.

## SENDER (you are writing AS this person)
Name: {sender_name}
Title: {sender_title}
Company: {sender_company}
Voice: {voice_tone}
Sentence style: {voice_sentence}
Vocabulary preferences: {voice_vocab}
No-go (NEVER use these): {voice_nogo}

## PROSPECT
Name: {prospect_name}
Title: {prospect_title}
Company: {prospect_company}
Headline: {prospect_headline}

## PROSPECT INTELLIGENCE (from analysis)
{prospect_intelligence}

## CAMPAIGN CONTEXT
Target: {campaign_target}
Relevance: {relevance_hook}

## CONVERSATION HISTORY (oldest first)
{conversation_history}

## THEIR LATEST MESSAGE (question — they want more info)
"{reply_text}"

## STRATEGY
The prospect asked a question. Your goal:
1. Answer their specific question directly and honestly
2. Use your knowledge of the campaign context to give a relevant answer
3. End with a soft CTA — offer to elaborate, share a resource, or suggest a call
4. Do NOT dodge the question or redirect to "hop on a call" without answering first

## CONSTRAINTS
- Match the sender's voice exactly
- ANSWER their question — do not deflect
- Keep under {max_chars} characters
- First name only for the prospect
- NO emojis unless the sender's voice uses them
- NO salesy language
- Be helpful and straightforward

## OUTPUT
Return ONLY valid JSON (no markdown):
{{
    "reasoning": {{
        "question_summary": "what they're actually asking",
        "answer_approach": "how you're addressing it",
        "cta_choice": "offer_call|share_resource|ask_followup"
    }},
    "message": "the actual reply text"
}}"""

REPLY_PROMPT_NEGATIVE = """Write a LinkedIn DM reply to a prospect who said they're NOT INTERESTED.

## SENDER (you are writing AS this person)
Name: {sender_name}
Title: {sender_title}
Company: {sender_company}
Voice: {voice_tone}
Sentence style: {voice_sentence}
Vocabulary preferences: {voice_vocab}
No-go (NEVER use these): {voice_nogo}

## PROSPECT
Name: {prospect_name}

## THEIR LATEST MESSAGE (negative sentiment — not interested)
"{reply_text}"

## STRATEGY
The prospect is not interested. Your ONLY goal:
1. Acknowledge their position with genuine respect
2. Close gracefully — no pushback, no objection handling, no "but actually..."
3. Leave the door open with ONE brief line: "Happy to reconnect if things change"
4. Keep it VERY SHORT — 1-2 sentences maximum

## CONSTRAINTS
- Match the sender's voice exactly
- MAXIMUM {max_chars} characters (keep it very short!)
- First name only for the prospect
- NO emojis
- ABSOLUTELY NO attempt to sell, persuade, or overcome objections
- NO "I understand, but..." or "What if..."
- Be genuinely respectful — they said no, respect it

## OUTPUT
Return ONLY valid JSON (no markdown):
{{
    "reasoning": {{
        "tone": "respectful and brief",
        "close_style": "graceful_exit"
    }},
    "message": "the actual reply text"
}}"""

REPLY_PROMPT_NEUTRAL = """Write a LinkedIn DM reply to a prospect who gave a NEUTRAL response.

## SENDER (you are writing AS this person)
Name: {sender_name}
Title: {sender_title}
Company: {sender_company}
Voice: {voice_tone}
Sentence style: {voice_sentence}
Vocabulary preferences: {voice_vocab}
No-go (NEVER use these): {voice_nogo}

## PROSPECT
Name: {prospect_name}
Title: {prospect_title}
Company: {prospect_company}
Headline: {prospect_headline}

## PROSPECT INTELLIGENCE (from analysis)
{prospect_intelligence}

## CAMPAIGN CONTEXT
Target: {campaign_target}
Relevance: {relevance_hook}

## CONVERSATION HISTORY (oldest first)
{conversation_history}

## THEIR LATEST MESSAGE (neutral — acknowledgment without clear intent)
"{reply_text}"

## STRATEGY
The prospect gave a neutral response (e.g., "thanks", "cool", "got it").
Your goal:
1. Build on the small signal — they did respond, which is positive
2. Reference something from the conversation or their profile
3. Ask a clarifying question to gauge their interest level
4. Keep it light and genuine — do NOT assume they're interested or disinterested

## CONSTRAINTS
- Match the sender's voice exactly
- DIRECTLY reference what they said
- Keep under {max_chars} characters
- First name only for the prospect
- NO emojis unless the sender's voice uses them
- NO salesy language
- Keep it conversational and low-pressure

## OUTPUT
Return ONLY valid JSON (no markdown):
{{
    "reasoning": {{
        "reply_hook": "what they said that you're building on",
        "angle": "how you're gauging interest without pressure",
        "cta_choice": "clarifying_question|shared_interest|light_value"
    }},
    "message": "the actual reply text"
}}"""

# Map sentiment to prompt template
_PROMPT_MAP = {
    "positive": REPLY_PROMPT_POSITIVE,
    "question": REPLY_PROMPT_QUESTION,
    "negative": REPLY_PROMPT_NEGATIVE,
    "neutral": REPLY_PROMPT_NEUTRAL,
}


def _format_conversation_history(messages: list[dict]) -> str:
    """Format conversation history for the prompt."""
    if not messages:
        return "No previous messages."
    lines = []
    for msg in messages:
        role = msg.get("role", "unknown")
        text = msg.get("text", "")
        label = "YOU (sender)" if role == "sdr" else "PROSPECT"
        lines.append(f"[{label}]: {text}")
    return "\n".join(lines) if lines else "No previous messages."


def _build_intelligence_text(prospect_analysis: dict[str, Any] | None) -> str:
    """Build a prospect intelligence text block for prompt injection."""
    if not prospect_analysis:
        return "No analysis available — use profile data only."
    tone = prospect_analysis.get("tone", {})
    pain_points = prospect_analysis.get("pain_points", [])
    summary = prospect_analysis.get("summary", "")
    parts = []
    if summary:
        parts.append(f"Profile: {summary}")
    if tone.get("recommended_approach"):
        parts.append(f"Approach: {tone['recommended_approach']}")
    if tone.get("formality_level"):
        parts.append(f"Their formality: {tone['formality_level']}/10")
    if tone.get("industry_jargon"):
        jargon = tone["industry_jargon"]
        if isinstance(jargon, list):
            parts.append(f"Their jargon: {', '.join(jargon[:5])}")
    if pain_points and isinstance(pain_points, list):
        parts.append(f"Likely pain points: {'; '.join(str(p) for p in pain_points[:3])}")
    return "\n".join(parts) if parts else "No analysis available — use profile data only."


async def generate_reply(
    prospect: dict[str, Any],
    sender_profile: dict[str, Any],
    voice_signature: dict[str, Any],
    campaign_context: dict[str, Any],
    conversation_history: list[dict[str, Any]],
    reply_text: str,
    sentiment: str,
    prospect_analysis: dict[str, Any] | None = None,
    booking_link: str = "",
) -> dict[str, Any]:
    """Generate a sentiment-aware reply to a prospect's message.

    Args:
        prospect: Prospect profile data (name, title, company, headline)
        sender_profile: User's own profile data
        voice_signature: Voice analysis results (tone, vocabulary, patterns)
        campaign_context: Campaign ICP and target description
        conversation_history: Previous messages in the thread (role + text)
        reply_text: The prospect's latest message text
        sentiment: Classified sentiment (positive, question, negative, neutral)
        prospect_analysis: Cached prospect intelligence (tone + pain points)
        booking_link: Optional calendar booking URL for positive replies

    Returns:
        Dict with "message" (str) and "reasoning" (dict).
    """
    # Route through backend if in backend mode and no local LLM key
    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
        client = get_linkedin_client()
        try:
            return await client.generate_reply(
                sender=sender_profile,
                prospect=prospect,
                voice=voice_signature,
                campaign_context=campaign_context,
                conversation_history=conversation_history,
                reply_text=reply_text,
                sentiment=sentiment,
                booking_link=booking_link,
            )
        finally:
            await client.close()

    # Determine max chars (negative replies should be short)
    max_chars = NEGATIVE_MAX_CHARS if sentiment == "negative" else REPLY_MAX_CHARS

    # ── Try v63 response prompt for positive/question/neutral ──
    # v63 has sophisticated 12-principle response logic with WARM-ENOUGH SIGNAL
    # Keep HeyLead's negative-specific prompt (v63 lacks sentiment-specific negative handling)
    use_v63 = (
        sentiment != "negative"
        and has_prompt("outreach_response")
        and has_prompt("outreach_system")
    )

    if use_v63:
        logger.debug("Using v63 response prompt for %s reply", sentiment)
        ctx = build_context_block(
            sender=sender_profile,
            prospect=prospect,
            campaign_config={
                **campaign_context,
                "booking_link": booking_link,
            },
            voice=voice_signature,
            history=conversation_history,
            analysis=prospect_analysis,
            max_chars=max_chars,
        )
        system = render_prompt("outreach_system", ctx)
        prompt = render_prompt("outreach_response", ctx)
        temp = get_prompt_temperature("outreach_response")

        llm_client = LLMClient()
        raw = await llm_client.generate(prompt, system=system, temperature=temp)

    else:
        # ── Fallback to legacy sentiment-specific prompts ──
        logger.debug("Using legacy %s reply prompt", sentiment)

        # Extract voice details
        voice_vocab = voice_signature.get("vocabulary_preferences", [])
        if isinstance(voice_vocab, list):
            voice_vocab = ", ".join(voice_vocab)

        prospect_name = prospect.get("name", "").split()[0] if prospect.get("name") else "there"
        prospect_intelligence = _build_intelligence_text(prospect_analysis)
        prompt_template = _PROMPT_MAP.get(sentiment, REPLY_PROMPT_NEUTRAL)

        # Build booking link section for positive replies
        booking_link_section = ""
        if booking_link and sentiment == "positive":
            booking_link_section = f"Available: {booking_link} — weave this naturally into the reply."
        elif sentiment == "positive":
            booking_link_section = "No booking link configured — suggest a time or ask their availability."

        format_kwargs: dict[str, Any] = {
            "sender_name": sender_profile.get("name", ""),
            "sender_title": sender_profile.get("title", ""),
            "sender_company": sender_profile.get("company", ""),
            "voice_tone": voice_signature.get("tone", "Professional, direct"),
            "voice_sentence": voice_signature.get("sentence_length", "Medium"),
            "voice_vocab": voice_vocab or "None specified",
            "voice_nogo": voice_signature.get("no_go", "Generic sales phrases"),
            "prospect_name": prospect_name,
            "reply_text": reply_text,
            "max_chars": max_chars,
        }

        if sentiment != "negative":
            format_kwargs.update({
                "prospect_title": prospect.get("title", ""),
                "prospect_company": prospect.get("company", ""),
                "prospect_headline": prospect.get("headline", ""),
                "prospect_intelligence": prospect_intelligence,
                "campaign_target": campaign_context.get("target_description", ""),
                "relevance_hook": campaign_context.get("relevance_hook", ""),
                "conversation_history": _format_conversation_history(conversation_history),
            })

        if sentiment == "positive":
            format_kwargs["booking_link_section"] = booking_link_section

        prompt = prompt_template.format(**format_kwargs)

        llm_client = LLMClient()
        raw = await llm_client.generate(prompt, system=REPLY_SYSTEM, temperature=0.7)

    # Parse JSON response with 2-tier repair
    fallback_msg = raw.strip().strip('"').strip("'").strip()
    result = parse_json(raw, fallback={"message": fallback_msg, "reasoning": {}})
    message = result.get("message", fallback_msg)
    reasoning = result.get("reasoning", {})

    # Clean up
    message = message.strip().strip('"').strip("'").strip()

    # Enforce character limit (LLM-based shortening with retry)
    message = await shorten_to_limit(message, max_chars)

    return {"message": message, "reasoning": reasoning}
