"""Tool 3: generate_and_send — Generate a personalized message and send (or queue for review).

Core outreach loop:
1. Pick next prospect from campaign queue
2. Generate personalized invitation message (voice-matched)
3. Run 5-stage validation
4. In Copilot mode: show for approval. In Autopilot: send immediately.
5. Track rate limits and scheduling
"""

from __future__ import annotations

import json
import logging
import time
from typing import Any

from ..ai.message_fixer import fix_message
from ..ai.message_generator import generate_message
from ..ai.message_improver import improve_message
from ..ai.message_validator import validate_message
from ..ai.llm_validator import llm_validate
from ..ai.prospect_analyzer import analyze_prospect
from ..config import get_tier
from ..constants import (
    COPILOT_APPROVAL_THRESHOLD,
    FREE_MONTHLY_INVITATIONS,
    INVITATION_NOTE_MAX_CHARS,
    MIN_WARMUP_ENGAGEMENTS,
    TIER_PRO,
)
from ..db.queries import (
    find_active_campaign,
    get_campaign_context,
    get_contact_analysis,
    get_contacts_for_campaign,
    get_engagement_count_for_outreach,
    get_monthly_usage,
    get_setting,
    increment_sent,
    increment_usage,
    log_action,
    save_contact_analysis,
    save_message,
    update_campaign,
    update_outreach,
)
from ..formatter import stars
from ..linkedin.rate_limiter import can_send_now, get_next_delay, update_limits_after_send
from ..services.channel_selector import (
    CHANNEL_EMAIL,
    CHANNEL_LINKEDIN,
    select_channel,
    has_email_channel,
    _extract_email,
)
from ..linkedin import (
    UnipileAuthError,
    UnipileError,
    get_account_id,
    get_linkedin_client,
)

logger = logging.getLogger(__name__)


