"""Zoho CRM integration — bidirectional.

Push approved touches to Zoho, pull reply activity, detect opt-outs,
and enforce pre-delivery safety checks.

Auth and API client shared via shared.zoho.
"""

import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

from shared.zoho.client import api as _zoho_api, clean_business_name

from state.backlog import get_lead, update_lead, burn_lead, pause_lead, load_backlog
from state.gate import gate_status
from capabilities.sequencing import cancel_sequence, pause_sequence_on_reply
from capabilities.description import build_description_brief, get_vertical_segment
from capabilities.drafting import validate_no_placeholders
from capabilities.respond import classify_reply

REPLY_CHECK_STATE_PATH = Path("./data/reply_check_state.json")


def _enforce_push_prerequisites(lead: dict):
    """Raise ValueError if any push prerequisite fails."""
    gate = gate_status()
    if not gate.get("open", True):
        raise ValueError(f"Gate is CLOSED: {gate.get('closed_reason', 'no reason')}")

    if not lead.get("reviewed_at"):
        raise ValueError("Lead not approved (reviewed_at not set)")

    if lead.get("email_is_filing_service"):
        raise ValueError("Filing-service email — route to outside sales")

    email = lead.get("email", "").strip()
    if not email:
        raise ValueError("No usable email address")

    if lead.get("lead_status") in ("burned", "paused"):
        raise ValueError(f"Lead is {lead['lead_status']}")

    # Suppression check — final safety gate
    from state.suppressions import is_suppressed
    suppressed, reason = is_suppressed(email)
    if suppressed:
        raise ValueError(f"Email {email} is suppressed ({reason})")

    # Block push if draft body contains bracket placeholders
    draft_body = lead.get("draft_body", "")
    if draft_body:
        passed, matched = validate_no_placeholders(draft_body)
        if not passed:
            raise ValueError(
                f"Draft contains bracket placeholder '{matched}'. "
                f"Re-draft the lead before pushing."
            )


# Zoho Sequence Trace field API names
# (adjust if Zoho uses different API names for your custom fields)
_ZOHO_SEQUENCE_FIELD = "Sequence"
_ZOHO_TOUCH_FIELDS = {
    1: "Touch_1",
    2: "Touch_2",
    3: "Touch_3",
    4: "Touch_4",
    5: "Touch_5",
    6: "Touch_6",
}
# Fallback: API may still return Touch1 (no underscore) for position 1
_ZOHO_TOUCH_FIELDS_FALLBACK = {1: "Touch1"}


