"""Tool: send_followup — Generate and send follow-up DMs after connection acceptance.

Core follow-up loop:
1. Find next "connected" outreach that needs a follow-up
2. Load conversation history
3. Resolve chat_id (prospect's linkedin_id → Unipile chat)
4. Generate personalized follow-up via 3-stage pipeline
5. In Copilot mode: show for approval. In Autopilot: send immediately.
6. Update outreach status and followup_count
"""

from __future__ import annotations

import json
import logging
import time
from typing import Any

from ..ai.followup_generator import generate_followup, FOLLOWUP_MAX_CHARS
from ..ai.llm_validator import llm_validate
from ..ai.message_fixer import fix_message
from ..ai.message_improver import improve_message
from ..ai.message_validator import validate_followup
from ..ai.prospect_analyzer import analyze_prospect
from ..config import get_tier
from ..constants import (
    COPILOT_APPROVAL_THRESHOLD,
    FREE_MAX_FOLLOWUPS,
    FREE_MONTHLY_MESSAGES,
    PRO_FOLLOWUP_SCHEDULE_DAYS,
    PRO_MAX_FOLLOWUPS,
    TIER_PRO,
)
from ..db.queries import (
    find_active_campaign,
    get_campaign,
    get_campaign_context,
    get_contact_analysis,
    get_followup_candidates,
    get_messages_for_outreach,
    get_monthly_usage,
    get_outreach,
    get_outreach_with_contact,
    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,
)

logger = logging.getLogger(__name__)


