"""SQLite CRUD helpers for HeyLead."""

from __future__ import annotations

import json
import time
import uuid
from typing import Any, Optional

from .schema import get_db


# ──────────────────────────────────────────────
# Settings (key-value store)
# ──────────────────────────────────────────────

def save_setting(key: str, value: Any) -> None:
    """Save a setting (JSON-serialized)."""
    db = get_db()
    db.execute(
        "INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)",
        (key, json.dumps(value), int(time.time())),
    )
    db.commit()
    db.close()


def get_setting(key: str, default: Any = None) -> Any:
    """Load a setting, returning default if not found."""
    db = get_db()
    row = db.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
    db.close()
    if row is None:
        return default
    try:
        return json.loads(row["value"])
    except (json.JSONDecodeError, TypeError):
        return row["value"]


def delete_setting(key: str) -> None:
    db = get_db()
    db.execute("DELETE FROM settings WHERE key = ?", (key,))
    db.commit()
    db.close()


# ──────────────────────────────────────────────
# Campaigns
# ──────────────────────────────────────────────

def create_campaign(
    name: str,
    icp_json: str = "",
    status: str = "draft",
    mode: str = "copilot",
    config_json: str = "",
    context_json: str = "",
) -> str:
    """Create a new campaign and return its ID."""
    campaign_id = str(uuid.uuid4())
    now = int(time.time())
    db = get_db()
    db.execute(
        """INSERT INTO campaigns (id, name, icp_json, status, mode, config_json, context_json, created_at, updated_at)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        (campaign_id, name, icp_json, status, mode, config_json, context_json, now, now),
    )
    db.commit()
    db.close()
    return campaign_id


def get_campaign(campaign_id: str) -> Optional[dict]:
    db = get_db()
    row = db.execute("SELECT * FROM campaigns WHERE id = ?", (campaign_id,)).fetchone()
    db.close()
    return dict(row) if row else None


def get_campaign_context(campaign_id: str) -> dict:
    """Get parsed campaign context (offerings, case_studies, social_proofs, preferences).

    Returns empty dict if campaign not found or context_json is null.
    """
    campaign = get_campaign(campaign_id)
    if not campaign:
        return {}
    context_raw = campaign.get("context_json", "")
    if not context_raw:
        return {}
    try:
        return json.loads(context_raw)
    except (json.JSONDecodeError, TypeError):
        return {}


def list_campaigns(status: Optional[str] = None) -> list[dict]:
    db = get_db()
    if status:
        rows = db.execute(
            "SELECT * FROM campaigns WHERE status = ? ORDER BY created_at DESC", (status,)
        ).fetchall()
    else:
        rows = db.execute("SELECT * FROM campaigns ORDER BY created_at DESC").fetchall()
    db.close()
    return [dict(r) for r in rows]


_VALID_CAMPAIGN_COLS = frozenset({
    "name", "icp_json", "status", "mode", "config_json", "context_json", "updated_at",
})


def update_campaign(campaign_id: str, **kwargs: Any) -> None:
    db = get_db()
    kwargs["updated_at"] = int(time.time())
    bad_keys = set(kwargs) - _VALID_CAMPAIGN_COLS
    if bad_keys:
        raise ValueError(f"Invalid campaign columns: {bad_keys}")
    set_clause = ", ".join(f"{k} = ?" for k in kwargs)
    values = list(kwargs.values()) + [campaign_id]
    db.execute(f"UPDATE campaigns SET {set_clause} WHERE id = ?", values)
    db.commit()
    db.close()


def find_active_campaign(campaign_id: str = "") -> tuple[Optional[dict], str]:
    """Find a campaign by ID or return first active. Returns (campaign, error_msg)."""
    if campaign_id:
        campaign = get_campaign(campaign_id)
        if not campaign:
            return None, f"Campaign not found: {campaign_id}"
        return campaign, ""
    campaigns = list_campaigns(status="active")
    if not campaigns:
        return None, (
            "No active campaigns.\n\n"
            "Create one first: create_campaign(\"your target description\")"
        )
    return campaigns[0], ""


# ──────────────────────────────────────────────
# Contacts
# ──────────────────────────────────────────────

def save_contact(
    campaign_id: str,
    name: str,
    title: str = "",
    company: str = "",
    linkedin_url: str = "",
    linkedin_id: str = "",
    profile_json: str = "",
    fit_score: float = 0.0,
) -> str:
    contact_id = str(uuid.uuid4())
    db = get_db()
    db.execute(
        """INSERT INTO contacts
           (id, campaign_id, name, title, company, linkedin_url, linkedin_id, profile_json, fit_score, created_at)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        (contact_id, campaign_id, name, title, company, linkedin_url, linkedin_id, profile_json, fit_score, int(time.time())),
    )
    db.commit()
    db.close()
    return contact_id