def _push_sequence_trace_to_zoho(zoho_id: str, sequence: dict) -> dict:
    """Push sequence trace data to Zoho Lead record.

    Writes the sequence spec (e.g. "3x60 — approved 2026-05-26")
    and individual touch reasoning + message to the Sequence Trace
    section fields on the Zoho lead.

    Args:
        zoho_id: Zoho CRM lead ID
        sequence: Sequence dict from lead (with touches, envelope, approved_at)

    Returns:
        Dict with status ("pushed"|"skipped"|"failed") and optional error
    """
    from shared.zoho.client import api as _zoho_api

    if not zoho_id:
        return {"status": "skipped", "error": "No zoho_id"}

    envelope = sequence.get("envelope", {})
    num_touches = envelope.get("touches", 0)
    duration_days = envelope.get("duration_days", 0)
    approved_at = sequence.get("approved_at", "")

    approved_date = approved_at.replace("Z", "").split(".")[0] if approved_at else ""

    spec_parts = []
    if num_touches and duration_days:
        spec_parts.append(f"{num_touches}\u00d7{duration_days}")
    if approved_date:
        spec_parts.append(f"approved {approved_date}")
    sequence_spec = " \u2014 ".join(spec_parts)

    payload = {_ZOHO_SEQUENCE_FIELD: sequence_spec}

    touches = sequence.get("touches", [])
    for i, touch in enumerate(touches):
        pos = i + 1
        if pos not in _ZOHO_TOUCH_FIELDS:
            break

        purpose = touch.get("purpose", "")
        differentiator = touch.get("differentiator_used", "") or ""
        subject = touch.get("subject", "")
        body = touch.get("body", "")

        lines = []
        if purpose:
            lines.append(f"Purpose: {purpose}")
        if differentiator:
            lines.append(f"Differentiator: {differentiator}")
        if subject:
            lines.append("")
            lines.append(f"Subject: {subject}")
        if body:
            lines.append("")
            lines.append("Message:")
            lines.append(body)

        field_value = "\n".join(lines)

        if len(field_value) > 2000:
            field_value = field_value[:1997] + "..."

        field_name = _ZOHO_TOUCH_FIELDS[pos]
        payload[field_name] = field_value

    try:
        _zoho_api("PUT", f"Leads/{zoho_id}", payload={"data": [payload]})
        return {"status": "pushed"}
    except Exception as e:
        # Touch_1 rename propagation fallback: retry with Touch1
        if "Touch_1" in str(payload) and "Invalid" in str(e):
            fallback_payload = dict(payload)
            for pos, fallback_name in _ZOHO_TOUCH_FIELDS_FALLBACK.items():
                if _ZOHO_TOUCH_FIELDS.get(pos) in fallback_payload:
                    fallback_payload[fallback_name] = fallback_payload.pop(_ZOHO_TOUCH_FIELDS[pos])
            try:
                _zoho_api("PUT", f"Leads/{zoho_id}", payload={"data": [fallback_payload]})
                return {"status": "pushed"}
            except Exception as e2:
                return {"status": "failed", "error": str(e2)}
        return {"status": "failed", "error": str(e)}


def _extract_last_name(lead: dict) -> str:
    """Derive a proper last name from principal names, agent name, or company name.

    Priority:
    1. Principal last name (e.g., "ELIZABETH WING" → "Wing")
    2. Agent last name if individual
    3. Empty string (caller should fall back to company name)
    """
    # Try principal names first
    principal_names = lead.get("principal_names") or []
    if principal_names:
        full_name = principal_names[0]
        if full_name:
            parts = full_name.strip().split()
            if len(parts) > 1:
                return parts[-1].title()
            return parts[-1].title()

    # Try agent name if individual agent
    if lead.get("agent_type") == "individual":
        agent_name = lead.get("agent_name", "")
        if agent_name:
            parts = agent_name.strip().split()
            if len(parts) > 1:
                return parts[-1].title()
            return parts[-1].title()

    return ""


def _compute_page_band(lead: dict) -> str:
    """Compute Page_Band picklist value from meter data."""
    mono = lead.get("equipment_mono_meter", 0) or 0
    color = lead.get("equipment_color_meter", 0) or 0
    total = mono + color
    if total == 0:
        return ""
    elif total <= 5000:
        return "Up to 5,000 pages"
    elif total <= 15000:
        return "5,001\u201315,000 pages"
    elif total <= 40000:
        return "15,001\u201340,000 pages"
    else:
        return "40,001+ pages"


def _compute_package_size(lead: dict) -> str:
    """Compute Package_Size from equipment model segment."""
    model = (lead.get("equipment_model") or "").upper()
    if any(x in model for x in ["C8255", "C8270", "B8255", "B8270"]):
        return "Extra Large (XL)"
    elif any(x in model for x in ["C8230", "C8235", "C8245", "B8230", "B8235", "B8245",
                                    "C8145", "C8150", "C8155", "B8145", "B8150", "B8155"]):
        return "Large (L)"
    elif any(x in model for x in ["C8120", "C8125", "C8130", "C8135", "B8120", "B8125",
                                    "C7130", "C7135", "C7140", "C7145", "B7130", "B7135"]):
        return "Medium (M)"
    elif any(x in model for x in ["C7120", "C7125", "B7120", "B7125", "C7000", "B7000",
                                    "B415", "B7025", "C605"]):
        return "Small (S)"
    else:
        return ""