async def run_send_followup(
    campaign_id: str = "",
    outreach_id: str = "",
    mode: str = "autopilot",
    format: str = "text",
) -> str:
    """Generate and send (or queue) a personalized follow-up DM.

    Flow:
    1. Find the active campaign and next "connected" outreach
    2. Check tier limits and follow-up schedule
    3. Load conversation history and resolve chat_id
    4. Generate follow-up using voice signature
    5. Validate the message (follow-up 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 = get_setting("setup_complete", False)
    if not setup_done:
        return (
            "Setup required before sending follow-ups.\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 campaign + follow-up candidate ──
    tier = get_tier()
    max_followups = PRO_MAX_FOLLOWUPS if tier == TIER_PRO else FREE_MAX_FOLLOWUPS

    if outreach_id:
        # Specific outreach requested
        outreach = get_outreach(outreach_id)
        if not outreach:
            await client.close()
            return f"Outreach not found: {outreach_id}"
        if outreach["status"] != "connected":
            await client.close()
            return (
                f"Outreach status is '{outreach['status']}', not 'connected'.\n"
                "Follow-ups can only be sent to connected prospects."
            )
        if outreach.get("followup_count", 0) >= max_followups:
            await client.close()
            return (
                f"Max follow-ups reached ({max_followups}) for this outreach.\n"
                f"{'Upgrade to Pro for up to 5 follow-ups.' if tier != TIER_PRO else 'Maximum follow-ups sent.'}"
            )
        campaign = get_campaign(outreach["campaign_id"])
        if not campaign:
            await client.close()
            return "Campaign not found for this outreach."
        campaign_id = campaign["id"]
        candidate = get_outreach_with_contact(outreach_id)
        if not candidate:
            await client.close()
            return "Could not load contact data for this outreach."
    else:
        # Find campaign
        campaign, err = find_active_campaign(campaign_id)
        if not campaign:
            await client.close()
            return err
        campaign_id = campaign["id"]

        # Block sends when campaign is paused
        if campaign.get("status") == "paused":
            await client.close()
            return (
                f"Campaign '{campaign['name']}' is paused.\n\n"
                "No messages will be sent while the campaign is paused.\n"
                "Use resume_campaign() to resume outreach."
            )

        # Find next follow-up candidate
        candidates = get_followup_candidates(campaign_id, max_followups)
        if not candidates:
            await client.close()
            return (
                f"No prospects ready for follow-up in '{campaign['name']}'.\n\n"
                "Prospects need to accept your connection first (status: 'connected').\n"
                "Use check_replies() to detect new connections, or generate_and_send() "
                "to reach new prospects."
            )
        candidate = candidates[0]
        outreach_id = candidate["outreach_id"]

    # ── Step 2: Check tier limits ──
    if tier != TIER_PRO:
        usage = 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 follow-ups."
            )

    # ── Step 3: Check follow-up schedule (Pro tier) ──
    followup_count = candidate.get("followup_count", 0)
    if tier == TIER_PRO and followup_count > 0:
        outreach_data = get_outreach(outreach_id) or {}
        last_updated = outreach_data.get("updated_at", 0)
        days_since_last = (int(time.time()) - last_updated) // 86400
        schedule_idx = min(followup_count, len(PRO_FOLLOWUP_SCHEDULE_DAYS) - 1)
        required_days = PRO_FOLLOWUP_SCHEDULE_DAYS[schedule_idx]
        if days_since_last < required_days:
            await client.close()
            wait_days = required_days - days_since_last
            return (
                f"Too early for follow-up #{followup_count + 1}.\n\n"
                f"Schedule: wait {required_days} days between follow-ups.\n"
                f"Next follow-up in {wait_days} day{'s' if wait_days != 1 else ''}."
            )

    # ── Step 4: Load conversation history ──
    messages = get_messages_for_outreach(outreach_id)

    # ── Step 5: Resolve chat_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

    # No existing chat — will create one when sending (first DM after acceptance).
    # Flag so the send step uses send_new_message() instead of send_message().
    create_new_chat = chat_id is None

    # For new chats, we need the provider_id (not the public slug).
    # Extract from miniProfileUrn in the LinkedIn URL if available.
    prospect_provider_id = prospect_linkedin_id
    if create_new_chat and not prospect_linkedin_id.startswith("ACo"):
        url = candidate.get("linkedin_url") or ""
        from urllib.parse import urlparse, parse_qs, unquote
        qs = parse_qs(urlparse(url).query)
        urn = unquote(qs.get("miniProfileUrn", [""])[0])
        if urn:
            prospect_provider_id = urn.split(":")[-1]

    # ── Step 6: Generate follow-up message ──
    sender_profile = get_setting("profile", {})
    voice_signature = get_setting("voice_signature", {})
    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", ""),
    }

    # Load campaign context (offerings, case_studies, social_proofs, preferences)
    campaign_ctx = get_campaign_context(campaign_id)

    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_id") or candidate.get("contact_db_id", "")
    if contact_db_id:
        prospect_analysis = 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:
                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
    ]

    # ── Generate → Improve → Validate → Fix pipeline ──
    message = ""
    reasoning = {}
    validation = None
    previous_sdr_messages = [m["text"] for m in messages if m.get("role") == "sdr"]

    # Attempt 1: Generate + Improve + Validate (+ Fix if needed)
    memory_entry = None
    try:
        result = await generate_followup(
            prospect=prospect_data,
            sender_profile=sender_profile,
            voice_signature=voice_signature,
            campaign_context=campaign_context,
            conversation_history=conversation_history,
            followup_number=followup_count + 1,
            prospect_analysis=prospect_analysis,
            campaign_ctx=campaign_ctx,
            outreach_id=outreach_id,
        )
        message = result.get("message", "")
        reasoning = result.get("reasoning", {})
        memory_entry = result.get("memory_entry")
    except Exception as e:
        logger.error(f"Follow-up generation failed: {e}")
        await client.close()
        return f"Failed to generate follow-up: {e}"

    # Improve stage — polish for naturalness
    try:
        message = await improve_message(
            draft=message,
            voice_signature=voice_signature,
            message_type="followup",
            max_chars=FOLLOWUP_MAX_CHARS,
        )
    except Exception as e:
        logger.warning(f"Improve stage failed, using raw message: {e}")

    # Validate (rule-based)
    validation = validate_followup(
        message,
        voice_signature,
        previous_messages=previous_sdr_messages,
        max_chars=FOLLOWUP_MAX_CHARS,
    )

    # LLM validation — context-sensitive checks (v63 validate prompt)
    if validation.is_valid:
        try:
            sender_company = sender_profile.get("company", "")
            llm_result = await llm_validate(
                message=message,
                history=conversation_history,
                company=sender_company,
                calendar_link=campaign_context.get("booking_link", ""),
            )
            if not llm_result.is_valid:
                validation.issues.extend(llm_result.issues)
                validation.is_valid = False
                logger.info("LLM validation caught issues: %s", llm_result.issues)
        except Exception as e:
            logger.warning(f"LLM validation skipped: {e}")

    # Fix stage — if validation failed, surgically fix issues
    if not validation.is_valid:
        logger.info(f"Follow-up validation failed, attempting fix: {validation.issues}")
        try:
            message = await fix_message(
                message=message,
                issues=validation.issues,
                voice_signature=voice_signature,
                message_type="followup",
                max_chars=FOLLOWUP_MAX_CHARS,
            )
            validation = validate_followup(
                message,
                voice_signature,
                previous_messages=previous_sdr_messages,
                max_chars=FOLLOWUP_MAX_CHARS,
            )
        except Exception as e:
            logger.warning(f"Fix stage failed: {e}")

    # Last resort: regenerate from scratch if still invalid
    if not validation.is_valid:
        logger.info(f"Fix failed, regenerating follow-up from scratch: {validation.issues}")
        try:
            result = await generate_followup(
                prospect=prospect_data,
                sender_profile=sender_profile,
                voice_signature=voice_signature,
                campaign_context=campaign_context,
                conversation_history=conversation_history,
                followup_number=followup_count + 1,
                prospect_analysis=prospect_analysis,
                campaign_ctx=campaign_ctx,
                outreach_id=outreach_id,
            )
            message = result.get("message", "")
            reasoning = result.get("reasoning", {})
            memory_entry = result.get("memory_entry")
            message = await improve_message(
                draft=message,
                voice_signature=voice_signature,
                message_type="followup",
                max_chars=FOLLOWUP_MAX_CHARS,
            )
            validation = validate_followup(
                message,
                voice_signature,
                previous_messages=previous_sdr_messages,
                max_chars=FOLLOWUP_MAX_CHARS,
            )
        except Exception as e:
            logger.error(f"Regeneration failed: {e}")

    if not validation or not validation.is_valid:
        issues_text = "\n".join(f"  ⚠️ {issue}" for issue in (validation.issues if validation else []))
        if mode == "autopilot":
            logger.warning(f"Autopilot blocked invalid follow-up: {issues_text}")
            log_action("validation_blocked", outreach_id=outreach_id, result="blocked",
                       details={"issues": validation.issues if validation else [], "type": "followup"})
            await client.close()
            return (
                f"⚠️ Follow-up for {candidate.get('name', 'Unknown')} failed validation "
                f"after Generate → Improve → Fix pipeline.\n\n"
                f"Issues:\n{issues_text}\n\n"
                "The message was NOT sent to protect your account.\n"
                "Try: send_followup(mode='copilot') to review and edit manually."
            )
        else:
            logger.warning(f"Copilot follow-up 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

    if mode == "copilot":
        # Show message for review
        update_outreach(outreach_id, next_action=message)

        output = [
            f"Follow-up #{followup_count + 1} for **{prospect_name}** ({role_str}):",
            f"   Fit: {stars(fit_score)}",
            "",
            f'   "{message}"',
            f"   ({len(message)}/{FOLLOWUP_MAX_CHARS} chars)",
            "",
        ]

        # Show reasoning
        if reasoning:
            hook = reasoning.get("hook", "")
            cta = reasoning.get("cta_choice", "")
            if hook:
                output.append(f"   Angle: {hook}")
            if cta:
                output.append(f"   CTA type: {cta}")
            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 follow-up",
            "  '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

        # ── EMAIL FOLLOW-UP PATH ──
        outreach_record = get_outreach(outreach_id)
        outreach_channel = (outreach_record or {}).get("channel", "linkedin")
        if outreach_channel == "email":
            return await _send_email_followup(
                client, account_id, outreach_id, candidate,
                prospect_name, role_str, message, followup_count,
                max_followups, voice_signature, reasoning, memory_entry,
            )

        # ── LINKEDIN DM PATH ──
        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:
                if create_new_chat:
                    send_result = await client.send_new_voice_message(
                        account_id=account_id,
                        provider_id=prospect_provider_id,
                        audio_path=audio_path,
                    )
                else:
                    send_result = await client.send_voice_message(
                        account_id=account_id,
                        chat_id=chat_id,
                        audio_path=audio_path,
                    )
                # If voice send failed, fall back to text
                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":
                if create_new_chat:
                    send_result = await client.send_new_message(
                        account_id=account_id,
                        provider_id=prospect_provider_id,
                        text=message,
                    )
                else:
                    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 follow-up: {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 DB
            new_count = followup_count + 1
            update_outreach(outreach_id, status="messaged", followup_count=new_count)
            save_message(outreach_id, role="sdr", text=message, format=actual_format)
            increment_usage("messages_sent")

            # Save conversation memory for future follow-ups
            if memory_entry:
                try:
                    from ..db.queries import save_outreach_memory
                    save_outreach_memory(outreach_id, memory_entry)
                except Exception as e:
                    logger.debug("Memory save failed: %s", e)
            log_action(
                "followup_sent",
                outreach_id=outreach_id,
                result="success",
                details={
                    "prospect": prospect_name,
                    "followup_number": new_count,
                    "message_length": len(message),
                    "reasoning": reasoning,
                },
            )

            remaining = max_followups - new_count
            return (
                f"Sent follow-up #{new_count} to {prospect_name} ({role_str})\n"
                f'   "{message}"\n\n'
                f"Follow-ups remaining: {remaining}/{max_followups}"
            )
        else:
            error = send_result.get("error", "Unknown error")
            log_action(
                "followup_failed",
                outreach_id=outreach_id,
                result="error",
                details={"error": error},
            )
            return f"Follow-up failed for {prospect_name}: {error}"


async def _send_email_followup(
    client: Any,
    account_id: str,
    outreach_id: str,
    candidate: dict,
    prospect_name: str,
    role_str: str,
    message: str,
    followup_count: int,
    max_followups: int,
    voice_signature: dict,
    reasoning: str,
    memory_entry: dict | None,
) -> str:
    """Send a follow-up via email channel."""
    from ..ai.email_generator import generate_email_followup
    from ..services.channel_selector import get_email_account_id, _extract_email

    email_account_id = get_email_account_id()
    if not email_account_id:
        await client.close()
        return "No email account connected for follow-up."

    prospect_email = _extract_email(candidate)
    if not prospect_email:
        await client.close()
        return f"No email address for {prospect_name}. Cannot send email follow-up."

    # Get previous email subject from messages
    previous_messages = get_messages_for_outreach(outreach_id)
    previous_subject = ""
    previous_body = ""
    for msg in previous_messages:
        if msg.get("role") == "sdr":
            text = msg.get("text", "")
            if text.startswith("[EMAIL] Subject: "):
                lines = text.split("\n\n", 1)
                previous_subject = lines[0].replace("[EMAIL] Subject: ", "")
                previous_body = lines[1] if len(lines) > 1 else ""
            break

    sender_profile = get_setting("profile", {})
    try:
        email_result = await generate_email_followup(
            prospect={"name": prospect_name, **candidate},
            sender_profile=sender_profile,
            voice_signature=voice_signature,
            previous_subject=previous_subject,
            previous_body=previous_body,
            followup_count=followup_count,
        )
    except Exception as e:
        await client.close()
        return f"Email follow-up generation failed: {e}"

    subject = email_result["subject"]
    body = email_result["body"]

    try:
        result = await client.send_email(
            account_id=email_account_id,
            to_email=prospect_email,
            to_name=prospect_name,
            subject=subject,
            body=body,
        )
    except Exception as e:
        await client.close()
        return f"Email follow-up send failed: {e}"
    finally:
        await client.close()

    if result.get("success"):
        new_count = followup_count + 1
        update_outreach(outreach_id, status="messaged", followup_count=new_count)
        save_message(outreach_id, role="sdr", text=f"[EMAIL] Subject: {subject}\n\n{body}")
        increment_usage("messages_sent")

        if memory_entry:
            try:
                from ..db.queries import save_outreach_memory
                save_outreach_memory(outreach_id, memory_entry)
            except Exception:
                pass

        log_action(
            "email_followup_sent", outreach_id=outreach_id, result="success",
            details={
                "prospect": prospect_name,
                "followup_number": new_count,
                "channel": "email",
                "subject": subject,
            },
        )

        remaining = max_followups - new_count
        return (
            f"📧 Email follow-up #{new_count} sent to {prospect_name} ({role_str})\n"
            f"   Subject: {subject}\n"
            f'   Body: "{body[:120]}..."\n\n'
            f"Follow-ups remaining: {remaining}/{max_followups}"
        )
    else:
        error = result.get("error", "Unknown error")
        log_action(
            "email_followup_failed", outreach_id=outreach_id, result="error",
            details={"error": error, "channel": "email"},
        )
        return f"Email follow-up failed for {prospect_name}: {error}"