def get_contacts_for_campaign(campaign_id: str, status: Optional[str] = None) -> list[dict]:
    db = get_db()
    if status:
        rows = db.execute(
            "SELECT * FROM contacts WHERE campaign_id = ? AND status = ?",
            (campaign_id, status),
        ).fetchall()
    else:
        rows = db.execute(
            "SELECT * FROM contacts WHERE campaign_id = ?", (campaign_id,)
        ).fetchall()
    db.close()
    return [dict(r) for r in rows]


_VALID_CONTACT_COLS = frozenset({
    "name", "title", "company", "fit_score", "status", "updated_at",
    "analysis_json",
})


def update_contact(contact_id: str, **kwargs: Any) -> None:
    """Update contact fields (name, title, company, fit_score, status)."""
    db = get_db()
    kwargs["updated_at"] = int(time.time())
    bad_keys = set(kwargs) - _VALID_CONTACT_COLS
    if bad_keys:
        raise ValueError(f"Invalid contact columns: {bad_keys}")
    set_clause = ", ".join(f"{k} = ?" for k in kwargs)
    values = list(kwargs.values()) + [contact_id]
    db.execute(f"UPDATE contacts SET {set_clause} WHERE id = ?", values)
    db.commit()
    db.close()


def save_contact_analysis(contact_id: str, analysis: dict[str, Any]) -> None:
    """Write prospect analysis to contacts.analysis_json."""
    db = get_db()
    db.execute(
        "UPDATE contacts SET analysis_json = ?, updated_at = ? WHERE id = ?",
        (json.dumps(analysis), int(time.time()), contact_id),
    )
    db.commit()
    db.close()


def get_contact_analysis(contact_id: str) -> dict[str, Any] | None:
    """Load cached prospect analysis from contacts.analysis_json.

    Returns parsed dict or None if not cached.
    """
    db = get_db()
    row = db.execute(
        "SELECT analysis_json FROM contacts WHERE id = ?",
        (contact_id,),
    ).fetchone()
    db.close()
    if not row or not row["analysis_json"]:
        return None
    try:
        return json.loads(row["analysis_json"])
    except (json.JSONDecodeError, TypeError):
        return None


# ──────────────────────────────────────────────
# Outreaches
# ──────────────────────────────────────────────

def create_outreach(campaign_id: str, contact_id: str, status: str = "pending") -> str:
    outreach_id = str(uuid.uuid4())
    now = int(time.time())
    db = get_db()
    db.execute(
        """INSERT INTO outreaches (id, campaign_id, contact_id, status, created_at, updated_at)
           VALUES (?, ?, ?, ?, ?, ?)""",
        (outreach_id, campaign_id, contact_id, status, now, now),
    )
    db.commit()
    db.close()
    return outreach_id


_VALID_OUTREACH_COLS = frozenset({
    "status", "next_action", "scheduled_at", "followup_count", "updated_at",
    "outcome_json", "memory_json",
})


def update_outreach(outreach_id: str, **kwargs: Any) -> None:
    db = get_db()
    kwargs["updated_at"] = int(time.time())
    bad_keys = set(kwargs) - _VALID_OUTREACH_COLS
    if bad_keys:
        raise ValueError(f"Invalid outreach columns: {bad_keys}")
    set_clause = ", ".join(f"{k} = ?" for k in kwargs)
    values = list(kwargs.values()) + [outreach_id]
    db.execute(f"UPDATE outreaches SET {set_clause} WHERE id = ?", values)
    db.commit()
    db.close()


def get_outreach(outreach_id: str) -> Optional[dict]:
    """Get a single outreach by ID."""
    db = get_db()
    row = db.execute("SELECT * FROM outreaches WHERE id = ?", (outreach_id,)).fetchone()
    db.close()
    return dict(row) if row else None


def get_outreach_memory(outreach_id: str) -> list[dict]:
    """Get structured conversation memory for an outreach.

    Returns a list of per-followup decision records (cta_used, pain_used,
    greeting, structure, etc.) used for anti-repetition in future follow-ups.
    """
    outreach = get_outreach(outreach_id)
    if not outreach:
        return []
    raw = outreach.get("memory_json", "")
    if not raw:
        return []
    try:
        memory = json.loads(raw)
        return memory if isinstance(memory, list) else []
    except (json.JSONDecodeError, TypeError):
        return []


def save_outreach_memory(outreach_id: str, memory_entry: dict) -> None:
    """Append a follow-up memory entry to the outreach's memory_json.

    Each entry captures what was used in a specific follow-up
    (CTA, pain angle, greeting, structure, etc.) so future
    follow-ups can avoid repeating the same patterns.
    """
    existing = get_outreach_memory(outreach_id)
    existing.append(memory_entry)
    update_outreach(outreach_id, memory_json=json.dumps(existing))


def get_outreach_with_contact(outreach_id: str) -> Optional[dict]:
    """Get outreach data merged with contact info for tool display."""
    db = get_db()
    row = db.execute(
        """SELECT o.id as outreach_id, o.campaign_id, o.contact_id,
                  o.status, o.followup_count, o.next_action, o.updated_at,
                  c.name, c.title, c.company, c.linkedin_url, c.linkedin_id,
                  c.fit_score, c.profile_json, c.analysis_json
           FROM outreaches o
           JOIN contacts c ON o.contact_id = c.id
           WHERE o.id = ?""",
        (outreach_id,),
    ).fetchone()
    db.close()
    return dict(row) if row else None