def _format_sequence_summary(lead: dict) -> str:
    """Format Sequence field value: preset name + approval date."""
    seq = lead.get("sequence") or {}
    envelope = seq.get("envelope", {})
    num_touches = envelope.get("touches", 0)
    duration_days = envelope.get("duration_days", 0)
    definition = seq.get("definition", "")
    approved_at = seq.get("approved_at", "")
    approved_date = approved_at[:10] if approved_at else ""
    parts = []
    if num_touches and duration_days:
        parts.append(f"{num_touches}\u00d7{duration_days}")
    if definition:
        parts.append(definition)
    if approved_date:
        parts.append(f"approved {approved_date}")
    return " \u2014 ".join(parts) if parts else ""


def _get_touch_body(lead: dict, position: int) -> str:
    """Get the body of a specific sequence touch."""
    seq = lead.get("sequence") or {}
    for touch in seq.get("touches", []):
        if touch.get("position") == position:
            return touch.get("body", "")
    return ""


def _format_renewal_trace(lead: dict) -> str:
    """Format Renewal_Trace text with all touches and dates."""
    seq = lead.get("sequence") or {}
    touches = seq.get("touches", [])
    lines = []
    for touch in touches:
        pos = touch.get("position", "?")
        purpose = touch.get("purpose", "")
        scheduled = touch.get("scheduled_date", "")[:10] if touch.get("scheduled_date") else ""
        status = touch.get("status", "pending")
        subject = touch.get("subject", "")
        lines.append(f"Touch {pos}: {purpose}")
        if scheduled:
            lines.append(f"  Scheduled: {scheduled}")
        lines.append(f"  Status: {status}")
        if subject:
            lines.append(f"  Subject: {subject}")
        lines.append("")
    if not lines:
        return ""
    return "\n".join(lines).strip()


def _pick_newest(records: list) -> dict | None:
    """Pick the most recently created record from a list of Zoho records."""
    if not records:
        return None
    return max(records, key=lambda r: r.get("Created_Time", "") or "")


