"""Tool: reply_to_prospect — Generate and send sentiment-aware replies to prospect messages.

Core reply loop:
1. Find next outreach needing a reply (hot_lead or replied with prospect message)
2. Load conversation history + last prospect message + sentiment
3. Resolve chat_id (prospect's linkedin_id → Unipile chat)
4. Generate personalized reply via sentiment-specific pipeline
5. In Copilot mode: show for approval. In Autopilot: send immediately.
6. Update outreach status
"""

from __future__ import annotations

import json
import logging
from typing import Any

from ..ai.prospect_analyzer import analyze_prospect
from ..ai.reply_generator import NEGATIVE_MAX_CHARS, REPLY_MAX_CHARS
from ..ai.reply_pipeline import run_reply_pipeline
from ..config import get_tier
from ..constants import (
    COPILOT_APPROVAL_THRESHOLD,
    FREE_MONTHLY_MESSAGES,
    TIER_PRO,
)
from ..db.queries import (
    find_active_campaign,
    get_campaign,
    get_campaign_context,
    get_contact_analysis,
    get_inbound_reply_candidates,
    get_messages_for_outreach,
    get_monthly_usage,
    get_outreach,
    get_outreach_with_contact,
    get_reply_candidates,
    get_setting,
    increment_usage,
    log_action,
    save_contact_analysis,
    save_message,
    update_outreach,
)
from ..formatter import stars
from ..linkedin import (
    UnipileAuthError,
    UnipileError,
    get_account_id,
    get_linkedin_client,
)
from ..db.async_bridge import run_db

logger = logging.getLogger(__name__)

SENTIMENT_ICONS = {
    "positive": "\U0001f525",
    "negative": "\U0001f44e",
    "question": "\u2753",
    "neutral": "\U0001f4ac",
    "engaged": "\U0001f4ac",
    "out_of_office": "\u2708\ufe0f",
    "opt_out": "\U0001f6ab",
}

SENTIMENT_LABELS = {
    "positive": "Positive",
    "negative": "Negative",
    "question": "Question",
    "neutral": "Neutral",
    "engaged": "Engaged",
    "out_of_office": "Out of Office",
    "opt_out": "Opt-Out",
}