def count_outreaches_by_status(campaign_id: str, status: str) -> int:
    """Count outreaches with a given status in a campaign."""
    db = get_db()
    row = db.execute(
        "SELECT COUNT(*) as cnt FROM outreaches WHERE campaign_id = ? AND status = ?",
        (campaign_id, status),
    ).fetchone()
    db.close()
    return row["cnt"] if row else 0


def get_followup_candidates(
    campaign_id: str,
    max_followups: int = 2,
) -> list[dict]:
    """Find outreaches ready for follow-up.

    Returns outreaches with status 'connected' and followup_count < max_followups,
    joined with contact data. Ordered by updated_at ASC (oldest first = most overdue).
    """
    db = get_db()
    rows = db.execute(
        """SELECT o.id as outreach_id, o.campaign_id, o.contact_id, o.status,
                  o.followup_count, o.updated_at as outreach_updated_at,
                  c.name, c.title, c.company, c.linkedin_url, c.linkedin_id,
                  c.profile_json, c.fit_score
           FROM outreaches o
           JOIN contacts c ON o.contact_id = c.id
           WHERE o.campaign_id = ?
             AND o.status = 'connected'
             AND o.followup_count < ?
           ORDER BY o.updated_at ASC""",
        (campaign_id, max_followups),
    ).fetchall()
    db.close()
    return [dict(r) for r in rows]


def count_followup_ready(campaign_id: str, max_followups: int = 2) -> int:
    """Count outreaches ready for follow-up in a campaign."""
    db = get_db()
    row = db.execute(
        """SELECT COUNT(*) as c FROM outreaches
           WHERE campaign_id = ? AND status = 'connected' AND followup_count < ?""",
        (campaign_id, max_followups),
    ).fetchone()
    db.close()
    return row["c"] if row else 0


def get_reply_candidates(campaign_id: str) -> list[dict]:
    """Find outreaches needing a reply, prioritized by sentiment urgency.

    Returns outreaches with status 'hot_lead' or 'replied' where the last
    message is from the prospect (role='prospect'). Ordered by sentiment
    priority: positive > question > neutral > negative.

    Excludes outreaches where the last message is from us (we already replied).
    """
    db = get_db()
    rows = db.execute(
        """SELECT o.id as outreach_id, o.campaign_id, o.contact_id,
                  o.status, o.followup_count, o.updated_at as outreach_updated_at,
                  c.name, c.title, c.company, c.linkedin_url, c.linkedin_id,
                  c.profile_json, c.fit_score,
                  c.id as contact_db_id,
                  m.text as last_reply_text,
                  m.sentiment as last_sentiment
           FROM outreaches o
           JOIN contacts c ON o.contact_id = c.id
           JOIN messages m ON m.outreach_id = o.id
           WHERE o.campaign_id = ?
             AND o.status IN ('hot_lead', 'replied')
             AND m.role = 'prospect'
             AND m.id = (
                 SELECT m2.id FROM messages m2
                 WHERE m2.outreach_id = o.id
                 ORDER BY m2.timestamp DESC LIMIT 1
             )
           ORDER BY
             CASE m.sentiment
               WHEN 'positive' THEN 1
               WHEN 'question' THEN 2
               WHEN 'neutral' THEN 3
               WHEN 'negative' THEN 4
               ELSE 5
             END,
             o.updated_at ASC""",
        (campaign_id,),
    ).fetchall()
    db.close()
    return [dict(r) for r in rows]


def get_pending_approval() -> Optional[dict]:
    """Find the most recently updated outreach with a pending approval.

    Returns the outreach with a non-null next_action field, ordered by
    updated_at DESC (most recent first).
    """
    db = get_db()
    row = db.execute(
        """SELECT * FROM outreaches
           WHERE next_action IS NOT NULL AND next_action != ''
           ORDER BY updated_at DESC LIMIT 1"""
    ).fetchone()
    db.close()
    return dict(row) if row else None


# ──────────────────────────────────────────────
# Messages
# ──────────────────────────────────────────────

def save_message(
    outreach_id: str,
    role: str,
    text: str,
    sentiment: str = "",
) -> str:
    msg_id = str(uuid.uuid4())
    db = get_db()
    db.execute(
        """INSERT INTO messages (id, outreach_id, role, text, sentiment, timestamp)
           VALUES (?, ?, ?, ?, ?, ?)""",
        (msg_id, outreach_id, role, text, sentiment, int(time.time())),
    )
    db.commit()
    db.close()
    return msg_id


def get_messages_for_outreach(outreach_id: str) -> list[dict]:
    db = get_db()
    rows = db.execute(
        "SELECT * FROM messages WHERE outreach_id = ? ORDER BY timestamp ASC",
        (outreach_id,),
    ).fetchall()
    db.close()
    return [dict(r) for r in rows]