def push_lead_to_zoho(lead: dict, dry_run: bool = False) -> dict:
    """Push a lead with an approved draft to Zoho CRM.

    Routes to cold or renewal push path based on lead_type.

    Prerequisites (enforced):
    - reviewed_at set (approval gate)
    - draft_subject and draft_body present
    - NOT email_is_filing_service
    - Has usable email
    - Gate is OPEN

    Two sequential API calls (load-bearing -- do not consolidate):
    Call 1: all metadata fields
    Call 2: send trigger field (separate call, triggers Zoho workflow)

    Args:
        lead: Lead dict
        dry_run: If True, return what would be pushed without pushing

    Returns:
        Dict with zoho_id, status, error (if any)
    """
    _enforce_push_prerequisites(lead)

    is_renewal = lead.get("lead_type") == "renewal"
    greeting = lead.get("primary_principal_name") or lead.get("greeting_name", "")
    last_name = _extract_last_name(lead) or greeting or clean_business_name(lead.get("name", ""))
    if not last_name:
        last_name = lead.get("name", "Unknown")[:50]

    phone = lead.get("phone") or lead.get("domain_phone") or lead.get("brave_phone", "")

    naics_raw = lead.get("naics", "")
    vertical, segment = get_vertical_segment(naics_raw)

    # Build Description brief
    description_brief = build_description_brief(lead)

    # --- Call 1 payload: shared standard fields ---
    call1_data = {
        "Last_Name": last_name,
        "Company": clean_business_name(lead.get("name", "")) or lead.get("name", ""),
        "First_Name": greeting.split()[0] if greeting else "",
        "City": lead.get("city", ""),
        "State": lead.get("state", "CT"),
        "Email": lead.get("email", ""),
        "Lead_Source": "Renewal" if is_renewal else "CT SoS",
        "Email_Draft_Subject": lead.get("draft_subject", ""),
        "Description": description_brief,
        "Business_Vertical": vertical,
        "Vertical_Segment": segment,
    }

    # Conditional standard fields (shared across paths)
    if phone:
        # Normalize phone: digits only, then format as (XXX) XXX-XXXX if 10 digits
        import re
        digits = re.sub(r'\D', '', phone)
        if len(digits) == 10:
            phone = f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
        elif len(digits) == 11 and digits[0] == '1':
            phone = f"({digits[1:4]}) {digits[4:7]}-{digits[7:]}"
        else:
            phone = digits  # Fallback to raw digits
        call1_data["Phone"] = phone
    if lead.get("website_url"):
        call1_data["Website"] = lead.get("website_url")
    if lead.get("filing_date"):
        fd = lead["filing_date"].split("T")[0]
        call1_data["Milestone_Label"] = fd
    if lead.get("agent_type") == "business" and lead.get("agent_name"):
        call1_data["Filing_Agent"] = lead["agent_name"]
    if lead.get("draft_anchor"):
        call1_data["LLMAnchor"] = lead["draft_anchor"]
    if lead.get("entity_type"):
        call1_data["Business_Type"] = lead["entity_type"]
    if lead.get("research_note"):
        from capabilities.research import _format_research_for_zoho
        call1_data["LLMResearch_Note"] = _format_research_for_zoho(lead, lead["research_note"])
    if lead.get("filing_date"):
        try:
            filing_dt = datetime.fromisoformat(lead["filing_date"].replace("Z", ""))
            age_years = (datetime.utcnow() - filing_dt).days // 365
            call1_data["Lead_Age_Years"] = str(age_years)
        except Exception:
            pass

    # --- Path-specific fields ---
    if is_renewal:
        # Renewal equipment metadata
        if lead.get("contract_expiration"):
            call1_data["Lease_End_Date"] = lead["contract_expiration"][:10]
        if lead.get("equipment_model"):
            call1_data["Digital_Copiers_Type"] = lead["equipment_model"]
        if lead.get("renewal_classification"):
            call1_data["Category"] = lead["renewal_classification"]
        page_band = _compute_page_band(lead)
        if page_band:
            call1_data["Page_Band"] = page_band
        package_size = _compute_package_size(lead)
        if package_size:
            call1_data["Package_Size"] = package_size
        if lead.get("operator_intel"):
            call1_data["Operator_Intel"] = (lead.get("operator_intel") or "")[:255]
        # Renewal sequence trace
        renewal_trace = _format_renewal_trace(lead)
        if renewal_trace:
            call1_data["Renewal_Trace"] = renewal_trace
        # Call 2 trigger field name
        call2_field = "Current_Touch_Body"
    else:
        # Cold sequence trace
        seq_summary = _format_sequence_summary(lead)
        if seq_summary:
            call1_data["Sequence"] = seq_summary
        for pos in range(1, 7):
            body = _get_touch_body(lead, pos)
            if body:
                call1_data[_ZOHO_TOUCH_FIELDS[pos]] = body
        # Call 2 trigger field name
        call2_field = "Email_Draft_Body2"

    call2_value = lead.get("draft_body", "")

    if dry_run:
        return {
            "zoho_id": None,
            "status": "dry_run",
            "data": {"call1": call1_data, "call2": {call2_field: call2_value}},
            "error": None,
        }

    # Search for existing lead
    existing = None
    email = lead.get("email", "")
    company = call1_data["Company"]

    if email:
        try:
            result = _zoho_api("GET", "Leads/search", params={
                "criteria": f"(Email:equals:{email})"
            })
            if result.get("data"):
                existing = _pick_newest(result["data"])
        except Exception:
            pass

    if not existing and company:
        try:
            import urllib.parse
            result = _zoho_api("GET", "Leads/search", params={
                "criteria": f"(Company:equals:{urllib.parse.quote(company)})"
            })
            if result.get("data"):
                existing = _pick_newest(result["data"])
        except Exception:
            pass

    now = datetime.utcnow().isoformat() + "Z"

    if existing:
        zoho_id = existing["id"]

        # Write-once: check if Description already populated on Zoho record
        skip_description = lead.get("description_pushed", False)
        if not skip_description:
            try:
                existing_full = _zoho_api("GET", f"Leads/{zoho_id}")
                zoho_desc = (existing_full.get("data", [{}])[0].get("Description") or "").strip()
                if zoho_desc:
                    skip_description = True
            except Exception:
                pass

        if skip_description:
            call1_data.pop("Description", None)

        # Call 1 -- all metadata fields
        _zoho_api("PUT", f"Leads/{zoho_id}", payload={"data": [call1_data]})

        # Call 2 -- send trigger field only
        _zoho_api("PUT", f"Leads/{zoho_id}", payload={
            "data": [{call2_field: call2_value}]
        })

        update_lead(lead["id"], {
            "zoho_id": zoho_id,
            "pushed_to_zoho": True,
            "pushed_at": now,
            "draft_status": "sent",
            "description_pushed": True,
            "last_updated": now,
        })

        # Push pending sequence trace if cold lead has an approved sequence
        if not is_renewal and lead.get("sequence_trace_pending") and lead.get("sequence"):
            _push_sequence_trace_to_zoho(zoho_id, lead["sequence"])
            update_lead(lead["id"], {"sequence_trace_pending": False, "last_updated": now})

        return {"zoho_id": zoho_id, "status": "updated", "error": None}
    else:
        # New record: create with Call 1 fields, then PUT body
        result1 = _zoho_api("POST", "Leads", payload={"data": [call1_data]})

        if not result1.get("data"):
            return {"zoho_id": None, "status": "failed", "error": result1}

        zoho_id = result1["data"][0]["details"]["id"]

        # Call 2 -- send trigger field only
        _zoho_api("PUT", f"Leads/{zoho_id}", payload={
            "data": [{call2_field: call2_value}]
        })

        update_lead(lead["id"], {
            "zoho_id": zoho_id,
            "pushed_to_zoho": True,
            "pushed_at": now,
            "draft_status": "sent",
            "description_pushed": True,
            "last_updated": now,
        })

        # Push pending sequence trace if cold lead has an approved sequence
        if not is_renewal and lead.get("sequence_trace_pending") and lead.get("sequence"):
            _push_sequence_trace_to_zoho(zoho_id, lead["sequence"])
            update_lead(lead["id"], {"sequence_trace_pending": False, "last_updated": now})

        return {"zoho_id": zoho_id, "status": "created", "error": None}