async def run_reply_to_prospect(
    outreach_id: str = "",
    mode: str = "autopilot",
    format: str = "text",
) -> str:
    """Generate and send (or queue) a sentiment-aware reply to a prospect.

    Flow:
    1. Find the active campaign and next outreach needing a reply
    2. Load conversation history and detect last prospect message
    3. Resolve chat_id
    4. Generate sentiment-aware reply
    5. Validate the reply (reply-specific pipeline)
    6. Copilot: show for review. Autopilot: send directly.

    Args:
        format: "text" (default) or "voice" (generates audio via Hume TTS).
    """

    # ── Step 0: Pre-checks ──
    setup_done = await run_db(get_setting, "setup_complete", False)
    if not setup_done:
        return (
            "Setup required before replying.\n\n"
            "Please run setup_profile first."
        )

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

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

    # ── Step 1: Find outreach needing a reply ──
    campaign = None
    campaign_id = ""

    if outreach_id:
        # Specific outreach requested
        outreach = await run_db(get_outreach, outreach_id)
        if not outreach:
            await client.close()
            return f"Outreach not found: {outreach_id}"
        if outreach["status"] not in ("hot_lead", "replied"):
            await client.close()
            return (
                f"Outreach status is '{outreach['status']}', not 'hot_lead' or 'replied'.\n"
                "Replies can only be sent to prospects who have messaged you.\n"
                "Use check_replies() first to detect new messages."
            )
        # Inbound outreaches have empty campaign_id — that's OK
        if outreach["campaign_id"]:
            campaign = await run_db(get_campaign, outreach["campaign_id"])
            if not campaign:
                await client.close()
                return "Campaign not found for this outreach."
            campaign_id = campaign["id"]
        else:
            campaign = None
            campaign_id = ""
        candidate = await run_db(get_outreach_with_contact, outreach_id)
        if not candidate:
            await client.close()
            return "Could not load contact data for this outreach."
    else:
        # Search ALL active campaigns for reply candidates (not just the first)
        from ..db.queries import list_campaigns
        all_campaigns = await run_db(list_campaigns, status="active")
        candidates = []
        for camp in all_campaigns:
            if camp.get("status") == "paused":
                continue
            camp_candidates = await run_db(get_reply_candidates, camp["id"])
            if camp_candidates:
                candidates = camp_candidates
                campaign = camp
                campaign_id = camp["id"]
                break

        # Also check inbound (campaign-less) outreaches
        if not candidates:
            candidates = await run_db(get_inbound_reply_candidates)

        if not candidates:
            await client.close()
            label = f"in '{campaign['name']}'" if campaign else "across campaigns or inbound"
            return (
                f"No prospects need a reply {label}.\n\n"
                "Prospects must have sent you a message first.\n"
                "Use check_replies() to detect new messages, or "
                "generate_and_send() to reach new prospects."
            )
        candidate = candidates[0]
        outreach_id = candidate["outreach_id"]
        # Update campaign reference from the candidate
        if candidate.get("campaign_id"):
            campaign = await run_db(get_campaign, candidate["campaign_id"])
            campaign_id = candidate["campaign_id"]
        else:
            campaign = None
            campaign_id = ""

    # ── Step 2: Check tier limits ──
    tier = get_tier()
    if tier != TIER_PRO:
        usage = await run_db(get_monthly_usage)
        if usage.get("messages_sent", 0) >= FREE_MONTHLY_MESSAGES:
            await client.close()
            return (
                f"Free tier limit reached: {FREE_MONTHLY_MESSAGES} messages/month.\n\n"
                "Upgrade to Pro ($29/mo) for unlimited messages."
            )

    # ── Step 3: Resolve chat_id ──
    # Prefer provider_id (ACoAAA format) from profile_json — that's what
    # chat attendees use.  Fall back to linkedin_id (public_id / slug).
    prospect_linkedin_id = ""
    pj = candidate.get("profile_json")
    if pj:
        try:
            _profile = json.loads(pj) if isinstance(pj, str) else pj
            prospect_linkedin_id = _profile.get("provider_id", "")
        except Exception:
            pass
    if not prospect_linkedin_id:
        prospect_linkedin_id = candidate.get("linkedin_id", "")
    if not prospect_linkedin_id:
        await client.close()
        return f"No LinkedIn ID for {candidate.get('name', 'Unknown')}. Cannot send DM."

    try:
        chat_id = await client.find_chat_for_user(account_id, prospect_linkedin_id)
    except Exception as e:
        logger.error(f"Failed to find chat: {e}")
        chat_id = None

    if not chat_id:
        await client.close()
        # Save a system note so the auto-reply candidate query's NOT EXISTS
        # guard excludes this outreach — prevents infinite retry loops.
        await run_db(save_message, outreach_id,
            role="sdr",
            text="[system] Auto-reply skipped: chat not found on LinkedIn.",
            format="system",)
        return (
            f"Could not find a chat with {candidate.get('name', 'the prospect')}.\n\n"
            "The chat may not have synced yet.\n"
            "Try check_replies() to update conversation data."
        )

    # ── Step 4: Load conversation history (enriched with LinkedIn) ──
    sender_provider_id = await run_db(get_setting, "profile", {}).get("provider_id", "")
    if chat_id and sender_provider_id:
        try:
            from ..services.conversation_enricher import get_enriched_conversation
            messages = await get_enriched_conversation(
                client, account_id, chat_id, outreach_id, sender_provider_id,
            )
        except Exception as e:
            logger.warning("Conversation enrichment failed, using local-only: %s", e)
            messages = await run_db(get_messages_for_outreach, outreach_id)
    else:
        messages = await run_db(get_messages_for_outreach, outreach_id)

    # Safety cap: block reply if last N messages are all from SDR (talking to ourselves).
    # Uses enriched messages (synced with LinkedIn) so prospect replies aren't missed.
    MAX_CONSECUTIVE_SDR = 3
    consecutive_sdr = 0
    for m in reversed(messages):
        if m.get("role") == "sdr":
            consecutive_sdr += 1
        else:
            break
    if consecutive_sdr >= MAX_CONSECUTIVE_SDR:
        await client.close()
        await run_db(log_action, "reply_cap_reached", outreach_id=outreach_id,
                   result="skipped", details={"consecutive_sdr": consecutive_sdr, "limit": MAX_CONSECUTIVE_SDR})
        return (
            f"⏭️ Message cap reached for {candidate.get('name', 'Unknown')} "
            f"({consecutive_sdr} consecutive SDR messages without prospect response)."
        )

    # ── Dedup guard: block if we already replied to the latest prospect message ──
    # Uses LOCAL DB (not enriched/LinkedIn) for consistency during concurrent jobs.
    # This prevents the race where multiple auto-reply jobs fire at the same time
    # and all pass the consecutive-SDR check because none have saved yet.
    if mode == "autopilot":
        local_msgs = await run_db(get_messages_for_outreach, outreach_id)
        _last_prospect_ts = 0
        for m in reversed(local_msgs):
            if m.get("role") == "prospect":
                _last_prospect_ts = m.get("timestamp") or m.get("created_at") or 0
                break
        if _last_prospect_ts:
            _sdr_after_prospect = any(
                m.get("role") == "sdr"
                and (m.get("timestamp") or m.get("created_at") or 0) >= _last_prospect_ts
                for m in local_msgs
            )
            if _sdr_after_prospect:
                await client.close()
                await run_db(log_action, "reply_dedup_blocked", outreach_id=outreach_id,
                           result="skipped", details={"reason": "already_replied_to_last_prospect_msg"})
                return (
                    f"Already replied to {candidate.get('name', 'Unknown')}'s last message — skipping."
                )

    # Find the last prospect message and its sentiment
    last_prospect_msg = None
    for msg in reversed(messages):
        if msg.get("role") == "prospect":
            last_prospect_msg = msg
            break

    if not last_prospect_msg:
        await client.close()
        return (
            f"No prospect message found for this outreach.\n\n"
            "The prospect needs to have sent you a message first.\n"
            "Use check_replies() to detect new messages."
        )

    reply_text = last_prospect_msg.get("text", "")
    sentiment = last_prospect_msg.get("sentiment", "neutral")

    # Don't auto-reply to opt_out, out_of_office, or negative sentiment
    if sentiment == "opt_out":
        await client.close()
        return (
            "This prospect has opted out of communication.\n"
            "No reply will be sent. Their outreach has been closed."
        )
    if sentiment == "out_of_office":
        await client.close()
        return (
            "This prospect is out of office.\n"
            "Wait for them to return before replying.\n"
            "Use send_followup() later when they're back."
        )
    if sentiment == "negative" and mode == "autopilot":
        await client.close()
        await run_db(update_outreach, outreach_id, status="opted_out")
        await run_db(log_action, "auto_reply_skipped_negative", outreach_id=outreach_id,
                   result="skipped", details={"text": reply_text[:200]})
        return (
            f"Prospect sent a negative/decline message — skipping auto-reply.\n"
            f"   \"{reply_text[:100]}\"\n"
            "Outreach closed. No further messages will be sent."
        )

    # Keyword-based decline detection (catches misclassified sentiments)
    # Especially important for non-English messages where sentiment may be wrong
    _decline_keywords = [
        "not interested", "no thanks", "no thank you", "don't contact",
        "remove me", "unsubscribe", "stop messaging", "other priorities",
        "не цікавить", "не зацікавлений", "інших пріоритет",  # Ukrainian
        "не интересует", "других приоритет",  # Russian
    ]
    reply_lower = reply_text.lower()
    if mode == "autopilot" and any(kw in reply_lower for kw in _decline_keywords):
        await client.close()
        await run_db(update_outreach, outreach_id, status="opted_out")
        await run_db(log_action, "auto_reply_skipped_decline_keywords", outreach_id=outreach_id,
                   result="skipped", details={"text": reply_text[:200]})
        return (
            f"Prospect's message contains decline keywords — skipping auto-reply.\n"
            f"   \"{reply_text[:100]}\"\n"
            "Outreach closed. No further messages will be sent."
        )

    # Reverse-pitch detection: check if prospect is selling to us
    if mode == "autopilot" and sentiment in ("engaged", "question", "neutral"):
        try:
            seller_result = None
            if hasattr(client, "classify_seller"):
                # Collect all prospect messages for better detection
                prospect_messages = [m["text"] for m in messages if m.get("role") == "prospect"]
                seller_result = await client.classify_seller(
                    messages=prospect_messages,
                    author_name=candidate.get("name", ""),
                    author_headline=candidate.get("title", ""),
                )
            if seller_result and seller_result.get("is_seller") and seller_result.get("confidence", 0) >= 0.4:
                await client.close()
                await run_db(update_outreach, outreach_id, status="reverse_pitch")
                await run_db(log_action, "reverse_pitch_detected", outreach_id=outreach_id,
                           result="skipped", details={
                               "text": reply_text[:200],
                               "seller_type": seller_result.get("seller_type", ""),
                               "confidence": seller_result.get("confidence", 0),
                           })
                seller_type = seller_result.get("seller_type", "unknown")
                return (
                    f"Reverse pitch detected from {candidate.get('name', 'Unknown')} "
                    f"(type: {seller_type}, confidence: {seller_result.get('confidence', 0):.0%}).\n"
                    f"   \"{reply_text[:100]}\"\n"
                    "They're selling to you, not buying. Auto-reply skipped.\n"
                    "Use send_message(action='reply') to respond manually if needed."
                )
        except Exception as e:
            logger.debug("Reverse-pitch check failed (non-critical): %s", e)

    # ── Step 5: Load context for reply generation ──
    sender_profile = await run_db(get_setting, "profile", {})
    voice_signature = await run_db(get_setting, "voice_signature", {})

    if campaign:
        campaign_config = json.loads(campaign.get("config_json") or "{}")
        icp_data = json.loads(campaign.get("icp_json") or "{}")
        campaign_context = {
            "target_description": campaign_config.get("target_description", ""),
            "relevance_hook": icp_data.get("relevance_hook", ""),
            "booking_link": campaign_config.get("booking_link", ""),
        }
        campaign_ctx = await run_db(get_campaign_context, campaign_id)
        # Merge context_json fields (offerings, case_studies, social_proofs) into campaign_context
        campaign_context.update(campaign_ctx)
        booking_link = campaign_config.get("booking_link", "")
    else:
        # Inbound outreach — no campaign context available
        campaign_config = {}
        icp_data = {}
        campaign_context = {"target_description": "", "relevance_hook": "", "booking_link": ""}
        campaign_ctx = {}
        booking_link = ""

    prospect_data = json.loads(candidate.get("profile_json", "{}")) if candidate.get("profile_json") else {
        "name": candidate.get("name", ""),
        "title": candidate.get("title", ""),
        "company": candidate.get("company", ""),
        "headline": f"{candidate.get('title', '')} at {candidate.get('company', '')}",
    }

    # ── Prospect Intelligence: load cached or generate ──
    prospect_analysis = None
    contact_db_id = candidate.get("contact_db_id") or candidate.get("contact_id", "")
    if contact_db_id:
        prospect_analysis = await run_db(get_contact_analysis, contact_db_id)
    if not prospect_analysis:
        try:
            prospect_analysis = await analyze_prospect(
                prospect=prospect_data,
                campaign_context=campaign_context,
                icp_data=icp_data,
            )
            if contact_db_id:
                await run_db(save_contact_analysis, contact_db_id, prospect_analysis)
        except Exception as e:
            logger.warning(f"Prospect analysis failed, proceeding without: {e}")

    # Format conversation history for the generator
    conversation_history = [
        {"role": m["role"], "text": m["text"]}
        for m in messages
    ]

    # ── Step 6: Generate → Improve → Validate → Fix pipeline ──
    max_chars = NEGATIVE_MAX_CHARS if sentiment == "negative" else REPLY_MAX_CHARS
    previous_sdr_messages = [m["text"] for m in messages if m.get("role") == "sdr"]

    message, reasoning, validation = await run_reply_pipeline(
        prospect=prospect_data,
        sender_profile=sender_profile,
        voice_signature=voice_signature,
        campaign_context=campaign_context,
        conversation_history=conversation_history,
        reply_text=reply_text,
        sentiment=sentiment,
        prospect_analysis=prospect_analysis,
        booking_link=booking_link,
        max_chars=max_chars,
        previous_sdr_messages=previous_sdr_messages,
    )

    if not message:
        await client.close()
        return f"Failed to generate reply: pipeline returned empty message"

    if not validation or not validation.is_valid:
        issues_text = "\n".join(f"  \u26a0\ufe0f {issue}" for issue in (validation.issues if validation else []))
        if mode == "autopilot":
            logger.warning(f"Autopilot blocked invalid reply: {issues_text}")
            await run_db(log_action, "validation_blocked", outreach_id=outreach_id, result="blocked",
                       details={"issues": validation.issues if validation else [], "type": "reply"})
            await client.close()
            return (
                f"\u26a0\ufe0f Reply for {candidate.get('name', 'Unknown')} failed validation "
                f"after Generate \u2192 Improve \u2192 Fix pipeline.\n\n"
                f"Issues:\n{issues_text}\n\n"
                "The message was NOT sent to protect your account.\n"
                "Try: reply_to_prospect(mode='copilot') to review and edit manually."
            )
        else:
            logger.warning(f"Copilot reply has validation warnings: {issues_text}")

    # ── Step 7: Copilot vs Autopilot ──
    prospect_name = candidate.get("name", "Unknown")
    prospect_title = candidate.get("title", "")
    prospect_company = candidate.get("company", "")
    fit_score = candidate.get("fit_score", 0)

    role_str = prospect_title
    if prospect_company:
        role_str += f" at {prospect_company}" if role_str else prospect_company

    sentiment_icon = SENTIMENT_ICONS.get(sentiment, "\U0001f4ac")
    sentiment_label = SENTIMENT_LABELS.get(sentiment, "Unknown")

    if False:  # copilot mode removed
        # Show message for review — store reply context in next_action as JSON
        reply_context = json.dumps({
            "type": "reply",
            "message": message,
            "sentiment": sentiment,
        })
        await run_db(update_outreach, outreach_id, next_action=reply_context)

        output = [
            f"Reply to **{prospect_name}** ({role_str}) \u2014 Sentiment: {sentiment_icon} {sentiment_label}",
            f"   Fit: {stars(fit_score)}",
            "",
            f'   Their message: "{reply_text[:200]}{"..." if len(reply_text) > 200 else ""}"',
            "",
            f'   "{message}"',
            f"   ({len(message)}/{max_chars} chars)",
            "",
        ]

        # Show reasoning
        if reasoning:
            strategy = reasoning.get("strategy", "") or reasoning.get("answer_approach", "") or reasoning.get("close_style", "") or reasoning.get("angle", "")
            if strategy:
                output.append(f"   Strategy: {strategy}")
                output.append("")

        # Show validation warnings
        if validation and validation.warnings:
            for w in validation.warnings:
                output.append(f"   {w}")
            output.append("")

        output.extend([
            "Send this? Reply with:",
            "  'yes' / 'send' to send this reply",
            "  'skip' to skip this prospect",
            "  'edit: [your text]' to send a custom message instead",
            "  'stop' to pause the campaign",
        ])

        await client.close()
        return "\n".join(line for line in output if line is not None)

    else:
        # Autopilot — send immediately
        actual_format = "text"
        audio_path = ""
        if format == "voice":
            try:
                from ..ai.voice_memo_generator import generate_voice_memo, cleanup_voice_memo
                from ..config import is_voice_memo_enabled
                if is_voice_memo_enabled():
                    voice_result = await generate_voice_memo(
                        message,
                        voice_signature=voice_signature,
                        humanize=campaign_config.get("voice_humanize", True),
                        noise_type=campaign_config.get("voice_noise_type", "auto"),
                        noise_volume=campaign_config.get("voice_noise_volume", "subtle"),
                    )
                    if voice_result.get("success"):
                        audio_path = voice_result["audio_path"]
                        actual_format = "voice"
                    else:
                        logger.warning("Voice gen failed (%s), falling back to text", voice_result.get("error"))
            except Exception as e:
                logger.warning("Voice gen error (%s), falling back to text", e)

        try:
            if actual_format == "voice" and audio_path:
                send_result = await client.send_voice_message(
                    account_id=account_id,
                    chat_id=chat_id,
                    audio_path=audio_path,
                )
                if not send_result.get("success"):
                    logger.warning("Voice send failed (%s), falling back to text", send_result.get("error"))
                    actual_format = "text"

            if actual_format == "text":
                send_result = await client.send_message(
                    account_id=account_id,
                    chat_id=chat_id,
                    text=message,
                )
        except UnipileAuthError:
            await client.close()
            if audio_path:
                try:
                    from ..ai.voice_memo_generator import cleanup_voice_memo
                    cleanup_voice_memo(audio_path)
                except Exception:
                    pass
            return (
                "LinkedIn account disconnected.\n\n"
                "Run setup_profile() again to reconnect."
            )
        except Exception as e:
            await client.close()
            if audio_path:
                try:
                    from ..ai.voice_memo_generator import cleanup_voice_memo
                    cleanup_voice_memo(audio_path)
                except Exception:
                    pass
            return f"Failed to send reply: {e}"
        finally:
            await client.close()
            if audio_path:
                try:
                    from ..ai.voice_memo_generator import cleanup_voice_memo
                    cleanup_voice_memo(audio_path)
                except Exception:
                    pass

        if send_result.get("success"):
            # Update status based on sentiment
            if sentiment == "negative":
                new_status = "closed_unhappy"
                import time as _time
                outcome_data = json.dumps({
                    "outcome": "lost",
                    "reason": "Prospect declined (negative reply)",
                    "closed_at": int(_time.time()),
                    "previous_status": candidate.get("status", ""),
                })
                await run_db(update_outreach, outreach_id, status=new_status, outcome_json=outcome_data, next_action=None)
            elif candidate.get("status") == "hot_lead":
                new_status = "hot_lead"  # Stay hot_lead
                await run_db(update_outreach, outreach_id, status=new_status)
            else:
                new_status = "messaged"  # Back to messaged, waiting for their response
                await run_db(update_outreach, outreach_id, status=new_status)
            local_msg_id = await run_db(save_message, outreach_id, role="sdr", text=message, format=actual_format)
            await run_db(increment_usage, "messages_sent")
            await run_db(log_action, "reply_sent",
                outreach_id=outreach_id,
                result="success",
                details={
                    "prospect": prospect_name,
                    "sentiment": sentiment,
                    "message_length": len(message),
                    "reasoning": reasoning,
                },)

            # Post-send verification (background, non-blocking)
            if actual_format == "text" and chat_id:
                from ..services.engagement_verifier import schedule_post_send_verify
                schedule_post_send_verify(
                    account_id=account_id, chat_id=chat_id,
                    sent_text=message, outreach_id=outreach_id,
                    local_message_id=local_msg_id, message_type="reply",
                    voice_signature=voice_signature,
                )

            status_note = ""
            if sentiment == "negative":
                status_note = "\nOutreach closed gracefully."
            elif sentiment == "positive":
                status_note = "\nConversation continues \u2014 check back for their response."
            elif sentiment == "engaged":
                status_note = "\nConversation deepening \u2014 building rapport before any pitch."

            return (
                f"Sent reply to {prospect_name} ({role_str}) \u2014 {sentiment_icon} {sentiment_label}\n"
                f'   "{message}"\n'
                f"{status_note}"
            )
        else:
            error = send_result.get("error", "Unknown error")
            await run_db(log_action, "reply_failed",
                outreach_id=outreach_id,
                result="error",
                details={"error": error},)
            return f"Reply failed for {prospect_name}: {error}"