# ──────────────────────────────────────────────
# Actions Log
# ──────────────────────────────────────────────

def log_action(
    action_type: str,
    outreach_id: str = "",
    result: str = "",
    details: Any = None,
) -> str:
    action_id = str(uuid.uuid4())
    db = get_db()
    db.execute(
        """INSERT INTO actions_log (id, outreach_id, action_type, result, details_json, timestamp)
           VALUES (?, ?, ?, ?, ?, ?)""",
        (action_id, outreach_id or None, action_type, result,
         json.dumps(details) if details else None, int(time.time())),
    )
    db.commit()
    db.close()
    return action_id


# ──────────────────────────────────────────────
# Rate Limits
# ──────────────────────────────────────────────

def get_rate_limit_today() -> dict:
    """Get or create today's rate limit record."""
    from datetime import date
    today = date.today().isoformat()
    db = get_db()
    row = db.execute("SELECT * FROM rate_limits WHERE date = ?", (today,)).fetchone()
    if row is None:
        rid = str(uuid.uuid4())
        db.execute(
            """INSERT INTO rate_limits (id, date, sent, accepted, daily_limit, blocked, updated_at)
               VALUES (?, ?, 0, 0, 15, 0, ?)""",
            (rid, today, int(time.time())),
        )
        db.commit()
        row = db.execute("SELECT * FROM rate_limits WHERE date = ?", (today,)).fetchone()
    db.close()
    return dict(row)


def increment_sent() -> None:
    from datetime import date
    today = date.today().isoformat()
    db = get_db()
    db.execute(
        "UPDATE rate_limits SET sent = sent + 1, updated_at = ? WHERE date = ?",
        (int(time.time()), today),
    )
    db.commit()
    db.close()


def increment_accepted() -> None:
    from datetime import date
    today = date.today().isoformat()
    db = get_db()
    db.execute(
        "UPDATE rate_limits SET accepted = accepted + 1, updated_at = ? WHERE date = ?",
        (int(time.time()), today),
    )
    db.commit()
    db.close()


def update_daily_limit(new_limit: int) -> None:
    from datetime import date
    today = date.today().isoformat()
    db = get_db()
    db.execute(
        "UPDATE rate_limits SET daily_limit = ?, updated_at = ? WHERE date = ?",
        (new_limit, int(time.time()), today),
    )
    db.commit()
    db.close()


def get_weekly_invitation_sum() -> int:
    """Sum invitations sent over the last 7 days from rate_limits table."""
    from datetime import date, timedelta
    today = date.today()
    week_ago = (today - timedelta(days=6)).isoformat()
    db = get_db()
    row = db.execute(
        "SELECT COALESCE(SUM(sent), 0) as total FROM rate_limits WHERE date >= ?",
        (week_ago,),
    ).fetchone()
    db.close()
    return row["total"] if row else 0


# ──────────────────────────────────────────────
# Usage Tracking (Free Tier)
# ──────────────────────────────────────────────

def get_monthly_usage() -> dict:
    """Get or create this month's usage record."""
    from datetime import date
    month = date.today().strftime("%Y-%m")
    db = get_db()
    row = db.execute("SELECT * FROM usage WHERE month = ?", (month,)).fetchone()
    if row is None:
        db.execute(
            "INSERT INTO usage (month, updated_at) VALUES (?, ?)",
            (month, int(time.time())),
        )
        db.commit()
        row = db.execute("SELECT * FROM usage WHERE month = ?", (month,)).fetchone()
    db.close()
    return dict(row)


_VALID_USAGE_FIELDS = frozenset({
    "invitations_sent",
    "messages_sent",
    "campaigns_created",
    "icps_generated",
    "engagements_sent",
})


def increment_usage(field: str) -> None:
    """Increment a usage counter (invitations_sent, messages_sent, etc.)."""
    if field not in _VALID_USAGE_FIELDS:
        raise ValueError(f"Invalid usage field: {field}. Must be one of {_VALID_USAGE_FIELDS}")
    from datetime import date
    month = date.today().strftime("%Y-%m")
    db = get_db()
    # Ensure row exists
    get_monthly_usage()
    db.execute(
        f"UPDATE usage SET {field} = {field} + 1, updated_at = ? WHERE month = ?",
        (int(time.time()), month),
    )
    db.commit()
    db.close()


# ──────────────────────────────────────────────
# Campaign Stats (for show_status)
# ──────────────────────────────────────────────