def push_touch_to_zoho(lead: dict, touch: dict, dry_run: bool = False) -> dict:
    """Push a specific sequence touch to Zoho for sending.

    Args:
        lead: Lead dict
        touch: Touch dict from sequence
        dry_run: If True, return what would be pushed

    Returns:
        Dict with status, zoho_email_id, error
    """
    zoho_id = lead.get("zoho_id")
    if not zoho_id:
        return {"status": "failed", "error": "Lead not in Zoho (no zoho_id)"}

    if touch.get("status") != "approved":
        return {"status": "failed", "error": f"Touch status is {touch.get('status')}, not approved"}

    zoho_data = {
        "Email_Draft_Subject": touch.get("subject", ""),
        "Email_Draft_Body2": touch.get("body", ""),
    }

    if dry_run:
        return {"status": "dry_run", "data": zoho_data, "error": None}

    update_payload = {"data": [zoho_data]}
    result = _zoho_api("PUT", f"Leads/{zoho_id}", payload=update_payload)

    if result.get("data"):
        now = datetime.utcnow().isoformat() + "Z"
        touch["status"] = "sent"
        touch["sent_at"] = now

        sequence = lead.get("sequence") or {}
        for t in sequence.get("touches", []):
            if t.get("position") == touch["position"]:
                t["status"] = "sent"
                t["sent_at"] = now

        update_lead(lead["id"], {"sequence": sequence, "last_updated": now})
        return {"status": "sent", "error": None}

    return {"status": "failed", "error": result}


