"""Follow-up message generator — 2-stage v63 pipeline with legacy fallback.

Generates LinkedIn follow-up DMs after a connection is accepted.
Unlike invitation messages (200 chars), follow-ups are full DMs (up to 500 chars).

v63 Pipeline (2-stage):
1. Reasoning (LLM): Strategic planning — link-gating, bullets policy, CTA rotation,
   length style, pain-first pattern, fresh opener selection
2. Generate (LLM): Write the message using reasoning decisions + voice

Legacy Pipeline (1-stage, fallback):
1. Generate (LLM): Single-shot with inline reasoning

Both are then passed through Improve → Validate → Fix in the tool layer.

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

from __future__ import annotations

import logging
from typing import Any

import json

from .followup_schemas import FollowUpReasoning
from .json_repair import parse_json
from .length_fixer import shorten_to_limit
from .llm import LLMClient
from .memory_extractor import extract_memory_from_reasoning
from .memory_formatter import format_memory_for_prompt
from .news_service import get_prospect_news
from .prompt_loader import (
    build_context_block,
    get_prompt_temperature,
    has_prompt,
    render_prompt,
)

logger = logging.getLogger(__name__)

# Maximum follow-up DM length (LinkedIn has no hard limit for DMs,
# but keeping it concise improves response rates)
FOLLOWUP_MAX_CHARS = 500

# ──────────────────────────────────────────────
# Legacy prompts — fallback if v63 JSON files are missing
# ──────────────────────────────────────────────

FOLLOWUP_SYSTEM_LEGACY = """You are an expert at writing LinkedIn follow-up DMs. You write genuine, personal follow-up messages that advance a relationship after a connection has been accepted.

Key principles:
- Sound like a real person, not a bot or salesperson
- Reference the CONVERSATION HISTORY — acknowledge what was said before
- Use a FRESH ANGLE — never repeat the same hook or pitch from earlier messages
- Match the sender's voice and tone exactly
- Keep messages concise (2-5 sentences, under 500 characters)
- End with a soft CTA — a question, observation, or resource offer
- Never use salesy phrases like "leverage", "synergy", "exciting opportunity"
- Never say "just following up" or "circling back" — these are lazy and obvious

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

FOLLOWUP_PROMPT_LEGACY = """Generate a LinkedIn follow-up DM.

## 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 (the person receiving this message)
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}

## FOLLOW-UP NUMBER: {followup_number}
(1 = first follow-up after connection accepted, 2 = second, etc.)

## CONSTRAINTS
- Match the sender's voice exactly
- Reference something from the conversation or the prospect's profile
- Use a COMPLETELY NEW ANGLE — do NOT repeat any hook, question, or observation from previous messages
- Keep under 500 characters
- Soft CTA: ask a question, share a resource, or make an observation
- First name only for the prospect
- NO emojis unless the sender's voice uses them
- NO "just following up", "circling back", "touching base", "checking in"
- NO salesy language

## OUTPUT
Return ONLY valid JSON (no markdown):
{{
    "reasoning": {{
        "hook": "brief description of the fresh angle",
        "pain_point": "which pain point or interest this addresses",
        "cta_choice": "question|resource_offer|meeting_soft|value_share"
    }},
    "message": "the actual follow-up message text"
}}"""