async def run_generate_and_send(
    campaign_id: str = "",
    mode: str = "autopilot",
    force_channel: str = "",
    target_outreach_id: str = "",
) -> str:
    """Generate and send (or queue) a personalized LinkedIn message.

    Flow:
    1. Find the active campaign and next pending prospect
    2. Generate a personalized message using voice signature
    3. Validate the message (5-stage pipeline)
    4. Copilot: show for review. Autopilot: send directly.

    Args:
        campaign_id: Campaign to send from.
        mode: "autopilot" or "copilot".
        force_channel: Override channel choice ("email" forces email path).
        target_outreach_id: Specific outreach to send (for email overflow).
    """

    # ── Step 0: Pre-checks ──
    setup_done = get_setting("setup_complete", False)
    if not setup_done:
        return (
            "❌ Setup required before sending messages.\n\n"
            "Please run setup_profile first — it connects your LinkedIn account and "
            "creates your voice signature so messages sound like you.\n\n"
            "Say 'set up my profile' and I'll walk you through it step by step."
        )

    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 + next prospect ──
    campaign, err = find_active_campaign(campaign_id)
    if not campaign:
        return f"❌ {err}"
    campaign_id = campaign["id"]

    # Block sends when campaign is paused
    if campaign.get("status") == "paused":
        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 pending outreach (or specific target for email overflow)
    from ..db.schema import get_db
    db = get_db()
    if target_outreach_id:
        # Specific outreach requested (e.g., email overflow from scheduler)
        row = db.execute(
            """SELECT o.id as outreach_id, o.contact_id, o.variant,
                      c.id as contact_db_id, c.campaign_id as contact_campaign_id,
                      c.name, c.title, c.company, c.linkedin_url, c.linkedin_id,
                      c.profile_json, c.analysis_json, c.fit_score, c.status as contact_status
               FROM outreaches o
               JOIN contacts c ON o.contact_id = c.id
               WHERE o.id = ? AND o.status = 'pending'
               LIMIT 1""",
            (target_outreach_id,),
        ).fetchone()
    else:
        row = db.execute(
            """SELECT o.id as outreach_id, o.contact_id, o.variant,
                      c.id as contact_db_id, c.campaign_id as contact_campaign_id,
                      c.name, c.title, c.company, c.linkedin_url, c.linkedin_id,
                      c.profile_json, c.analysis_json, c.fit_score, c.status as contact_status
               FROM outreaches o
               JOIN contacts c ON o.contact_id = c.id
               WHERE o.campaign_id = ? AND o.status = 'pending'
               ORDER BY c.fit_score DESC
               LIMIT 1""",
            (campaign_id,),
        ).fetchone()
    db.close()

    if not row:
        return (
            f"✅ All prospects in this campaign have been reached!\n\n"
            f"Campaign: {campaign['name']}\n"
            "Use show_status to see results, or create_campaign for a new target."
        )

    prospect = dict(row)
    outreach_id = prospect["outreach_id"]
    contact_id = prospect["contact_id"]

    # ── Channel selection ──
    from ..linkedin.rate_limiter import BLOCK_DAILY, BLOCK_WEEKLY
    _, _, block_type = can_send_now()
    linkedin_limit_hit = block_type in (BLOCK_DAILY, BLOCK_WEEKLY)

    outreach_data = {"channel": "linkedin"}  # Default
    channel = select_channel(
        outreach=outreach_data,
        prospect=prospect,
        campaign_config=json.loads(campaign.get("config_json") or "{}"),
        force_channel=force_channel,
        linkedin_limit_hit=linkedin_limit_hit,
    )

    # ── Step 1.5: Warm-up check (skip for DM channel — existing connections) ──
    eng_count = get_engagement_count_for_outreach(outreach_id)

    if eng_count < MIN_WARMUP_ENGAGEMENTS and mode == "autopilot" and channel != "dm":
        # Autopilot: skip un-warmed prospects, find a warmed-up one
        from ..db.schema import get_db as _get_db
        db2 = _get_db()
        warmed_row = db2.execute(
            """SELECT o.id as outreach_id, o.contact_id, o.variant,
                      c.id as contact_db_id, c.campaign_id as contact_campaign_id,
                      c.name, c.title, c.company, c.linkedin_url, c.linkedin_id,
                      c.profile_json, c.analysis_json, c.fit_score, c.status as contact_status
               FROM outreaches o
               JOIN contacts c ON o.contact_id = c.id
               WHERE o.campaign_id = ? AND o.status = 'pending'
                 AND (SELECT COUNT(*) FROM engagements e WHERE e.outreach_id = o.id) >= ?
               ORDER BY c.fit_score DESC
               LIMIT 1""",
            (campaign_id, MIN_WARMUP_ENGAGEMENTS),
        ).fetchone()
        db2.close()

        if warmed_row:
            prospect = dict(warmed_row)
            outreach_id = prospect["outreach_id"]
            contact_id = prospect["contact_id"]
            eng_count = MIN_WARMUP_ENGAGEMENTS  # Known to be warmed
        else:
            return (
                f"⏸️ No warmed-up prospects in '{campaign['name']}'.\n\n"
                f"All pending prospects need engagement warm-up first.\n"
                f"Run engage_prospect() to warm them up, then try again.\n"
                f"Or use copilot mode to override: generate_and_send(mode='copilot')"
            )

    # ── Step 2: Check rate limits ──
    can_send, reason, _block = can_send_now()
    if not can_send and mode == "autopilot":
        if channel == CHANNEL_EMAIL:
            # Email overflow — LinkedIn limits don't block email sends.
            # Check email-specific limits instead.
            from ..linkedin.rate_limiter import can_send_email_now
            can_email, email_reason = can_send_email_now()
            if not can_email:
                return f"⏸️ Email sending paused: {email_reason}\n\nThe queue is ready — will resume automatically."
            # Proceed with email path
        else:
            return f"⏸️ Sending paused: {reason}\n\nThe queue is ready — will resume automatically."

    # Check free tier monthly limit
    tier = get_tier()
    if tier != TIER_PRO:
        usage = get_monthly_usage()
        if usage.get("invitations_sent", 0) >= FREE_MONTHLY_INVITATIONS:
            return (
                f"⚠️ Free tier limit reached: {FREE_MONTHLY_INVITATIONS} invitations/month.\n\n"
                "Upgrade to Pro ($29/mo) for unlimited outreach.\n"
                "Your campaign will resume next month if you stay on Free."
            )

    # ── Step 3: Generate 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", ""),
    }

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

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

    # ── Company Enrichment: fetch company profile for better personalization ──
    company_name = prospect_data.get("company") or prospect.get("company", "")
    if company_name and not prospect_data.get("company_data"):
        try:
            company_data = await client.get_company_profile(account_id, company_name)
            if company_data and company_data.get("name"):
                prospect_data["company_data"] = company_data
                logger.info(f"Enriched company data for {company_name}: {company_data.get('industry', 'unknown industry')}")
        except Exception as e:
            logger.debug(f"Company enrichment failed for {company_name} (non-critical): {e}")

    # ── Mutual Connections: check for warm intro paths ──
    mutual_info = ""
    try:
        linkedin_id = prospect.get("linkedin_id") or prospect_data.get("public_id") or ""
        if linkedin_id:
            profile = await client.get_profile(account_id, linkedin_id)
            shared = 0
            if isinstance(profile, dict):
                shared = profile.get("shared_connections") or profile.get("mutual_connections") or 0
            if shared and int(shared) > 0:
                mutual_info = f"You share {shared} mutual connection(s) with this prospect."
                prospect_data["mutual_connections"] = int(shared)
                logger.info("Warm intro path: %d mutual connections with %s", shared, prospect.get("name", ""))
    except Exception as e:
        logger.debug("Mutual connection check failed (non-critical): %s", e)

    # ── Job Intent Signals: check if prospect's company is hiring ──
    try:
        prospect_co = prospect_data.get("company") or prospect.get("company", "")
        campaign_target = campaign_context.get("target_description", "")
        if prospect_co and campaign_target:
            from ..services.intent_signals import search_job_intent, build_intent_context
            intent_companies = await search_job_intent(
                client, account_id, campaign_target, limit=10,
            )
            # Find if prospect's company is among hiring companies
            co_lower = prospect_co.lower().strip()
            for ic in intent_companies:
                if ic["company_name"].lower().strip() == co_lower:
                    prospect_data["hiring_intent"] = ic["intent_signal"]
                    prospect_data["hiring_jobs"] = ic["jobs"][:3]
                    logger.info("Hiring intent found for %s: %s", prospect_co, ic["intent_signal"])
                    break
    except Exception as e:
        logger.debug("Job intent check failed (non-critical): %s", e)

    # ── Prospect Intelligence: analyze or load cached ──
    prospect_analysis = None
    contact_db_id = prospect.get("contact_db_id") or contact_id
    cached = get_contact_analysis(contact_db_id)
    if cached:
        prospect_analysis = cached
        logger.info(f"Loaded cached prospect analysis for {prospect.get('name', 'Unknown')}")
    else:
        try:
            prospect_analysis = await analyze_prospect(
                prospect=prospect_data,
                campaign_context=campaign_context,
                icp_data=icp_data,
            )
            save_contact_analysis(contact_db_id, prospect_analysis)
            logger.info(f"Generated and cached prospect analysis for {prospect.get('name', 'Unknown')}")
        except Exception as e:
            logger.warning(f"Prospect analysis failed, proceeding without: {e}")

    # ── A/B Test Variant: inject variant instructions if test is running ──
    outreach_variant = prospect.get("variant") if prospect else None
    if outreach_variant and campaign_id:
        try:
            from ..db.queries import list_ab_tests
            running_tests = list_ab_tests(campaign_id, status="running")
            if running_tests:
                test = running_tests[0]
                variant_desc = test["variant_a"] if outreach_variant == "A" else test["variant_b"]
                variant_instruction = (
                    f"\n\nA/B TEST ACTIVE — This prospect is in Variant {outreach_variant}.\n"
                    f"Messaging instruction for this variant: {variant_desc}\n"
                    f"Follow this instruction precisely for controlled testing."
                )
                campaign_ctx = campaign_ctx or {}
                existing_prefs = campaign_ctx.get("campaign_preferences", "")
                campaign_ctx["campaign_preferences"] = (existing_prefs + variant_instruction).strip()
                logger.info("Applied A/B variant %s instruction for outreach", outreach_variant)
        except Exception as e:
            logger.debug("A/B variant injection failed (non-critical): %s", e)

    # ── Generate → Improve → Validate → Fix pipeline ──
    message = ""
    reasoning = ""
    validation = None

    # Attempt 1: Generate + Improve + Validate (+ Fix if needed)
    try:
        result = await generate_message(
            prospect=prospect_data,
            sender_profile=sender_profile,
            voice_signature=voice_signature,
            campaign_context=campaign_context,
            prospect_analysis=prospect_analysis,
            campaign_ctx=campaign_ctx,
        )
        message = result["message"]
        reasoning = result.get("reasoning", "")
    except Exception as e:
        logger.error(f"Message generation failed: {e}")
        return f"❌ Failed to generate message: {e}"

    # Log reasoning for debugging/quality analysis
    if reasoning:
        log_action("message_reasoning", outreach_id=outreach_id,
                   details={"reasoning": reasoning[:500]})

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

    # Validate (rule-based)
    validation = validate_message(message, voice_signature, INVITATION_NOTE_MAX_CHARS)

    # LLM validation — context-sensitive checks (guardrails, company names, etc.)
    if validation.is_valid:
        try:
            sender_company = sender_profile.get("company", "")
            prospect_co = prospect_data.get("company", prospect.get("company", ""))
            llm_result = await llm_validate(
                message=message,
                history=[],
                company=sender_company,
                message_type="invitation",
                prospect_company=prospect_co,
                max_chars=INVITATION_NOTE_MAX_CHARS,
            )
            if not llm_result.is_valid:
                validation.issues.extend(llm_result.issues)
                validation.is_valid = False
                logger.info("LLM validation caught invitation 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"Validation failed, attempting fix: {validation.issues}")
        try:
            message = await fix_message(
                message=message,
                issues=validation.issues,
                voice_signature=voice_signature,
                message_type="invitation",
                max_chars=INVITATION_NOTE_MAX_CHARS,
            )
            validation = validate_message(message, voice_signature, INVITATION_NOTE_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 from scratch: {validation.issues}")
        try:
            result = await generate_message(
                prospect=prospect_data,
                sender_profile=sender_profile,
                voice_signature=voice_signature,
                campaign_context=campaign_context,
                prospect_analysis=prospect_analysis,
                campaign_ctx=campaign_ctx,
            )
            message = result["message"]
            message = await improve_message(
                draft=message,
                voice_signature=voice_signature,
                message_type="invitation",
                max_chars=INVITATION_NOTE_MAX_CHARS,
            )
            validation = validate_message(message, voice_signature, INVITATION_NOTE_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":
            # CRITICAL: Never send invalid messages in autopilot mode
            logger.warning(f"Autopilot blocked invalid message: {issues_text}")
            log_action("validation_blocked", outreach_id=outreach_id, result="blocked",
                       details={"issues": validation.issues if validation else []})
            return (
                f"⚠️ Message for {prospect.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: generate_and_send(mode='copilot') to review and edit manually."
            )
        else:
            # Copilot: user will see warnings and decide
            logger.warning(f"Copilot message has validation warnings: {issues_text}")

    # ── Step 4: Copilot vs Autopilot ──
    prospect_name = prospect.get("name", "Unknown")
    prospect_title = prospect.get("title", "")
    prospect_company = prospect.get("company", "")
    prospect_url = prospect.get("linkedin_url", "")
    fit_score = prospect.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 — don't send yet
        # Store the message in the outreach record so we can send it on approval
        update_outreach(outreach_id, status="review_pending", next_action=message)

        output = [
            f"📝 Message for **{prospect_name}** ({role_str}):",
            f"   Fit: {stars(fit_score)}",
            f"   {prospect_url}" if prospect_url else "",
            "",
            f'   "{message}"',
            f"   ({len(message)}/200 chars)",
            "",
        ]

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

        # Mutual connections hint
        if mutual_info:
            output.append(f"   🤝 {mutual_info}")
            output.append("")

        # Warm-up warning in copilot mode
        if eng_count < MIN_WARMUP_ENGAGEMENTS:
            output.append(
                f"   ⚠️ Warm-up: {eng_count}/{MIN_WARMUP_ENGAGEMENTS} engagements. "
                "Consider engage_prospect() first for better acceptance."
            )
            output.append("")

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

        # Track approvals for Autopilot prompt
        approval_count = get_setting("copilot_approvals", 0)
        if approval_count >= COPILOT_APPROVAL_THRESHOLD - 1:
            output.extend([
                "",
                f"💡 You've approved {approval_count + 1} messages in a row.",
                "   Ready to switch to Autopilot? Say 'autopilot' and I'll handle the rest.",
            ])

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

    else:
        # Autopilot — send immediately
        if not can_send:
            update_outreach(outreach_id, next_action=message)
            return f"⏸️ Queued for later: {reason}"

        # ── EMAIL CHANNEL PATH ──
        if channel == CHANNEL_EMAIL:
            return await _send_email_outreach(
                client, account_id, outreach_id, prospect, prospect_data,
                prospect_name, role_str, message, voice_signature,
                campaign_context, campaign_ctx, prospect_analysis,
            )

        # ── DM CHANNEL PATH (connections-only campaigns) ──
        if channel == "dm":
            provider_id = (
                prospect_data.get("provider_id", "")
                or prospect.get("linkedin_id", "")
                or prospect_data.get("public_id", "")
            )
            if not provider_id:
                update_outreach(outreach_id, status="error")
                await client.close()
                return f"❌ No LinkedIn ID for {prospect_name}. Skipping."

            try:
                result = await client.send_new_message(
                    account_id=account_id,
                    provider_id=provider_id,
                    text=message,
                )
                if result.get("success"):
                    update_outreach(outreach_id, status="messaged", channel="linkedin",
                                    last_attempt_error=None)
                    save_message(outreach_id, role="sdr", text=message)
                    increment_usage("messages_sent")
                    log_action("dm_sent", outreach_id=outreach_id, result="success",
                               details={"prospect": prospect_name, "message_length": len(message)})
                    await client.close()
                    return (
                        f"✅ DM sent to {prospect_name} ({role_str})\n"
                        f'   "{message}"\n'
                    )
                else:
                    error = result.get("error", "Unknown error")
                    update_outreach(outreach_id, last_attempt_error=error)
                    await client.close()
                    return f"❌ DM failed for {prospect_name}: {error}"
            except Exception as e:
                update_outreach(outreach_id, last_attempt_error=str(e))
                await client.close()
                return f"❌ DM failed for {prospect_name}: {e}"

        # ── LINKEDIN CHANNEL PATH ──
        # Get the provider_id for Unipile invitation
        provider_id = (
            prospect_data.get("provider_id", "")
            or prospect.get("linkedin_id", "")
            or prospect_data.get("public_id", "")
        )
        if not provider_id:
            update_outreach(outreach_id, status="error")
            await client.close()
            return f"❌ No LinkedIn ID for {prospect_name}. Skipping."

        # Track invite attempt
        try:
            from ..db.queries import get_outreach
            current = get_outreach(outreach_id) or {}
            attempts = (current.get("invite_attempts") or 0) + 1
            update_outreach(outreach_id, invite_attempts=attempts)
        except Exception:
            attempts = 1

        # Circuit-breaker: skip after too many failed attempts
        from ..constants import MAX_INVITE_ATTEMPTS
        if attempts > MAX_INVITE_ATTEMPTS:
            update_outreach(outreach_id, status="skipped",
                            last_attempt_error=f"Exceeded {MAX_INVITE_ATTEMPTS} invite attempts")
            log_action("invitation_max_attempts", outreach_id=outreach_id,
                       result="skipped", details={"attempts": attempts, "prospect": prospect_name})
            await client.close()
            return f"Skipped {prospect_name}: exceeded {MAX_INVITE_ATTEMPTS} invite attempts."

        # Send the invitation via Unipile
        result = await client.send_invitation(
            account_id=account_id,
            provider_id=provider_id,
            message=message,
        )

        try:
            # Update rate limits
            increment_sent()
            new_limit = update_limits_after_send(blocked=result.get("blocked", False))

            if result["success"]:
                # Update DB
                update_outreach(outreach_id, status="invited", channel=CHANNEL_LINKEDIN, last_attempt_error=None)
                save_message(outreach_id, role="sdr", text=message)
                increment_usage("invitations_sent")
                log_action("invitation_sent", outreach_id=outreach_id, result="success",
                           details={"prospect": prospect_name, "message_length": len(message)})

                delay = get_next_delay()
                delay_minutes = delay // 60

                return (
                    f"✅ Sent to {prospect_name} ({role_str})\n"
                    f'   "{message}"\n\n'
                    f"⏱️ Next send in ~{delay_minutes} minutes\n"
                    f"📊 Daily limit: {new_limit}/day"
                )
            else:
                error = result.get("error", "Unknown error")
                if result.get("auth_error"):
                    # Account disconnected — don't change outreach status, just alert user
                    update_outreach(outreach_id, status="pending")
                    log_action("auth_error", outreach_id=outreach_id, result="blocked",
                               details={"error": error})
                    return (
                        "🔑 LinkedIn account disconnected.\n\n"
                        "Run setup_profile() again to reconnect your LinkedIn account."
                    )
                elif result.get("rate_limited_422"):
                    # 422 temporary_provider_limit — LinkedIn restricts personalized
                    # invites (weekly limit or non-Sales Navigator). Retry without message.
                    if message:
                        log_action("invitation_retry_no_message", outreach_id=outreach_id,
                                   result="retrying", details={"error": error})
                        result2 = await client.send_invitation(
                            account_id=account_id,
                            provider_id=provider_id,
                            message="",
                        )
                        if result2["success"]:
                            update_outreach(outreach_id, status="invited", channel=CHANNEL_LINKEDIN,
                                            last_attempt_error=None)
                            save_message(outreach_id, role="sdr", text="")
                            increment_usage("invitations_sent")
                            log_action("invitation_sent", outreach_id=outreach_id, result="success",
                                       details={"prospect": prospect_name, "message_length": 0,
                                                "note": "retried without message (422 rate limit)"})
                            return (
                                f"✅ Sent to {prospect_name} (without personalized message)\n"
                                f"   LinkedIn restricted personalized invites — sent blank invite.\n\n"
                                f"📊 Daily limit: {new_limit}/day"
                            )
                        # Retry also failed — treat as rate limit block
                        error = result2.get("error", error)
                    # No message or retry also failed: keep as pending, don't skip
                    update_outreach(outreach_id, status="pending", last_attempt_error=error[:500])
                    log_action("invitation_blocked", outreach_id=outreach_id, result="blocked",
                               details={"error": error, "note": "422 weekly/personalized limit"})
                    return (
                        f"⚠️ LinkedIn invitation limit (422): {error}\n\n"
                        f"Daily limit reduced to {new_limit}. Will retry later."
                    )
                elif result.get("blocked"):
                    update_outreach(outreach_id, status="pending", last_attempt_error=error[:500])
                    log_action("invitation_blocked", outreach_id=outreach_id, result="blocked",
                               details={"error": error})
                    return (
                        f"⚠️ LinkedIn blocked the send: {error}\n\n"
                        f"Daily limit reduced to {new_limit}. Will retry later."
                    )
                elif "422" in str(error):
                    # 422 = invalid profile or permanently rejected — don't retry
                    update_outreach(outreach_id, status="skipped", last_attempt_error=error[:500])
                    log_action("invitation_permanent_error", outreach_id=outreach_id, result="skipped",
                               details={"error": error, "attempts": attempts})
                    return f"Skipped {prospect_name}: permanent error (422) — {error}"
                else:
                    update_outreach(outreach_id, status="error", last_attempt_error=error[:500])
                    log_action("invitation_failed", outreach_id=outreach_id, result="error",
                               details={"error": error})
                    return f"❌ Send failed for {prospect_name}: {error}"
        finally:
            await client.close()


async def _send_email_outreach(
    client: Any,
    account_id: str,
    outreach_id: str,
    prospect: dict,
    prospect_data: dict,
    prospect_name: str,
    role_str: str,
    linkedin_message: str,
    voice_signature: dict,
    campaign_context: dict,
    campaign_ctx: dict | None,
    prospect_analysis: dict | None,
) -> str:
    """Send outreach via email channel instead of LinkedIn.

    Generates a proper email (subject + body) and sends via connected email account.
    """
    from ..ai.email_generator import generate_email
    from ..services.channel_selector import get_email_account_id

    email_account_id = get_email_account_id()
    if not email_account_id:
        await client.close()
        return "❌ No email account connected. Connect email via setup_profile."

    # Extract prospect email
    prospect_email = _extract_email(prospect) or _extract_email(prospect_data)
    if not prospect_email:
        # Can't send email without an address — fall back to LinkedIn
        await client.close()
        return (
            f"❌ No email address found for {prospect_name}.\n\n"
            "Email outreach requires a prospect email. Falling back to LinkedIn.\n"
            "Run generate_and_send() again (will use LinkedIn)."
        )

    # Generate email
    sender_profile = get_setting("profile", {})
    full_ctx = dict(campaign_context)
    if campaign_ctx:
        full_ctx.update(campaign_ctx)

    try:
        email_result = await generate_email(
            prospect=prospect_data,
            sender_profile=sender_profile,
            voice_signature=voice_signature,
            campaign_context=full_ctx,
            prospect_analysis=prospect_analysis,
        )
    except Exception as e:
        await client.close()
        return f"❌ Email generation failed: {e}"

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

    # Send via Unipile email API
    try:
        result = await client.send_email(
            account_id=email_account_id,
            to_email=prospect_email,
            to_name=prospect_name,
            subject=subject,
            body=body,
            tracking_label=f"campaign_{outreach_id[:8]}",
        )
    except Exception as e:
        await client.close()
        return f"❌ Email send failed: {e}"
    finally:
        await client.close()

    if result.get("success"):
        update_outreach(outreach_id, status="invited", channel=CHANNEL_EMAIL)
        save_message(outreach_id, role="sdr", text=f"[EMAIL] Subject: {subject}\n\n{body}")
        increment_usage("invitations_sent")
        # Track email-specific rate limit
        from ..linkedin.rate_limiter import increment_email_sent as _inc_email
        _inc_email()
        log_action(
            "email_sent", outreach_id=outreach_id, result="success",
            details={
                "prospect": prospect_name,
                "email": prospect_email,
                "subject": subject,
                "channel": "email",
            },
        )
        return (
            f"📧 Email sent to {prospect_name} ({role_str})\n"
            f"   To: {prospect_email}\n"
            f"   Subject: {subject}\n"
            f'   Body: "{body[:150]}..."\n\n'
            "Open/click tracking enabled."
        )
    else:
        error = result.get("error", "Unknown error")
        update_outreach(outreach_id, status="error")
        log_action(
            "email_failed", outreach_id=outreach_id, result="error",
            details={"error": error, "channel": "email"},
        )
        return f"❌ Email failed for {prospect_name}: {error}"