def push_batch(lead_ids: list[str], dry_run: bool = False) -> dict:
    """Push multiple approved leads to Zoho.

    Returns counts: pushed, skipped, failed, errors.
    """
    result = {"pushed": 0, "skipped": 0, "failed": 0, "errors": []}

    for lead_id in lead_ids:
        try:
            lead = get_lead(lead_id)
            if not lead:
                result["skipped"] += 1
                result["errors"].append(f"{lead_id}: not found")
                continue

            push_result = push_lead_to_zoho(lead, dry_run=dry_run)
            if push_result.get("status") in ("created", "updated", "dry_run"):
                result["pushed"] += 1
            else:
                result["failed"] += 1
                result["errors"].append(f"{lead_id}: {push_result.get('error')}")
        except ValueError as e:
            result["skipped"] += 1
            result["errors"].append(f"{lead_id}: {e}")
        except Exception as e:
            result["failed"] += 1
            result["errors"].append(f"{lead_id}: {e}")

    return result


def process_reply(lead_id: str, reply_text: str) -> dict:
    """Process a reply using the 5-category classifier.

    Called by:
    - check_for_replies() (automated polling)
    - absel respond (manual paste)

    Returns:
    {
        "action": "continue" | "burn" | "pause" | "flag_bounce" | "engage",
        "category": classification category,
        "details": human-readable summary,
    }
    """
    lead = get_lead(lead_id)
    if not lead:
        return {"action": "error", "details": f"Lead {lead_id} not found"}

    classification = classify_reply(reply_text)
    category = classification["category"]
    now = datetime.utcnow().isoformat() + "Z"

    base_update = {
        "reply_received": True,
        "reply_received_at": now,
        "reply_text": reply_text[:500],
        "reply_action": category,
        "last_updated": now,
    }

    if category == "autoresponder":
        lead["last_autoresponder"] = {
            "received_at": now,
            "text": reply_text[:500],
            "return_date": classification.get("return_date"),
        }
        update_lead(lead_id, {
            **base_update,
            "last_autoresponder": lead["last_autoresponder"],
        })
        return {
            "action": "continue",
            "category": "autoresponder",
            "details": f"Autoresponder: '{classification['matched_pattern']}'. Sequence continues.",
        }

    elif category == "hard_opt_out":
        try:
            burn_lead(lead_id, reason=f"opt_out_hard: '{classification['matched_pattern']}'")
        except Exception:
            pass
        try:
            cancel_sequence(lead_id)
        except Exception:
            pass
        update_lead(lead_id, {**base_update, "lead_status": "burned"})
        return {
            "action": "burn",
            "category": "hard_opt_out",
            "details": f"Opt-out: '{classification['matched_pattern']}'. Lead burned, sequence canceled.",
        }

    elif category == "soft_opt_out":
        try:
            pause_lead(lead_id, reason=f"opt_out_soft: '{classification['matched_pattern']}'")
        except Exception:
            pass
        try:
            pause_sequence_on_reply(lead_id)
        except Exception:
            pass
        update_lead(lead_id, base_update)
        return {
            "action": "pause",
            "category": "soft_opt_out",
            "details": f"Soft decline: '{classification['matched_pattern']}'. Lead and sequence paused. Review needed.",
        }

    elif category == "bounce":
        update_lead(lead_id, {
            **base_update,
            "email_bounced": True,
            "email_bounce_reason": reply_text[:200],
            "email_bounced_at": now,
        })
        try:
            pause_sequence_on_reply(lead_id)
        except Exception:
            pass
        return {
            "action": "flag_bounce",
            "category": "bounce",
            "details": f"Delivery failure: '{classification['matched_pattern']}'. Email flagged, sequence paused.",
        }

    elif category == "engagement":
        try:
            pause_sequence_on_reply(lead_id)
        except Exception:
            pass
        update_lead(lead_id, base_update)
        return {
            "action": "engage",
            "category": "engagement",
            "details": f"Human reply detected. Sequence paused. Use 'absel respond {lead_id}' to draft response.",
        }

    return {"action": "error", "category": "unknown", "details": "Unhandled classification"}