def get_campaign_stats(campaign_id: str) -> dict:
    """Calculate aggregate stats for a campaign.

    Rates:
    - acceptance_rate: connected / invited (cumulative, includes pending invitations)
    - mature_acceptance_rate: connected / mature_invited (only invitations > 7 days old)
    - reply_rate: replied / connected
    """
    import time as _time
    db = get_db()

    total = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ?", (campaign_id,)
    ).fetchone()["c"]

    invited = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status NOT IN ('pending', 'skipped')",
        (campaign_id,),
    ).fetchone()["c"]

    connected = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status IN ('connected', 'messaged', 'replied', 'hot_lead', 'closed_happy', 'closed_unhappy')",
        (campaign_id,),
    ).fetchone()["c"]

    replied = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status IN ('replied', 'hot_lead', 'closed_happy', 'closed_unhappy')",
        (campaign_id,),
    ).fetchone()["c"]

    hot_leads = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'hot_lead'",
        (campaign_id,),
    ).fetchone()["c"]

    pending_invitations = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'invited'",
        (campaign_id,),
    ).fetchone()["c"]

    skipped = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'skipped'",
        (campaign_id,),
    ).fetchone()["c"]

    closed_happy = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'closed_happy'",
        (campaign_id,),
    ).fetchone()["c"]

    closed_unhappy = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'closed_unhappy'",
        (campaign_id,),
    ).fetchone()["c"]

    opted_out = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'opted_out'",
        (campaign_id,),
    ).fetchone()["c"]

    # Mature invitations: sent more than 7 days ago (had time to respond)
    seven_days_ago = int(_time.time()) - (7 * 86400)
    mature_invited = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status NOT IN ('pending', 'skipped') AND updated_at < ?",
        (campaign_id, seven_days_ago),
    ).fetchone()["c"]

    db.close()

    acceptance_rate = connected / invited if invited > 0 else 0.0
    mature_acceptance_rate = connected / mature_invited if mature_invited > 0 else 0.0
    reply_rate = replied / connected if connected > 0 else 0.0

    return {
        "total_prospects": total,
        "invited": invited,
        "connected": connected,
        "replied": replied,
        "hot_leads": hot_leads,
        "pending_invitations": pending_invitations,
        "skipped": skipped,
        "closed_happy": closed_happy,
        "closed_unhappy": closed_unhappy,
        "opted_out": opted_out,
        "acceptance_rate": acceptance_rate,
        "mature_acceptance_rate": mature_acceptance_rate,
        "reply_rate": reply_rate,
    }


def get_campaign_outcomes(campaign_id: str) -> dict:
    """Get outcome breakdown for a campaign.

    Returns dict with closed_happy, closed_unhappy, opted_out counts,
    total_closed, conversion_rate, and individual outcome details.
    """
    db = get_db()

    closed_happy = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'closed_happy'",
        (campaign_id,),
    ).fetchone()["c"]

    closed_unhappy = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'closed_unhappy'",
        (campaign_id,),
    ).fetchone()["c"]

    opted_out = db.execute(
        "SELECT COUNT(*) as c FROM outreaches WHERE campaign_id = ? AND status = 'opted_out'",
        (campaign_id,),
    ).fetchone()["c"]

    # Individual outcome details with contact info
    rows = db.execute(
        """SELECT o.id as outreach_id, o.status, o.outcome_json, o.updated_at,
                  c.name, c.title, c.company, c.fit_score
           FROM outreaches o
           JOIN contacts c ON o.contact_id = c.id
           WHERE o.campaign_id = ?
             AND o.status IN ('closed_happy', 'closed_unhappy', 'opted_out')
           ORDER BY o.updated_at DESC""",
        (campaign_id,),
    ).fetchall()
    db.close()

    outcomes = []
    for row in rows:
        r = dict(row)
        outcome_data = {}
        if r.get("outcome_json"):
            try:
                outcome_data = json.loads(r["outcome_json"])
            except (json.JSONDecodeError, TypeError):
                pass
        outcomes.append({
            "outreach_id": r["outreach_id"],
            "status": r["status"],
            "name": r.get("name", "Unknown"),
            "title": r.get("title", ""),
            "company": r.get("company", ""),
            "fit_score": r.get("fit_score", 0),
            "reason": outcome_data.get("reason", ""),
            "booking_link": outcome_data.get("booking_link", ""),
            "closed_at": outcome_data.get("closed_at", r.get("updated_at", 0)),
        })

    total_closed = closed_happy + closed_unhappy
    conversion_rate = closed_happy / total_closed if total_closed > 0 else 0.0

    return {
        "closed_happy": closed_happy,
        "closed_unhappy": closed_unhappy,
        "opted_out": opted_out,
        "total_closed": total_closed,
        "conversion_rate": conversion_rate,
        "outcomes": outcomes,
    }


