"""Backfill unreplied inbox messages — one-time catch-up script.

Scans the LinkedIn inbox, finds conversations where the prospect
messaged but never got a reply, and processes them through the
inbound pipeline (classify → send discovery DM).
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Any

from ..ai.inbound_qualifier import (
    InboundQualification,
    generate_discovery_question,
    qualify_inbound,
)
from ..constants import DAILY_INBOUND_DM_LIMIT
from ..db.async_bridge import run_db
from ..db.queries import (
    count_inbound_dms_today,
    count_inbound_dms_today,
    create_outreach,
    get_inbound_signal_by_sender,
    get_setting,
    list_campaigns,
    list_icps,
    list_inbound_signals,
    log_action,
    save_contact,
    save_inbound_signal,
    save_message,
    update_inbound_signal,
    update_outreach,
)
from ..db.signal_queries import get_contact_by_linkedin_id
from ..tools.inbox import _extract_chat_info, _fetch_raw_chats, _resolve_profile

logger = logging.getLogger(__name__)


async def run_backfill_inbox(
    limit: int = 50,
    dry_run: bool = False,
    min_confidence: float = 0.0,
    send_only: bool = False,
) -> str:
    """Scan inbox for unreplied messages and process them.

    Args:
        limit: Max conversations to scan.
        dry_run: If True, classify only — don't send DMs.
        min_confidence: Only send DMs for signals >= this confidence.
        send_only: If True, skip detection — send DMs for already-classified
            signals in the DB. Avoids API rate limits from re-scanning.

    Returns:
        Summary string.
    """
    from ..linkedin import UnipileError, get_account_id, get_linkedin_client

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

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

    try:
        if send_only:
            return await _send_classified_signals(
                client, account_id, limit, min_confidence,
            )
        return await _backfill(
            client, account_id, limit, dry_run, min_confidence,
        )
    except Exception as e:
        logger.error("Backfill error: %s", e, exc_info=True)
        return f"Backfill error: {e}"
    finally:
        await client.close()


async def _send_classified_signals(
    client: Any,
    account_id: str,
    limit: int,
    min_confidence: float,
) -> str:
    """Send DMs for already-classified inbound signals. No API scanning needed."""
    classified_signals = await run_db(
        list_inbound_signals, status="classified", signal_type="message", limit=limit,
    )
    if not classified_signals:
        return "No classified signals waiting to be actioned. Run `backfill_inbox()` first to detect and classify."

    today_sent = await run_db(count_inbound_dms_today)
    remaining = max(0, DAILY_INBOUND_DM_LIMIT - today_sent)
    if remaining == 0:
        return f"Daily DM limit reached ({today_sent}/{DAILY_INBOUND_DM_LIMIT}). Run again tomorrow."

    voice = await run_db(get_setting, "voice_signature", {})
    lines = [f"**Processing {len(classified_signals)} classified signals** (limit: {remaining} remaining today)\n"]
    sent = 0
    skipped = 0
    errors = 0

    for signal in classified_signals[:remaining]:
        intent = signal.get("intent", "unknown")
        confidence = signal.get("confidence", 0) or 0
        sender_id = signal.get("sender_id", "")
        sender_name = signal.get("sender_name", "Unknown")

        # Skip spam/vendor/job_seeking
        if intent in ("spam", "job_seeking", "vendor_pitch"):
            await run_db(update_inbound_signal, signal["id"], status="dismissed")
            lines.append(f"- {sender_name}: skipped ({intent})")
            skipped += 1
            continue

        if confidence < min_confidence:
            lines.append(f"- {sender_name}: skipped (confidence {confidence:.0%} < {min_confidence:.0%})")
            skipped += 1
            continue

        if not sender_id:
            skipped += 1
            continue

        # Skip if already a campaign contact
        existing_contact = await run_db(get_contact_by_linkedin_id, sender_id)
        if existing_contact:
            await run_db(update_inbound_signal, signal["id"], status="dismissed")
            lines.append(f"- {sender_name}: skipped (already a contact)")
            skipped += 1
            continue

        sender_profile = {
            "name": sender_name,
            "headline": signal.get("sender_headline", ""),
            "company": signal.get("sender_company", ""),
        }
        qual = InboundQualification(
            intent=intent,
            matched_icp_id=signal.get("matched_icp_id"),
            confidence=confidence,
            recommended_action=signal.get("recommended_action", "ask_purpose"),
            reasoning=signal.get("reasoning", ""),
        )

        try:
            # Throttle: small delay between sends
            if sent > 0:
                await asyncio.sleep(3)

            dm_result = await generate_discovery_question(
                sender_profile=sender_profile,
                signal_type="message",
                content=signal.get("content", ""),
                voice=voice,
                qualification=qual,
            )
            dm_text = dm_result.get("message", "")
            if not dm_text:
                lines.append(f"- {sender_name}: skipped (no DM generated)")
                skipped += 1
                continue

            # Find chat and send
            chat_id = await client.find_chat_for_user(account_id, sender_id)
            if chat_id:
                result = await client.send_message(
                    account_id=account_id, chat_id=chat_id, text=dm_text,
                )
            else:
                result = await client.send_new_message(
                    account_id=account_id, provider_id=sender_id, text=dm_text,
                )

            if result.get("success"):
                campaign_id = signal.get("campaign_id", "")
                now = int(time.time())
                try:
                    contact_id = await run_db(
                        save_contact,
                        campaign_id=campaign_id,
                        name=sender_name,
                        title=signal.get("sender_headline", ""),
                        company=signal.get("sender_company", ""),
                        linkedin_url=signal.get("sender_url", ""),
                        linkedin_id=sender_id,
                        source="inbound_dm",
                    )
                    outreach_id = await run_db(
                        create_outreach,
                        campaign_id=campaign_id,
                        contact_id=contact_id,
                        status="connected",
                        signal_id=signal["id"],
                    )
                    await run_db(update_outreach, outreach_id, accepted_at=now)
                    await run_db(save_message, outreach_id, role="sdr", text=dm_text)
                    await run_db(
                        save_message, outreach_id, role="prospect",
                        text=signal.get("content", ""),
                    )
                    await run_db(
                        update_inbound_signal, signal["id"],
                        status="engaged",
                        outreach_id=outreach_id,
                        actioned_at=now,
                    )
                except Exception as e:
                    logger.warning("Failed to create contact/outreach for %s: %s", sender_name, e)

                await run_db(
                    log_action,
                    "inbound_discovery_dm_sent",
                    result="success",
                    details={
                        "signal_id": signal["id"],
                        "sender_name": sender_name,
                        "intent": intent,
                        "confidence": confidence,
                        "dm_text": dm_text[:200],
                        "backfill": True,
                    },
                )
                lines.append(f"- **{sender_name}**: sent DM")
                sent += 1
            else:
                error_msg = result.get("error", "unknown error")
                if "rate limit" in error_msg.lower():
                    lines.append(f"- {sender_name}: rate limited — stopping")
                    errors += 1
                    break  # Stop on rate limit
                lines.append(f"- {sender_name}: failed ({error_msg})")
                errors += 1

        except Exception as e:
            logger.warning("DM failed for %s: %s", sender_name, e)
            lines.append(f"- {sender_name}: error ({e})")
            errors += 1

    lines.append(f"\n**Result**: {sent} sent, {skipped} skipped, {errors} failed")
    return "\n".join(lines)


async def _backfill(
    client: Any,
    account_id: str,
    limit: int,
    dry_run: bool,
    min_confidence: float,
) -> str:
    """Core backfill logic."""
    # ── 1. Fetch all inbox conversations ─────────────────────────────────
    raw_chats = await _fetch_raw_chats(client, account_id, limit)
    if not raw_chats:
        return "No conversations found in inbox."

    chat_infos = [_extract_chat_info(c) for c in raw_chats]
    logger.info("Fetched %d conversations from inbox", len(chat_infos))

    # ── 2. Find unreplied conversations ──────────────────────────────────
    unreplied: list[dict[str, Any]] = []
    BATCH = 10

    for start in range(0, len(chat_infos), BATCH):
        batch = chat_infos[start : start + BATCH]
        tasks = [
            client.get_chat_messages(account_id, ci["chat_id"], limit=20)
            for ci in batch
            if ci["chat_id"]
        ]
        msg_results = await asyncio.gather(*tasks, return_exceptions=True)

        task_idx = 0
        for ci in batch:
            if not ci["chat_id"]:
                continue
            msgs = msg_results[task_idx]
            task_idx += 1

            if isinstance(msgs, Exception) or not msgs:
                continue

            # Check if we (account owner) ever replied in this chat
            our_messages = [
                m for m in msgs
                if m.get("sender_id") == account_id
            ]
            if our_messages:
                continue  # We already replied — skip

            # Collect the prospect's messages
            prospect_msgs = [
                m for m in msgs
                if m.get("sender_id") and m.get("sender_id") != account_id
            ]
            if not prospect_msgs:
                continue

            # Get the most recent prospect message
            prospect_msgs.sort(key=lambda m: m.get("timestamp", 0), reverse=True)
            latest = prospect_msgs[0]
            text = (latest.get("text") or "").strip()
            if not text:
                continue

            sender_id = latest.get("sender_id", "")
            if not sender_id:
                continue

            # Skip if already a campaign contact
            existing_contact = await run_db(get_contact_by_linkedin_id, sender_id)
            if existing_contact:
                continue

            unreplied.append({
                "chat_id": ci["chat_id"],
                "sender_id": sender_id,
                "sender_name": ci.get("contact_name") or latest.get("sender_name", ""),
                "sender_headline": ci.get("contact_headline", ""),
                "provider_id": ci.get("contact_provider_id") or sender_id,
                "text": text,
                "all_texts": "\n".join(
                    (m.get("text") or "") for m in prospect_msgs if m.get("text")
                ),
                "message_id": latest.get("message_id", ""),
                "timestamp": latest.get("timestamp", 0),
            })

    if not unreplied:
        return "All inbox conversations have replies. Nothing to backfill."

    # Resolve missing names/headlines
    needs_profile = [u for u in unreplied if not u["sender_name"]]
    for start in range(0, len(needs_profile), BATCH):
        batch = needs_profile[start : start + BATCH]
        tasks = [
            _resolve_profile(client, account_id, u["provider_id"])
            for u in batch
        ]
        profiles = await asyncio.gather(*tasks, return_exceptions=True)
        for u, profile in zip(batch, profiles):
            if isinstance(profile, dict):
                u["sender_name"] = profile.get("name", "")
                u["sender_headline"] = profile.get("headline", "")

    logger.info("Found %d unreplied conversations", len(unreplied))

    # ── 3. Classify each unreplied message ───────────────────────────────
    active_icps = await run_db(list_icps, status="active")
    classified: list[dict[str, Any]] = []

    for u in unreplied:
        sender_id = u["sender_id"]

        # Check if signal already exists
        existing = await run_db(get_inbound_signal_by_sender, sender_id, "message")
        if existing:
            status = existing.get("status", "")
            if status in ("engaged",):
                continue  # Already handled
            if status in ("new", "classified", "accepted"):
                # Use existing signal
                classified.append({**u, "signal": existing})
                continue

        # Save new signal
        signal_id = await run_db(
            save_inbound_signal,
            signal_type="message",
            sender_name=u["sender_name"],
            sender_id=sender_id,
            sender_headline=u.get("sender_headline", ""),
            content=u["text"],
            message_id=u.get("message_id", ""),
        )

        # Classify
        profile = {
            "name": u["sender_name"],
            "headline": u.get("sender_headline", ""),
            "company": "",
            "title": u.get("sender_headline", ""),
        }
        try:
            qual = await qualify_inbound(
                profile=profile,
                content=u["all_texts"] or u["text"],
                signal_type="message",
                active_icps=active_icps,
            )
        except Exception as e:
            logger.warning("Classify failed for %s: %s", u["sender_name"], e)
            qual = InboundQualification(
                intent="unknown",
                matched_icp_id=None,
                confidence=0.3,
                recommended_action="ask_purpose",
                reasoning=f"Classification error: {e}",
            )

        # Resolve campaign
        try:
            from ..services.inbound_pipeline import _resolve_campaign_for_signal

            sig_dict = {
                "content": u["text"],
                "sender_headline": u.get("sender_headline", ""),
                "sender_company": "",
            }
            campaign_id = _resolve_campaign_for_signal(sig_dict)
        except Exception:
            campaign_id = ""

        try:
            await run_db(
                update_inbound_signal,
                signal_id,
                intent=qual.intent,
                matched_icp_id=qual.matched_icp_id or "",
                confidence=qual.confidence,
                recommended_action=qual.recommended_action,
                reasoning=qual.reasoning,
                status="classified",
                qualified_at=int(time.time()),
                **({"campaign_id": campaign_id} if campaign_id else {}),
            )
        except Exception as e:
            # FK constraint or other DB error — retry without campaign_id
            logger.debug("Update with campaign_id failed: %s, retrying without", e)
            await run_db(
                update_inbound_signal,
                signal_id,
                intent=qual.intent,
                confidence=qual.confidence,
                recommended_action=qual.recommended_action,
                reasoning=qual.reasoning,
                status="classified",
                qualified_at=int(time.time()),
            )

        signal = {
            "id": signal_id,
            "signal_type": "message",
            "sender_name": u["sender_name"],
            "sender_id": sender_id,
            "sender_headline": u.get("sender_headline", ""),
            "sender_company": "",
            "sender_url": "",
            "content": u["text"],
            "message_id": u.get("message_id", ""),
            "intent": qual.intent,
            "confidence": qual.confidence,
            "matched_icp_id": qual.matched_icp_id,
            "recommended_action": qual.recommended_action,
            "reasoning": qual.reasoning,
            "campaign_id": campaign_id,
        }
        classified.append({**u, "signal": signal, "qual": qual})

    if not classified:
        return "No unreplied messages to process after dedup."

    # ── 4. Build summary of classified signals ───────────────────────────
    lines = [f"**Backfill: {len(classified)} unreplied conversations found**\n"]

    for i, item in enumerate(classified, 1):
        sig = item.get("signal", {})
        name = item.get("sender_name") or "Unknown"
        headline = item.get("sender_headline", "")
        intent = sig.get("intent", "?")
        conf = sig.get("confidence", 0) or 0
        text_preview = (item.get("text") or "")[:80].replace("\n", " ")

        line = f"{i}. **{name}**"
        if headline:
            line += f" — {headline[:50]}"
        line += f"\n   Intent: {intent} ({conf:.0%})"
        line += f"\n   Message: {text_preview}"
        if len(item.get("text", "")) > 80:
            line += "..."
        lines.append(line)

    if dry_run:
        lines.append(
            f"\n**Dry run** — no DMs sent. "
            f"Run with `dry_run=False` to send discovery DMs."
        )
        return "\n".join(lines)

    # ── 5. Send discovery DMs ────────────────────────────────────────────
    today_sent = await run_db(count_inbound_dms_today)
    remaining = max(0, DAILY_INBOUND_DM_LIMIT - today_sent)
    if remaining == 0:
        lines.append(
            f"\n**Daily DM limit reached** ({today_sent}/{DAILY_INBOUND_DM_LIMIT}). "
            f"Run again tomorrow."
        )
        return "\n".join(lines)

    voice = await run_db(get_setting, "voice_signature", {})
    sent = 0
    skipped = 0
    errors = 0

    lines.append(f"\n**Sending discovery DMs** (limit: {remaining} remaining today)\n")

    for item in classified[:remaining]:
        sig = item.get("signal", {})
        intent = sig.get("intent", "unknown")
        confidence = sig.get("confidence", 0) or 0

        # Skip spam/vendor/job_seeking
        if intent in ("spam", "job_seeking", "vendor_pitch"):
            await run_db(update_inbound_signal, sig["id"], status="dismissed")
            lines.append(f"- {item.get('sender_name')}: skipped ({intent})")
            skipped += 1
            continue

        # Skip below min confidence
        if confidence < min_confidence:
            lines.append(
                f"- {item.get('sender_name')}: skipped (confidence {confidence:.0%} < {min_confidence:.0%})"
            )
            skipped += 1
            continue

        sender_id = item["sender_id"]
        sender_profile = {
            "name": item.get("sender_name", ""),
            "headline": item.get("sender_headline", ""),
            "company": "",
        }

        qual = InboundQualification(
            intent=intent,
            matched_icp_id=sig.get("matched_icp_id"),
            confidence=confidence,
            recommended_action=sig.get("recommended_action", "ask_purpose"),
            reasoning=sig.get("reasoning", ""),
        )

        try:
            dm_result = await generate_discovery_question(
                sender_profile=sender_profile,
                signal_type="message",
                content=item.get("all_texts") or item.get("text", ""),
                voice=voice,
                qualification=qual,
            )
            dm_text = dm_result.get("message", "")
            if not dm_text:
                lines.append(f"- {item.get('sender_name')}: skipped (no DM generated)")
                skipped += 1
                continue

            # Send to existing chat
            chat_id = item.get("chat_id", "")
            if chat_id:
                result = await client.send_message(
                    account_id=account_id, chat_id=chat_id, text=dm_text,
                )
            else:
                result = await client.send_new_message(
                    account_id=account_id, provider_id=sender_id, text=dm_text,
                )

            if result.get("success"):
                # Create contact + outreach for tracking
                campaign_id = sig.get("campaign_id", "")
                now = int(time.time())
                try:
                    contact_id = await run_db(
                        save_contact,
                        campaign_id=campaign_id,
                        name=item.get("sender_name", ""),
                        title=item.get("sender_headline", ""),
                        company="",
                        linkedin_url="",
                        linkedin_id=sender_id,
                        source="inbound_dm",
                    )
                    outreach_id = await run_db(
                        create_outreach,
                        campaign_id=campaign_id,
                        contact_id=contact_id,
                        status="connected",
                        signal_id=sig["id"],
                    )
                    await run_db(update_outreach, outreach_id, accepted_at=now)
                    await run_db(save_message, outreach_id, role="sdr", text=dm_text)
                    # Also save their original message
                    await run_db(
                        save_message, outreach_id, role="prospect",
                        text=item.get("text", ""),
                    )
                    await run_db(
                        update_inbound_signal, sig["id"],
                        status="engaged",
                        outreach_id=outreach_id,
                        actioned_at=now,
                    )
                except Exception as e:
                    logger.warning(
                        "Failed to create contact/outreach for %s: %s",
                        item.get("sender_name"), e,
                    )

                await run_db(
                    log_action,
                    "inbound_discovery_dm_sent",
                    result="success",
                    details={
                        "signal_id": sig["id"],
                        "sender_name": item.get("sender_name", ""),
                        "intent": intent,
                        "confidence": confidence,
                        "dm_text": dm_text[:200],
                        "backfill": True,
                    },
                )
                lines.append(f"- **{item.get('sender_name')}**: sent DM")
                sent += 1
            else:
                error_msg = result.get("error", "unknown error")
                lines.append(f"- {item.get('sender_name')}: failed ({error_msg})")
                errors += 1

        except Exception as e:
            logger.warning("DM failed for %s: %s", item.get("sender_name"), e)
            lines.append(f"- {item.get('sender_name')}: error ({e})")
            errors += 1

    lines.append(f"\n**Result**: {sent} sent, {skipped} skipped, {errors} failed")
    return "\n".join(lines)