def check_for_replies(since: Optional[str] = None) -> list[dict]:
    """Poll Zoho for leads that have received email replies.

    Args:
        since: ISO timestamp (default: last 24 hours)

    Returns:
        List of dicts with lead_id, reply_text, reply_date, reply_from
    """
    if since is None:
        since = (datetime.utcnow() - timedelta(hours=24)).isoformat() + "Z"

    # Load last check state
    last_check = since
    if REPLY_CHECK_STATE_PATH.exists():
        try:
            with open(REPLY_CHECK_STATE_PATH) as f:
                state = json.load(f)
                last_check = state.get("last_checked", since)
        except Exception:
            pass

    replies = []

    # Query Zoho for email activity
    # This uses the Zoho CRM Activities API
    try:
        result = _zoho_api("GET", "Activities", params={
            "fields": "Activity_Type,Subject,Description,Created_Time,Who_Id",
            "criteria": f"(Activity_Type:equals:Email) and (Created_Time:after:{last_check})",
        })

        for activity in result.get("data", []):
            if activity.get("Activity_Type") == "Email":
                subject = activity.get("Subject", "")
                if "Re:" in subject or "re:" in subject.lower():
                    replies.append({
                        "zoho_id": activity.get("Who_Id", ""),
                        "reply_text": activity.get("Description", ""),
                        "reply_date": activity.get("Created_Time", ""),
                        "reply_from": "",
                    })
    except Exception as e:
        pass  # Zoho API may not have activities endpoint accessible

    # Save check state
    with open(REPLY_CHECK_STATE_PATH, 'w') as f:
        json.dump({"last_checked": datetime.utcnow().isoformat() + "Z"}, f, indent=2)

    return replies


def get_due_touches_safe(target_date: Optional[str] = None) -> list[dict]:
    """Get touches due today, AFTER checking for replies.

    1. Poll Zoho for any replies since last check
    2. Classify each reply via process_reply():
       - autoresponder -> log, sequence continues (touch still due)
       - hard opt-out -> burn + cancel (touch removed)
       - soft opt-out -> pause (touch removed)
       - bounce -> pause + flag email (touch removed)
       - engagement -> pause (touch removed)
    3. THEN query for due touches (autoresponder leads still included)
    """
    # Step 1: check replies
    replies = check_for_replies()
    for reply in replies:
        zoho_id = reply.get("zoho_id", "")
        if zoho_id:
            backlog = load_backlog()
            for lead in backlog:
                if lead.get("zoho_id") == zoho_id:
                    result = process_reply(lead["id"], reply["reply_text"])
                    action = result.get("action", "")
                    if action == "continue":
                        # Autoresponder -- log and keep in due list
                        pass
                    elif action in ("flag_bounce",):
                        # Bounce flagged, sequence paused -- will be filtered
                        pass
                    elif action in ("burn", "pause", "engage"):
                        # Opt-out or engagement -- lead/sequence paused
                        pass
                    break

    # Step 2: get due touches (burned/paused leads excluded by get_due_touches)
    from capabilities.sequencing import get_due_touches
    return get_due_touches(target_date)


def _get_last_reply_check() -> str:
    """Get the timestamp of the last reply check."""
    if REPLY_CHECK_STATE_PATH.exists():
        try:
            with open(REPLY_CHECK_STATE_PATH) as f:
                state = json.load(f)
                return state.get("last_checked", "")
        except Exception:
            pass
    return ""


def get_replies_summary(unhandled_only: bool = False) -> list[dict]:
    """Get all leads with replies.

    Args:
        unhandled_only: Only show leads with reply_action set

    Returns:
        List of dicts with lead info and reply details
    """
    backlog = load_backlog()
    results = []

    for lead in backlog:
        if lead.get("reply_received"):
            if unhandled_only and lead.get("reply_action") is None:
                continue
            results.append({
                "lead_id": lead["id"],
                "lead_name": lead.get("name", ""),
                "reply_received_at": lead.get("reply_received_at", ""),
                "reply_text": lead.get("reply_text", ""),
                "reply_action": lead.get("reply_action", ""),
                "lead_status": lead.get("lead_status", ""),
            })

    return results