def get_stale_outreaches(campaign_id: str, stale_days: int = 14) -> list[dict]:
    """Find outreaches with no activity for N days.

    Returns outreaches in active states (connected, messaged, hot_lead)
    whose updated_at is older than stale_days ago, joined with contact info.
    """
    import time as _time
    cutoff = int(_time.time()) - (stale_days * 86400)
    now = int(_time.time())

    db = get_db()
    rows = db.execute(
        """SELECT o.id as outreach_id, o.status, o.updated_at,
                  c.name, c.title, c.company, c.fit_score
           FROM outreaches o
           JOIN contacts c ON o.contact_id = c.id
           WHERE o.campaign_id = ?
             AND o.status IN ('connected', 'messaged', 'hot_lead')
             AND o.updated_at < ?
           ORDER BY o.updated_at ASC""",
        (campaign_id, cutoff),
    ).fetchall()
    db.close()

    results = []
    for row in rows:
        r = dict(row)
        days_stale = (now - (r.get("updated_at") or 0)) // 86400
        results.append({
            "outreach_id": r["outreach_id"],
            "status": r["status"],
            "name": r.get("name", "Unknown"),
            "title": r.get("title", ""),
            "company": r.get("company", ""),
            "fit_score": r.get("fit_score", 0),
            "days_stale": days_stale,
            "updated_at": r.get("updated_at", 0),
        })

    return results


# ──────────────────────────────────────────────
# ICPs (Ideal Customer Profiles)
# ──────────────────────────────────────────────

def save_icp(
    name: str,
    icp_json: str,
    target_desc: str = "",
    source_url: str = "",
    confidence: float = 0.5,
) -> str:
    """Create a new ICP and return its ID."""
    icp_id = str(uuid.uuid4())
    now = int(time.time())
    db = get_db()
    db.execute(
        """INSERT INTO icps (id, name, icp_json, target_desc, source_url, status, confidence, created_at, updated_at)
           VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?)""",
        (icp_id, name, icp_json, target_desc, source_url, confidence, now, now),
    )
    db.commit()
    db.close()
    return icp_id


def get_icp(icp_id: str) -> Optional[dict]:
    """Load an ICP by ID."""
    db = get_db()
    row = db.execute("SELECT * FROM icps WHERE id = ?", (icp_id,)).fetchone()
    db.close()
    return dict(row) if row else None


def list_icps(status: Optional[str] = None) -> list[dict]:
    """List all ICPs, optionally filtered by status."""
    db = get_db()
    if status:
        rows = db.execute(
            "SELECT * FROM icps WHERE status = ? ORDER BY created_at DESC",
            (status,),
        ).fetchall()
    else:
        rows = db.execute(
            "SELECT * FROM icps ORDER BY created_at DESC",
        ).fetchall()
    db.close()
    return [dict(r) for r in rows]


_VALID_ICP_COLS = frozenset({
    "name", "icp_json", "target_desc", "source_url", "status",
    "confidence", "updated_at",
})


def update_icp(icp_id: str, **kwargs: Any) -> None:
    """Update an ICP's fields."""
    db = get_db()
    kwargs["updated_at"] = int(time.time())
    bad_keys = set(kwargs) - _VALID_ICP_COLS
    if bad_keys:
        raise ValueError(f"Invalid ICP columns: {bad_keys}")
    set_clause = ", ".join(f"{k} = ?" for k in kwargs)
    values = list(kwargs.values()) + [icp_id]
    db.execute(f"UPDATE icps SET {set_clause} WHERE id = ?", values)
    db.commit()
    db.close()


def delete_icp(icp_id: str) -> None:
    """Delete an ICP and its sources/chunks."""
    db = get_db()
    # Delete chunks for this ICP's sources
    db.execute(
        """DELETE FROM icp_chunks WHERE source_id IN
           (SELECT id FROM icp_sources WHERE icp_id = ?)""",
        (icp_id,),
    )
    db.execute("DELETE FROM icp_sources WHERE icp_id = ?", (icp_id,))
    db.execute("DELETE FROM icps WHERE id = ?", (icp_id,))
    db.commit()
    db.close()


# ──────────────────────────────────────────────
# Engagements
# ──────────────────────────────────────────────