def _format_conversation_history(messages: list[dict]) -> str:
    """Format conversation history for the prompt."""
    if not messages:
        return "No previous messages — this is the first follow-up after connection acceptance."
    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"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_followup(
    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]],
    followup_number: int = 1,
    prospect_analysis: dict[str, Any] | None = None,
    campaign_ctx: dict[str, Any] | None = None,
    outreach_id: str = "",
) -> dict[str, Any]:
    """Generate a personalized LinkedIn follow-up DM.

    Uses v63's 2-stage pipeline (reasoning → generate) if prompt files exist,
    otherwise falls back to legacy single-stage generation.

    Args:
        prospect: Prospect profile data (name, title, company, headline, etc.)
        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)
        followup_number: Which follow-up this is (1=first, 2=second, etc.)
        prospect_analysis: Optional prospect analysis results
        campaign_ctx: Optional campaign context (offerings, case_studies, social_proofs)
        outreach_id: Optional outreach ID for loading conversation memory

    Returns:
        Dict with "message" (str), "reasoning" (dict or str), and
        optional "memory_entry" (dict) for saving after send.
    """
    # 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_followup(
                sender=sender_profile,
                prospect=prospect,
                voice=voice_signature,
                campaign_context=campaign_context,
                conversation_history=conversation_history,
                followup_number=followup_number,
            )
        finally:
            await client.close()

    # ── Try v63 2-stage pipeline ──
    use_v63 = (
        has_prompt("followup_reasoning")
        and has_prompt("followup_message")
        and has_prompt("outreach_system")
    )

    if use_v63:
        return await _generate_v63(
            prospect, sender_profile, voice_signature, campaign_context,
            conversation_history, followup_number, prospect_analysis, campaign_ctx,
            outreach_id,
        )
    else:
        return await _generate_legacy(
            prospect, sender_profile, voice_signature, campaign_context,
            conversation_history, followup_number, prospect_analysis,
        )


async def _generate_v63(
    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]],
    followup_number: int,
    prospect_analysis: dict[str, Any] | None,
    campaign_ctx: dict[str, Any] | None,
    outreach_id: str = "",
) -> dict[str, Any]:
    """Generate using v63's 2-stage pipeline: Reasoning → Message → Memory."""
    logger.info("Using v63 2-stage pipeline for follow-up #%d", followup_number)

    max_chars = FOLLOWUP_MAX_CHARS
    ctx = build_context_block(
        sender=sender_profile,
        prospect=prospect,
        campaign_config=campaign_context,
        voice=voice_signature,
        campaign_context=campaign_ctx,
        history=conversation_history,
        analysis=prospect_analysis,
        max_chars=max_chars,
    )

    # ── Load conversation memory (anti-repetition) ──
    memory: list[dict] = []
    if outreach_id:
        try:
            from ..db.queries import get_outreach_memory
            memory = get_outreach_memory(outreach_id)
            if memory:
                logger.info("Loaded %d memory entries for outreach %s", len(memory), outreach_id[:8])
        except Exception as e:
            logger.debug("Memory load skipped: %s", e)
    ctx["previously_used"] = format_memory_for_prompt(memory)

    llm_client = LLMClient()

    # ── STAGE 1: Reasoning ──
    system_prompt = render_prompt("outreach_system", ctx)
    reasoning_prompt = render_prompt("followup_reasoning", ctx)
    reasoning_temp = get_prompt_temperature("followup_reasoning")

    reasoning_raw = await llm_client.generate(
        reasoning_prompt, system=system_prompt, temperature=reasoning_temp,
    )
    logger.debug("Stage 1 reasoning: %s", reasoning_raw[:200])

    # ── Parse structured reasoning (FollowUpReasoning) ──
    reasoning_structured = _parse_structured_reasoning(reasoning_raw)

    # ── STAGE 2: Generate message using reasoning decisions ──
    # Pass flattened decisions + skeleton to Stage 2 for precise control
    if reasoning_structured:
        decisions_dict = reasoning_structured.decisions.model_dump()
        skeleton_dict = reasoning_structured.message_skeleton.model_dump()
        # Build a formatted reasoning block for the prompt
        ctx["reasoning"] = (
            f"DECISIONS:\n{json.dumps(decisions_dict, indent=2)}\n\n"
            f"MESSAGE SKELETON:\n{json.dumps(skeleton_dict, indent=2)}"
        )
        logger.info(
            "Structured reasoning parsed: length_style=%s, allow_link=%s, cta=%s",
            decisions_dict.get("length_style"),
            decisions_dict.get("allow_link"),
            decisions_dict.get("cta_choice", "")[:40],
        )
    else:
        # Fallback: pass raw text if structured parsing failed
        ctx["reasoning"] = reasoning_raw
        logger.warning("Structured reasoning parse failed, using raw text")

    message_prompt = render_prompt("followup_message", ctx)
    message_temp = get_prompt_temperature("followup_message")

    message_raw = await llm_client.generate(
        message_prompt, system=system_prompt, temperature=message_temp,
    )

    # v63 returns plain text (not JSON)
    message = message_raw.strip().strip('"').strip("'").strip()

    # ── STAGE 3: News enhancement (optional) ──
    news_context = None
    try:
        news_context = await get_prospect_news(
            prospect=prospect,
            sender=sender_profile,
            campaign_context=campaign_context,
        )
    except Exception as e:
        logger.debug("News lookup skipped: %s", e)

    if news_context and has_prompt("followup_news"):
        try:
            news_ctx = {
                "draft_message": message,
                "news_context": news_context,
                "symbols_limit": str(max_chars),
            }
            enhanced = await llm_client.generate(
                render_prompt("followup_news", news_ctx),
                system=system_prompt,
                temperature=get_prompt_temperature("followup_news"),
            )
            enhanced = enhanced.strip().strip('"').strip("'").strip()
            if enhanced and len(enhanced) <= max_chars:
                message = enhanced
                logger.info("News enhancement applied to follow-up")
        except Exception as e:
            logger.warning("News enhancement failed, using original: %s", e)

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

    # ── Extract conversation memory for this follow-up ──
    memory_entry = None
    try:
        memory_entry = await extract_memory_from_reasoning(
            reasoning_text=reasoning_raw,
            message_text=message,
            followup_number=followup_number,
        )
        logger.info("Extracted memory entry for follow-up #%d", followup_number)
    except Exception as e:
        logger.debug("Memory extraction skipped: %s", e)

    # Include structured reasoning in the result for downstream use (validate/fix)
    reasoning_output: dict[str, Any] | str = reasoning_raw
    if reasoning_structured:
        reasoning_output = {
            "decisions": reasoning_structured.decisions.model_dump(),
            "message_skeleton": reasoning_structured.message_skeleton.model_dump(),
            "_raw": reasoning_raw,
        }

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


