"""Personalized message generator — the core product engine.

Generates LinkedIn invitation messages that:
1. Sound like the user (voice signature matching)
2. Reference prospect-specific details
3. Stay under 200 chars (regular LinkedIn limit)
4. Follow sales methodology from v63 production prompts
5. Return structured reasoning for debugging/quality analysis

v63 prompt features:
- Pain-first messaging pattern
- No company names (theirs or ours)
- No industries/domains/roles/titles
- No links in first message
- CTA library with soft nudges
- No "noticed", "saw", "impressed by", no em dashes
"""

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 .news_service import get_prospect_news
from .prompt_loader import (
    build_context_block,
    get_prompt_temperature,
    has_prompt,
    render_prompt,
)

logger = logging.getLogger(__name__)

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

MESSAGE_SYSTEM_LEGACY = """You are an expert at writing LinkedIn connection request messages. You write brief, genuine, personal messages that get accepted.

Key principles:
- Sound like a real person, not a bot or salesperson
- Reference something SPECIFIC about the prospect (their work, posts, expertise)
- Never use salesy phrases like "leverage", "synergy", "exciting opportunity", "I noticed"
- Keep it under 200 characters
- End with a soft question or observation, not a hard sell
- Match the sender's voice and tone exactly

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

MESSAGE_PROMPT_LEGACY = """Write a LinkedIn connection request message.

## 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}
Location: {prospect_location}

## PROSPECT INTELLIGENCE (from analysis)
{prospect_intelligence}

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

## CONSTRAINTS
- MAXIMUM 200 characters (this is a hard limit — LinkedIn enforces it)
- Use the sender's voice — match their tone, formality, and vocabulary
- Reference something specific about the prospect
- Soft CTA: ask a question or make an observation. NEVER say "let's hop on a call"
- First name only for the prospect (e.g., "Sarah" not "Sarah Chen")
- NO emojis unless the sender's voice uses them
- NO "I noticed you..." or "I came across your profile..."

## OUTPUT
Return ONLY valid JSON (no markdown):
{{
    "reasoning": "brief explanation of your approach — what angle you chose and why",
    "message": "the actual message text, under 200 characters"
}}"""


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])}")
    # Deep profile hooks (education, certs, publications)
    edu_hooks = prospect_analysis.get("education_hooks")
    if edu_hooks and isinstance(edu_hooks, list):
        parts.append(f"Education: {', '.join(edu_hooks[:2])}")
    cert_hooks = prospect_analysis.get("certification_hooks")
    if cert_hooks and isinstance(cert_hooks, list):
        parts.append(f"Certifications: {', '.join(cert_hooks[:2])}")
    if prospect_analysis.get("has_publications"):
        parts.append("Published author — thought leader angle available")
    # Signal context injection (from signal_activator.py)
    signal_ctx = prospect_analysis.get("signal_context")
    if signal_ctx and isinstance(signal_ctx, dict):
        parts.append("")  # blank line separator
        parts.append("SIGNAL CONTEXT (reference naturally, don't be creepy):")
        if signal_ctx.get("engagement_hook"):
            parts.append(f"Suggested angle: {signal_ctx['engagement_hook']}")
        if signal_ctx.get("signal_summary"):
            summary = signal_ctx["signal_summary"][:150]
            parts.append(f"Context: {summary}")
        if signal_ctx.get("signal_angle"):
            parts.append(f"Approach: {signal_ctx['signal_angle'].replace('_', ' ')}")
        if signal_ctx.get("DO_NOT"):
            parts.append(f"Important: {signal_ctx['DO_NOT']}")
    return "\n".join(parts) if parts else "No analysis available — use profile data only."


async def generate_message(
    prospect: dict[str, Any],
    sender_profile: dict[str, Any],
    voice_signature: dict[str, Any],
    campaign_context: dict[str, Any],
    prospect_analysis: dict[str, Any] | None = None,
    campaign_ctx: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Generate a personalized LinkedIn invitation message.

    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
        prospect_analysis: Optional prospect analysis results
        campaign_ctx: Optional campaign context (offerings, case_studies, social_proofs)

    Returns:
        Dict with "message" (str, ≤200 chars) and "reasoning" (str).
    """
    # 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:
            msg = await client.generate_message(
                sender_profile, prospect, voice_signature, campaign_context,
            )
            # Backend returns plain str — wrap in dict for compatibility
            return {"message": msg, "reasoning": ""}
        finally:
            await client.close()

    max_chars = 200

    # ── Try v63 prompts ──
    if has_prompt("outreach_invitation") and has_prompt("outreach_system"):
        logger.debug("Using v63 prompts for invitation message")
        ctx = build_context_block(
            sender=sender_profile,
            prospect=prospect,
            campaign_config=campaign_context,
            voice=voice_signature,
            campaign_context=campaign_ctx,
            analysis=prospect_analysis,
            max_chars=max_chars,
        )
        # Optional: enrich trigger_info with recent news
        try:
            news = await get_prospect_news(prospect, sender_profile, campaign_context)
            if news:
                ctx["trigger_info"] = (ctx.get("trigger_info", "") + "\n\n" + news).strip()
        except Exception:
            pass  # Silently skip — news is optional for invitations

        system = render_prompt("outreach_system", ctx)
        prompt = render_prompt("outreach_invitation", ctx)
        temp = get_prompt_temperature("outreach_invitation")

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

        # v63 returns plain text (not JSON) — wrap it
        message = raw.strip().strip('"').strip("'").strip()
        reasoning = "v63 prompt — pain-first, no company names, soft CTA"

    else:
        # ── Fallback to legacy prompts ──
        logger.debug("Using legacy prompts for invitation message (v63 not found)")

        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 = MESSAGE_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_location=prospect.get("location", ""),
            prospect_intelligence=prospect_intelligence,
            campaign_target=campaign_context.get("target_description", ""),
            relevance_hook=campaign_context.get("relevance_hook", ""),
        )

        client = LLMClient()
        raw = await client.generate(prompt, system=MESSAGE_SYSTEM_LEGACY, temperature=0.7)

        result = parse_json(raw, fallback={"message": raw, "reasoning": ""})
        message = result.get("message", raw)
        reasoning = result.get("reasoning", "")

    # Clean up — strip quotes, whitespace, etc.
    message = message.strip().strip('"').strip("'").strip()

    # Enforce char limit — LLM shortening with truncation fallback
    if len(message) > max_chars:
        message = await shorten_to_limit(message, max_chars)

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