def save_engagement(
    outreach_id: str,
    action_type: str,
    post_id: str,
    post_text: str = "",
    text: str = "",
    reaction_type: str = "",
    status: str = "sent",
    reasoning: str = "",
) -> str:
    """Save an engagement action (comment or reaction). Returns engagement ID."""
    engagement_id = str(uuid.uuid4())
    db = get_db()
    db.execute(
        """INSERT INTO engagements
           (id, outreach_id, action_type, post_id, post_text, text,
            reaction_type, status, reasoning, created_at)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        (engagement_id, outreach_id, action_type, post_id, post_text,
         text, reaction_type, status, reasoning, int(time.time())),
    )
    db.commit()
    db.close()
    return engagement_id


def get_engagement_stats(campaign_id: str = "") -> dict:
    """Get engagement stats, optionally filtered by campaign."""
    db = get_db()
    if campaign_id:
        comments = db.execute(
            """SELECT COUNT(*) as c FROM engagements e
               JOIN outreaches o ON e.outreach_id = o.id
               WHERE o.campaign_id = ? AND e.action_type = 'comment'""",
            (campaign_id,),
        ).fetchone()["c"]
        reactions = db.execute(
            """SELECT COUNT(*) as c FROM engagements e
               JOIN outreaches o ON e.outreach_id = o.id
               WHERE o.campaign_id = ? AND e.action_type = 'react'""",
            (campaign_id,),
        ).fetchone()["c"]
    else:
        comments = db.execute(
            "SELECT COUNT(*) as c FROM engagements WHERE action_type = 'comment'"
        ).fetchone()["c"]
        reactions = db.execute(
            "SELECT COUNT(*) as c FROM engagements WHERE action_type = 'react'"
        ).fetchone()["c"]
    db.close()
    return {"comments": comments, "reactions": reactions, "total": comments + reactions}


def get_engagement_candidates(
    campaign_id: str,
    max_per_outreach: int = 3,
) -> list[dict]:
    """Find outreaches ready for post engagement.

    Returns outreaches that haven't exceeded the engagement limit.
    Includes pending/invited/connected/messaged/replied — engaging with
    posts BEFORE connection acceptance is a warm-up tactic.
    """
    db = get_db()
    rows = db.execute(
        """SELECT o.id as outreach_id, o.campaign_id, o.contact_id, o.status,
                  o.followup_count, o.updated_at as outreach_updated_at,
                  c.name, c.title, c.company, c.linkedin_url, c.linkedin_id,
                  c.profile_json, c.fit_score,
                  (SELECT COUNT(*) FROM engagements e WHERE e.outreach_id = o.id) as engagement_count
           FROM outreaches o
           JOIN contacts c ON o.contact_id = c.id
           WHERE o.campaign_id = ?
             AND o.status IN ('pending', 'invited', 'connected', 'messaged', 'replied')
             AND (SELECT COUNT(*) FROM engagements e WHERE e.outreach_id = o.id) < ?
           ORDER BY o.updated_at ASC""",
        (campaign_id, max_per_outreach),
    ).fetchall()
    db.close()
    return [dict(r) for r in rows]


def get_daily_engagement_count() -> int:
    """Count engagements sent today."""
    import datetime
    today = datetime.date.today()
    today_start = int(time.mktime(today.timetuple()))
    db = get_db()
    row = db.execute(
        "SELECT COUNT(*) as c FROM engagements WHERE created_at >= ?",
        (today_start,),
    ).fetchone()
    db.close()
    return row["c"] if row else 0


def get_engagement_count_for_outreach(outreach_id: str) -> int:
    """Count engagements for a specific outreach."""
    db = get_db()
    row = db.execute(
        "SELECT COUNT(*) as c FROM engagements WHERE outreach_id = ?",
        (outreach_id,),
    ).fetchone()
    db.close()
    return row["c"] if row else 0


def get_engaged_post_ids(outreach_id: str) -> set[str]:
    """Return post_ids already engaged for this outreach."""
    db = get_db()
    rows = db.execute(
        "SELECT DISTINCT post_id FROM engagements WHERE outreach_id = ?",
        (outreach_id,),
    ).fetchall()
    db.close()
    return {row["post_id"] for row in rows if row["post_id"]}


def get_last_activity_timestamp(outreach_id: str) -> int:
    """Get the most recent activity timestamp for an outreach.

    Checks messages, engagements, and outreach updated_at.
    Returns Unix timestamp (0 if no activity).
    """
    db = get_db()
    # Latest message
    msg = db.execute(
        "SELECT MAX(timestamp) as t FROM messages WHERE outreach_id = ?",
        (outreach_id,),
    ).fetchone()
    # Latest engagement
    eng = db.execute(
        "SELECT MAX(created_at) as t FROM engagements WHERE outreach_id = ?",
        (outreach_id,),
    ).fetchone()
    # Outreach updated_at
    out = db.execute(
        "SELECT updated_at FROM outreaches WHERE id = ?",
        (outreach_id,),
    ).fetchone()
    db.close()

    timestamps = [
        msg["t"] if msg and msg["t"] else 0,
        eng["t"] if eng and eng["t"] else 0,
        out["updated_at"] if out else 0,
    ]
    return max(timestamps)


def get_error_outreaches(campaign_id: str = "") -> list[dict]:
    """Find outreaches with status 'error', optionally filtered by campaign."""
    db = get_db()
    if campaign_id:
        rows = db.execute(
            """SELECT o.id as outreach_id, o.campaign_id, o.contact_id,
                      o.status, o.followup_count, o.updated_at,
                      c.name, c.title, c.company, c.linkedin_url, c.fit_score
               FROM outreaches o
               JOIN contacts c ON o.contact_id = c.id
               WHERE o.campaign_id = ? AND o.status = 'error'
               ORDER BY o.updated_at DESC""",
            (campaign_id,),
        ).fetchall()
    else:
        rows = db.execute(
            """SELECT o.id as outreach_id, o.campaign_id, o.contact_id,
                      o.status, o.followup_count, o.updated_at,
                      c.name, c.title, c.company, c.linkedin_url, c.fit_score
               FROM outreaches o
               JOIN contacts c ON o.contact_id = c.id
               WHERE o.status = 'error'
               ORDER BY o.updated_at DESC"""
        ).fetchall()
    db.close()
    return [dict(r) for r in rows]


# ──────────────────────────────────────────────
# Scheduler Jobs (Sprint 17)
# ──────────────────────────────────────────────

def create_scheduler_job(
    campaign_id: str,
    job_type: str,
    scheduled_at: int,
    outreach_id: Optional[str] = None,
) -> str:
    """Create a new scheduler job and return its ID."""
    job_id = str(uuid.uuid4())
    db = get_db()
    db.execute(
        """INSERT INTO scheduler_jobs (id, campaign_id, outreach_id, job_type, status, scheduled_at)
           VALUES (?, ?, ?, ?, 'pending', ?)""",
        (job_id, campaign_id, outreach_id, job_type, scheduled_at),
    )
    db.commit()
    db.close()
    return job_id


def get_ready_jobs(limit: int = 10) -> list[dict]:
    """Get jobs that are ready to execute (pending and scheduled_at <= now)."""
    now = int(time.time())
    db = get_db()
    rows = db.execute(
        """SELECT * FROM scheduler_jobs
           WHERE status = 'pending' AND scheduled_at <= ?
           ORDER BY scheduled_at ASC
           LIMIT ?""",
        (now, limit),
    ).fetchall()
    db.close()
    return [dict(r) for r in rows]


def claim_job(job_id: str) -> bool:
    """Atomically claim a pending job (set to running). Returns True if claimed."""
    now = int(time.time())
    db = get_db()
    cursor = db.execute(
        """UPDATE scheduler_jobs SET status = 'running', started_at = ?
           WHERE id = ? AND status = 'pending'""",
        (now, job_id),
    )
    db.commit()
    changed = cursor.rowcount > 0
    db.close()
    return changed


def complete_job(job_id: str, error: Optional[str] = None) -> None:
    """Mark a job as completed or failed."""
    now = int(time.time())
    status = "failed" if error else "completed"
    db = get_db()
    if error:
        db.execute(
            """UPDATE scheduler_jobs
               SET status = ?, completed_at = ?, error = ?, retry_count = retry_count + 1
               WHERE id = ?""",
            (status, now, error, job_id),
        )
    else:
        db.execute(
            """UPDATE scheduler_jobs SET status = ?, completed_at = ? WHERE id = ?""",
            (status, now, job_id),
        )
    db.commit()
    db.close()


def retry_job(job_id: str, new_scheduled_at: int) -> None:
    """Reset a failed job to pending with a new scheduled time."""
    db = get_db()
    db.execute(
        """UPDATE scheduler_jobs SET status = 'pending', scheduled_at = ?,
           started_at = NULL, completed_at = NULL, error = NULL
           WHERE id = ?""",
        (new_scheduled_at, job_id),
    )
    db.commit()
    db.close()


def get_pending_job_count(campaign_id: str, job_type: str) -> int:
    """Count pending/running jobs of a given type for a campaign (dedup check)."""
    db = get_db()
    row = db.execute(
        """SELECT COUNT(*) as cnt FROM scheduler_jobs
           WHERE campaign_id = ? AND job_type = ? AND status IN ('pending', 'running')""",
        (campaign_id, job_type),
    ).fetchone()
    db.close()
    return row["cnt"] if row else 0


def get_pending_outreach_job(outreach_id: str, job_type: str) -> Optional[dict]:
    """Check if a specific outreach already has a pending/running job of this type."""
    db = get_db()
    row = db.execute(
        """SELECT * FROM scheduler_jobs
           WHERE outreach_id = ? AND job_type = ? AND status IN ('pending', 'running')
           LIMIT 1""",
        (outreach_id, job_type),
    ).fetchone()
    db.close()
    return dict(row) if row else None


def cleanup_old_jobs(days: int = 7) -> int:
    """Purge completed/failed jobs older than N days. Returns count deleted."""
    cutoff = int(time.time()) - (days * 86400)
    db = get_db()
    cursor = db.execute(
        """DELETE FROM scheduler_jobs
           WHERE status IN ('completed', 'failed') AND created_at < ?""",
        (cutoff,),
    )
    db.commit()
    deleted = cursor.rowcount
    db.close()
    return deleted


def get_scheduler_stats() -> dict:
    """Get scheduler job stats for the dashboard."""
    db = get_db()
    rows = db.execute(
        """SELECT status, job_type, COUNT(*) as cnt
           FROM scheduler_jobs
           GROUP BY status, job_type"""
    ).fetchall()

    # Next scheduled jobs
    next_jobs = db.execute(
        """SELECT job_type, scheduled_at, outreach_id
           FROM scheduler_jobs
           WHERE status = 'pending'
           ORDER BY scheduled_at ASC
           LIMIT 5"""
    ).fetchall()

    # Recent completed/failed
    recent = db.execute(
        """SELECT job_type, status, completed_at, error
           FROM scheduler_jobs
           WHERE status IN ('completed', 'failed')
           ORDER BY completed_at DESC
           LIMIT 10"""
    ).fetchall()

    db.close()
    return {
        "counts": [dict(r) for r in rows],
        "next_jobs": [dict(r) for r in next_jobs],
        "recent": [dict(r) for r in recent],
    }