def _parse_structured_reasoning(raw: str) -> FollowUpReasoning | None:
    """Parse raw LLM output into a FollowUpReasoning model.

    Uses 2-tier JSON parsing (raw → regex cleanup) then Pydantic validation.
    Returns None if parsing fails — caller falls back to raw text.
    """
    try:
        parsed = parse_json(raw, fallback=None)
        if parsed:
            return FollowUpReasoning.model_validate(parsed)
    except Exception as e:
        logger.debug("Structured reasoning parse attempt 1 failed: %s", e)

    # Try extracting just decisions + message_skeleton from a larger JSON
    try:
        parsed = parse_json(raw, fallback={})
        if "decisions" in parsed and "message_skeleton" in parsed:
            return FollowUpReasoning.model_validate(parsed)
        # Sometimes LLM wraps in extra layer
        for key in ("reasoning", "result", "output"):
            if key in parsed and isinstance(parsed[key], dict):
                inner = parsed[key]
                if "decisions" in inner:
                    return FollowUpReasoning.model_validate(inner)
    except Exception as e:
        logger.debug("Structured reasoning parse attempt 2 failed: %s", e)

    return None


async def _generate_legacy(
    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]],
    followup_number: int,
    prospect_analysis: dict[str, Any] | None,
) -> dict[str, Any]:
    """Generate using legacy single-stage prompt (fallback)."""
    logger.info("Using legacy prompts for follow-up #%d (v63 not found)", followup_number)

    # 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 = FOLLOWUP_PROMPT_LEGACY.format(
        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,
        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),
        followup_number=followup_number,
    )

    llm_client = LLMClient()
    raw = await llm_client.generate(prompt, system=FOLLOWUP_SYSTEM_LEGACY, 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, FOLLOWUP_MAX_CHARS)

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