"""Renewal capability — upgrade recommendations, lead creation, renewal sequences.

Two responsibilities:
1. recommend_upgrade() — maps current equipment model to recommended
   upgrade using config-driven upgrade paths + meter count urgency.
2. create_renewal_leads() — converts Zohar renewal data into absel
   backlog leads with lead_type: "renewal".

Renewal leads skip the normal harvest → enrich → research pipeline.
They already have equipment data, contact info, and contract terms
from Zoho. The sequence engine generates renewal-mode emails.
"""

import re
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

from state.backlog import get_lead, update_lead, save_backlog, load_backlog
from capabilities.sequencing import preview_sequence, approve_sequence


# ── Upgrade paths: current model → recommended upgrade + reason ─────
# Config, not AI. Operator maintains this list as models change.

UPGRADE_PATHS = {
    "AltaLink C8145": {
        "upgrade": "AltaLink C8245",
        "reason": "higher monthly duty cycle, lower cost per page at volume",
    },
    "AltaLink C8230": {
        "upgrade": "AltaLink C8245",
        "reason": "faster print speed, enhanced scanning capabilities",
    },
    "AltaLink C8235": {
        "upgrade": "AltaLink C8255",
        "reason": "higher volume capacity, advanced finishing options",
    },
    "AltaLink C8245": {
        "upgrade": "AltaLink C8255",
        "reason": "higher volume capacity, newer platform with improved color accuracy",
    },
    "AltaLink C8255": {
        "upgrade": "AltaLink C8270",
        "reason": "enterprise-class speed and finishing for growing print volumes",
    },
    "AltaLink B8045": {
        "upgrade": "AltaLink B8065",
        "reason": "faster mono output, enhanced scanning and workflow automation",
    },
    "AltaLink B8065": {
        "upgrade": "AltaLink B8090",
        "reason": "highest mono volume capacity, production-class finishing",
    },
    "VersaLink B415": {
        "upgrade": "VersaLink B7025",
        "reason": "workgroup-class volume, duplex scanning",
    },
    "VersaLink B600": {
        "upgrade": "VersaLink B7025",
        "reason": "higher speed and paper capacity for growing offices",
    },
    "VersaLink B605": {
        "upgrade": "VersaLink B7030",
        "reason": "faster output, larger paper capacity, enhanced mobile printing",
    },
    "VersaLink C405": {
        "upgrade": "VersaLink C505",
        "reason": "higher color print speed, improved media handling",
    },
    "VersaLink C505": {
        "upgrade": "AltaLink C8230",
        "reason": "C505 is solid but the AltaLink platform offers better total cost of ownership",
    },
    "VersaLink C605": {
        "upgrade": "AltaLink C8230",
        "reason": "C605 is low-volume -- the C8230 handles real office workloads",
    },
    "WorkCentre 6515": {
        "upgrade": "AltaLink C8230",
        "reason": "WorkCentre series is end of life -- the AltaLink platform is the modern replacement",
    },
    "WorkCentre 6655": {
        "upgrade": "AltaLink C8245",
        "reason": "WorkCentre series is end of life -- the AltaLink C8245 exceeds 6655 capabilities",
    },
    "WorkCentre 7830": {
        "upgrade": "AltaLink C8245",
        "reason": "WorkCentre series is end of life -- AltaLink offers modern workflow tools",
    },
    "WorkCentre 7845": {
        "upgrade": "AltaLink C8255",
        "reason": "WorkCentre series is end of life -- AltaLink C8255 matches or exceeds 7845 volume",
    },
}


def classify_renewal(lead: dict) -> dict:
    """Classify a renewal lead by contract status.

    Pure local logic — no API calls. Uses days_until_expiration
    to determine classification, urgency, framing keyword, and
    recommended sequence preset.

    Returns:
        {
            "classification": str,
            "urgency": str,
            "framing": str,
            "recommended_preset": str,
        }
    """
    days = lead.get("days_until_expiration", 0) or 0

    if days < -90:
        return {
            "classification": "expired_critical",
            "urgency": "critical",
            "framing": "lapsed",
            "recommended_preset": "renewal_lapsed",
        }
    elif days < 0:
        return {
            "classification": "expired_recent",
            "urgency": "urgent",
            "framing": "recently_expired",
            "recommended_preset": "renewal_lapsed",
        }
    elif days <= 30:
        return {
            "classification": "upcoming_urgent",
            "urgency": "urgent",
            "framing": "imminent",
            "recommended_preset": "renewal_90",
        }
    elif days <= 90:
        return {
            "classification": "upcoming_standard",
            "urgency": "standard",
            "framing": "upcoming",
            "recommended_preset": "renewal_90",
        }
    else:
        return {
            "classification": "upcoming_early",
            "urgency": "early",
            "framing": "early_notice",
            "recommended_preset": "renewal_90",
        }


def _assess_meter_data(lead: dict) -> dict:
    """Assess meter data quality and generate appropriate context.

    When both meters are 0, the data is almost certainly MISSING
    (never recorded), not that the machine printed 0 pages.

    Returns:
        {
            "meters_available": bool,
            "meter_insight": str,
            "suggest_xda": bool,
            "prompt_context": str,   # injected into drafting prompt
        }
    """
    mono = lead.get("equipment_mono_meter", 0) or 0
    color = lead.get("equipment_color_meter", 0) or 0

    if mono == 0 and color == 0:
        return {
            "meters_available": False,
            "meter_insight": "Meters unavailable — not recorded in system",
            "suggest_xda": True,
            "prompt_context": (
                "METER DATA: Current readings are not available for this machine.\n"
                "Do NOT reference page counts, volume, or usage levels.\n"
                "Instead, note that remote monitoring should be set up so we can\n"
                "track usage and ensure supplies arrive on time.\n"
                "Frame this as a service improvement (Touch 2 or 3, not Touch 1)."
            ),
        }
    elif mono == 0 or color == 0:
        valid_meter = mono if mono > 0 else color
        meter_type = "mono" if mono > 0 else "color"
        return {
            "meters_available": True,
            "meter_insight": f"{valid_meter:,} {meter_type} pages recorded",
            "suggest_xda": True,
            "prompt_context": (
                f"METER DATA: This machine has {valid_meter:,} {meter_type} pages on record.\n"
                f"Only reference the {meter_type} meter — the other meter shows 0 which\n"
                f"likely means it wasn't recorded.\n"
                f"Consider suggesting remote monitoring for complete tracking\n"
                f"(Touch 2 or 3, not Touch 1)."
            ),
        }
    else:
        total = mono + color
        return {
            "meters_available": True,
            "meter_insight": f"{total:,} total pages ({mono:,} mono, {color:,} color)",
            "suggest_xda": False,
            "prompt_context": (
                f"METER DATA: This machine has printed {total:,} pages "
                f"({mono:,} mono, {color:,} color)."
            ),
        }


def recommend_upgrade(current_model: str, meters: Optional[dict] = None,
                       days_until_expiration: Optional[int] = None) -> dict:
    """Recommend an upgrade based on current equipment and usage.

    Factors:
    - Current model's upgrade path (from UPGRADE_PATHS)
    - Meter counts (high volume → recommend higher-tier upgrade)
    - Unknown models get a generic recommendation

    Returns:
    {
        "recommended_model": "AltaLink C8245",
        "reason": "...",
        "meter_insight": "...",
        "upgrade_urgency": "recommended" | "suggested" | "optional" | "critical" | "urgent",
    }

    When the contract is expired, contract status overrides meter-based
    urgency. A machine without service coverage is critical regardless
    of page count.
    """
    meters = meters or {}
    mono_raw = meters.get("mono") or 0
    color_raw = meters.get("color") or 0
    try:
        mono = int(mono_raw)
    except (ValueError, TypeError):
        mono = 0
    try:
        color = int(color_raw)
    except (ValueError, TypeError):
        color = 0
    total_pages = mono + color

    path = UPGRADE_PATHS.get(current_model)

    if path:
        recommended = path["upgrade"]
        reason = path["reason"]
    else:
        recommended = "AltaLink C8245"
        reason = "newer platform with improved features and lower cost per page"

    if total_pages > 100000:
        urgency = "recommended"
        meter_insight = f"{total_pages:,} total pages -- heavy use, upgrade strongly recommended"
    elif total_pages > 50000:
        urgency = "recommended"
        meter_insight = f"{total_pages:,} total pages -- solid use, good time to upgrade"
    elif total_pages > 20000:
        urgency = "suggested"
        meter_insight = f"{total_pages:,} total pages -- moderate use"
    else:
        urgency = "optional"
        meter_insight = f"{total_pages:,} total pages -- light use, current equipment may still serve"

    # Contract status overrides meter urgency when expired
    if days_until_expiration is not None and days_until_expiration < 0:
        months_expired = abs(days_until_expiration) // 30
        if months_expired > 6:
            urgency = "critical"
            meter_insight = (
                f"Contract expired {months_expired} months ago. "
                f"Operating without service coverage. "
                f"{meter_insight}"
            )
        elif months_expired > 0:
            urgency = "urgent"
            meter_insight = (
                f"Contract expired {months_expired} months ago. "
                f"{meter_insight}"
            )

    return {
        "recommended_model": recommended,
        "reason": reason,
        "meter_insight": meter_insight,
        "upgrade_urgency": urgency,
    }


def _derive_temporal_category(days_until: Optional[int]) -> str:
    """Derive temporal category from days until expiration.

    Negative values = expired contracts (different tone, schedule, urgency).
    """
    if days_until is None:
        return "renewal_120"
    if days_until < -90:
        return "renewal_expired"
    elif days_until < 0:
        return "renewal_expired_recent"
    elif days_until <= 30:
        return "renewal_30"
    elif days_until <= 60:
        return "renewal_60"
    elif days_until <= 90:
        return "renewal_90"
    else:
        return "renewal_120"


def _renewal_exists(zoho_deal_id: str) -> bool:
    """Check if a renewal already exists in the backlog."""
    backlog = load_backlog()
    for lead in backlog:
        if lead.get("zoho_deal_id") == zoho_deal_id:
            return True
    return False


def create_renewal_lead(renewal: dict, skip_zoho_check: bool = True) -> Optional[dict]:
    """Convert a Zohar renewal record into an absel backlog lead.

    Skips if zoho_deal_id already exists in backlog.
    Zoho dedup is skipped by default (renewals are sourced FROM Zoho).

    Returns the new lead dict, or None if skipped (duplicate).
    """
    zoho_deal_id = renewal.get("zoho_deal_id")
    if not zoho_deal_id:
        return None

    if _renewal_exists(zoho_deal_id):
        return None

    # Suppression check
    from state.suppressions import is_suppressed
    contact_email = renewal.get("contact_email", "")
    suppressed, reason = is_suppressed(contact_email)
    if suppressed:
        return None

    # Zoho dedup check (optional)
    if not skip_zoho_check and contact_email:
        from state.zoho_dedup import is_in_zoho
        try:
            in_zoho, context = is_in_zoho(contact_email)
            if in_zoho:
                return None
        except Exception:
            pass  # Fail open

    equipment = renewal.get("equipment") or {}
    meters = {
        "mono": int(equipment.get("mono_meter") or 0),
        "color": int(equipment.get("color_meter") or 0),
    }
    current_model = equipment.get("model", "")
    days_until = renewal.get("days_until_expiration")
    upgrade = recommend_upgrade(current_model, meters, days_until_expiration=days_until)

    # Compute equipment age from contract length
    contract_months = renewal.get("contract_length_months")
    equipment_age_months = contract_months

    temporal = _derive_temporal_category(days_until)

    contact_name = renewal.get("contact_name", "").strip()
    greeting = contact_name.split()[0] if contact_name else ""

    # Build lead dict with fields
    lead_id = f"renewal_{uuid.uuid4().hex[:12]}"
    now = datetime.utcnow().isoformat() + "Z"

    # Abstract lead dict so we can run classify_renewal on it
    lead = {
        "id": lead_id,
        "name": renewal.get("account_name", ""),
        "lead_type": "renewal",
        "lead_status": "active",

        "filing_date": (renewal.get("contract_expiration") or "")[:10],
        "harvested_at": now,
        "harvest_source": "zoho_renewal",

        "greeting_name": greeting,
        "greeting_name_source": "zoho_contact",
        "email": renewal.get("contact_email", ""),
        "phone": "",

        "equipment_model": current_model,
        "equipment_serial": equipment.get("serial", ""),
        "equipment_mono_meter": meters["mono"],
        "equipment_color_meter": meters["color"],
        "equipment_age_months": equipment_age_months,

        "contract_expiration": renewal.get("contract_expiration", ""),
        "days_until_expiration": days_until,
        "contract_value": renewal.get("contract_value", ""),
        "contract_length_months": contract_months,
        "zoho_deal_id": zoho_deal_id,

        "recommended_upgrade": upgrade["recommended_model"],
        "upgrade_reason": upgrade["reason"],
        "meter_insight": upgrade["meter_insight"],
        "upgrade_urgency": upgrade["upgrade_urgency"],

        "temporal_category": temporal,
        "sequence": None,

        "draft_status": "none",
        "reviewed_at": "",

        "created_at": now,
        "last_updated": now,
    }

    # Classify renewal and store on lead
    classification = classify_renewal(lead)
    lead["renewal_classification"] = classification["classification"]
    lead["renewal_urgency"] = classification["urgency"]

    backlog = load_backlog()
    backlog.append(lead)
    save_backlog(backlog)

    return lead


def create_renewal_leads(renewals: list[dict], skip_zoho_check: bool = True) -> dict:
    """Create renewal leads from a list of Zohar renewal records.

    Args:
        renewals: List of Zohar renewal records
        skip_zoho_check: Skip Zoho dedup (default True — renewals come FROM Zoho)

    Returns:
        {"created": N, "skipped": N, "total": N}
    """
    created = 0
    skipped = 0

    for renewal in renewals:
        lead = create_renewal_lead(renewal, skip_zoho_check=skip_zoho_check)
        if lead:
            created += 1
        else:
            skipped += 1

    return {"created": created, "skipped": skipped, "total": len(renewals)}


def get_renewal_leads(urgent_only: bool = False) -> list[dict]:
    """Return all renewal leads from the backlog.

    Args:
        urgent_only: If True, only renewals within 30 days.

    Returns:
        List of renewal lead dicts.
    """
    backlog = load_backlog()
    renewals = [
        l for l in backlog
        if l.get("lead_type") == "renewal"
        and l.get("lead_status", "active") == "active"
    ]

    if urgent_only:
        renewals = [l for l in renewals if l.get("days_until_expiration", 999) <= 30]

    # Sort: expired first (most urgent), then by days_until ascending
    def _sort_key(l):
        days = l.get("days_until_expiration", 999) or 999
        # Expired (negative) sort before upcoming (positive)
        # Within expired, most negative (longest expired) = most urgent
        if days < 0:
            return (0, days)  # expired group, sorted by how long expired
        else:
            return (1, days)  # upcoming group, sorted by how soon

    renewals.sort(key=_sort_key)
    return renewals


def cancel_renewal_lead(lead_id: str) -> dict:
    """Cancel a renewal lead — removes it from active renewal pipeline.

    Sets lead_status to "cancelled" with timestamp. The lead stays in
    backlog for audit purposes but is excluded from all renewal operations.

    Args:
        lead_id: Lead to cancel

    Returns:
        The updated lead dict

    Raises:
        KeyError: If lead not found
        ValueError: If lead is not a renewal lead
    """
    from state.backlog import get_lead, update_lead
    from datetime import datetime, timezone

    lead = get_lead(lead_id)
    if not lead:
        raise KeyError(f"Lead {lead_id} not found")
    if lead.get("lead_type") != "renewal":
        raise ValueError(f"Lead {lead_id} is not a renewal lead (type: {lead.get('lead_type')})")

    now = datetime.now(timezone.utc).isoformat() + "Z"
    update_lead(lead_id, {
        "lead_status": "cancelled",
        "cancelled_at": now,
        "cancelled_reason": "",
        "last_updated": now,
    })
    return get_lead(lead_id)


def calculate_renewal_schedule(
    touches: int,
    days_until_expiration: int,
    buffer_days: int = 15,
    lapsed_duration: int = 21,
) -> list[int]:
    """Schedule renewal touches anchored to the expiration date.

    When the contract is expired (days_until_expiration <= 0), the
    expiration date is ignored. Schedule is forward-looking from
    today using lapsed_duration (default 21 days for lapsed preset).

    Upcoming: deadline-anchored, touches distributed toward expiration.
      4 touches, 90 days until expiration, 15-day buffer:
        Touch 1: Day 0   (90 days before)
        Touch 2: Day 25  (65 days before)
        Touch 3: Day 50  (40 days before)
        Touch 4: Day 75  (15 days before)

    Expired: forward-looking, evenly spaced.
      3 touches, 21-day duration:
        Touch 1: Day 0
        Touch 2: Day 10
        Touch 3: Day 21
    """
    if touches <= 1:
        return [0]

    if days_until_expiration <= 0:
        # Expired: schedule forward from today
        gap = lapsed_duration // (touches - 1) if touches > 1 else lapsed_duration
        gap = max(gap, 5)
        schedule = [min(i * gap, lapsed_duration) for i in range(touches)]
        schedule[-1] = lapsed_duration
        return schedule

    # Upcoming: anchor to expiration date
    available_days = max(1, days_until_expiration - buffer_days)
    gap = available_days / (touches - 1) if touches > 1 else 0

    schedule = [int(round(i * gap)) for i in range(touches)]

    schedule[-1] = available_days

    schedule = [max(0, d) for d in schedule]
    for i in range(1, len(schedule)):
        schedule[i] = max(schedule[i], schedule[i - 1] + 1)

    return schedule


# ── Manual renewal lead creation ────────────────────────────────────

PROFESSIONAL_TITLES = {"dr", "mr", "ms", "mrs", "esq"}
CREDENTIAL_SUFFIXES = {"md", "dds", "do", "esq", "lcsw", "psy.d", "ph.d", "jd"}


def _parse_expiration_date(date_str: str) -> str:
    """Parse expiration date from operator input into ISO format.

    Accepts:
    - "7/27/2026" (M/DD/YYYY)
    - "7/27/26" (M/DD/YY)
    - "2026-07-27" (ISO)
    - "July 27, 2026" (natural language)

    Returns ISO format: "2026-07-27"
    Raises ValueError if unparseable.
    """
    cleaned = date_str.strip()

    # ISO format: YYYY-MM-DD
    iso_match = re.match(r"^(\d{4})-(\d{1,2})-(\d{1,2})$", cleaned)
    if iso_match:
        y, m, d = iso_match.groups()
        return f"{y}-{int(m):02d}-{int(d):02d}"

    # US format: M/DD/YYYY or M/DD/YY
    us_match = re.match(r"^(\d{1,2})/(\d{1,2})/(\d{2,4})$", cleaned)
    if us_match:
        m, d, y = us_match.groups()
        y = int(y)
        if y < 100:
            y += 2000
        return f"{y:04d}-{int(m):02d}-{int(d):02d}"

    # Natural language: "Month DD, YYYY"
    for fmt in ("%B %d, %Y", "%B %d %Y", "%b %d, %Y", "%b %d %Y"):
        try:
            parsed = datetime.strptime(cleaned, fmt)
            return parsed.strftime("%Y-%m-%d")
        except ValueError:
            continue

    raise ValueError(
        f"Could not parse expiration date: '{date_str}'. "
        "Use M/DD/YYYY (e.g. 7/27/2026), YYYY-MM-DD, or natural language."
    )


def _resolve_greeting_from_contact(contact_name: str) -> tuple[str, str]:
    """Extract greeting name from contact name.

    Returns (greeting_name, greeting_name_source).

    "Amy"                → ("Amy", "contact_first")
    "Amy Johnson"        → ("Amy", "contact_first")
    "Dr. Amy Johnson"    → ("Dr. Johnson", "contact_professional")
    "Amy Johnson, DDS"   → ("Dr. Johnson", "contact_professional")
    "Amy Johnson MD"     → ("Dr. Johnson", "contact_professional")
    ""                   → ("", "contact_first")
    """
    cleaned = contact_name.strip()
    if not cleaned:
        return ("", "contact_first")

    parts = cleaned.replace(",", "").split()
    has_title = False
    title = ""

    # Check for professional title prefix
    if parts and parts[0].lower().rstrip(".") in PROFESSIONAL_TITLES:
        has_title = True
        raw_title = parts[0].rstrip(".")
        title = raw_title + "." if not raw_title.endswith(".") else raw_title
        parts = parts[1:]

    if not parts:
        return ("", "contact_first")

    # Check for credential suffix in last part
    last = parts[-1].lower().rstrip(".")
    if last in CREDENTIAL_SUFFIXES:
        has_title = True
        if not title:
            title = "Dr."
        parts = parts[:-1]

    if not parts:
        return ("", "contact_first")

    if has_title:
        # Use "Dr. Johnson" pattern
        first_name = parts[0].capitalize()
        last_name = parts[-1].capitalize() if len(parts) > 1 else ""
        greeting = f"{title} {last_name}" if last_name else f"{title} {first_name}"
        return (greeting, "contact_professional")

    # Simple: just first name
    return (parts[0].capitalize(), "contact_first")


def _extract_company_from_email(email: str) -> str:
    """Extract company name from email domain.

    "amy@starline.com"     → "Starline"
    "amy.johnson@co.net"   → "Co"
    ""                     → "Unknown Company"

    Simple heuristic: domain part, split on dots, title-case first segment.
    """
    if not email or "@" not in email:
        return "Unknown Company"
    domain = email.split("@", 1)[1].split(".")[0]
    return domain.capitalize()


def _extract_contact_from_email(email: str) -> str:
    """Extract contact name from email prefix.

    "amy@starline.com"       → "Amy"
    "amy.johnson@co.net"     → "Amy"
    "amy-johnson@co.net"     → "Amy"
    ""                       → ""

    Simple heuristic: email prefix, split on non-alpha, title-case first segment.
    """
    if not email or "@" not in email:
        return ""
    prefix = email.split("@", 1)[0]
    # Split on common separators and take first alpha segment
    parts = re.split(r"[._\-+]+", prefix)
    for p in parts:
        if p.strip() and p.isalpha():
            return p.strip().capitalize()
    return ""


def create_renewal_lead_manual(
    email: str,
    contact_name: str | None = None,
    company_name: str | None = None,
    equipment_model: str | None = None,
    equipment_serial: str | None = "",
    contract_expiration: str | None = None,
    mono_meter: int = 0,
    color_meter: int = 0,
    phone: str | None = None,
    notes: str | None = None,
    intel: str | None = None,
    force: bool = False,
) -> dict:
    """Create a renewal lead from operator-provided details.

    No Zoho query needed. The operator provides everything.

    Steps:
    1. Validate required fields (email, model, expiration)
    2. Fill defaults from email when optional fields omitted
    3. Parse and validate the expiration date
    4. Compute days_until_expiration from today
    5. Look up upgrade recommendation from UPGRADE_PATHS
    6. Compute meter insight and upgrade urgency
    7. Resolve greeting name from contact_name
    8. Check for duplicates (by email + equipment_serial)
    9. Build the renewal lead dict with full schema
    10. Save to backlog
    11. Return the lead dict

    Raises ValueError on validation failure or duplicate (unless force=True).
    """
    # ── 1. Validate required fields ──
    if not email:
        raise ValueError("email is required")
    if not equipment_model:
        raise ValueError("equipment_model is required")
    if not contract_expiration:
        raise ValueError("contract_expiration is required")

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

    # Zoho dedup check (skip for renewals — existing customers expected in Zoho)
    if not force:
        from state.zoho_dedup import is_in_zoho
        try:
            in_zoho, context = is_in_zoho(email)
            if in_zoho:
                raise ValueError(f"Email {email} already exists in Zoho ({context})")
        except ValueError:
            raise
        except Exception:
            pass  # Fail open

    # ── 2. Fill defaults from email ──
    if not company_name:
        company_name = _extract_company_from_email(email)
    if not contact_name:
        contact_name = _extract_contact_from_email(email)

    # ── 3. Parse expiration date ──
    parsed_expiration = _parse_expiration_date(contract_expiration)

    # ── 4. Compute days until expiration ──
    from datetime import date as _date
    exp_date = _date.fromisoformat(parsed_expiration)
    days_until = (exp_date - _date.today()).days

    # ── 5. Upgrade recommendation ──
    meters = {"mono": mono_meter, "color": color_meter}
    upgrade = recommend_upgrade(equipment_model, meters, days_until_expiration=days_until)

    # ── 6. Resolve greeting ──
    greeting_name, greeting_source = _resolve_greeting_from_contact(contact_name)

    # ── 7. Check duplicates ──
    serial_key = equipment_serial or "unknown"
    backlog = load_backlog()
    existing = [
        l for l in backlog
        if l.get("email") == email
        and l.get("equipment_serial") == serial_key
        and l.get("lead_type") == "renewal"
    ]
    if existing and not force:
        raise ValueError(
            f"Renewal lead already exists for {email} / {serial_key}: "
            f"{existing[0]['id']}. Use --force to create anyway."
        )

    # ── 8. Build lead dict ──
    temporal = _derive_temporal_category(days_until)
    lead_id = f"renewal_{uuid.uuid4().hex[:12]}"
    now = datetime.utcnow().isoformat() + "Z"

    # Build lead dict
    lead = {
        "id": lead_id,
        "name": company_name,
        "lead_type": "renewal",
        "lead_status": "active",

        "filing_date": parsed_expiration,
        "harvested_at": now,
        "harvest_source": "zoho_renewal",

        "greeting_name": greeting_name,
        "greeting_name_source": greeting_source,
        "email": email,
        "phone": phone or "",

        "equipment_model": equipment_model,
        "equipment_serial": serial_key,
        "equipment_mono_meter": mono_meter,
        "equipment_color_meter": color_meter,
        "equipment_age_months": None,

        "contract_expiration": parsed_expiration,
        "days_until_expiration": days_until,
        "contract_value": "",
        "contract_length_months": None,
        "zoho_deal_id": "",

        "recommended_upgrade": upgrade["recommended_model"],
        "upgrade_reason": upgrade["reason"],
        "meter_insight": upgrade["meter_insight"],
        "upgrade_urgency": upgrade["upgrade_urgency"],

        "temporal_category": temporal,
        "sequence": None,

        "draft_status": "none",
        "reviewed_at": "",

        "notes": notes or "",
        "operator_intel": intel or "",
        "created_at": now,
        "last_updated": now,
    }

    # Classify renewal and store on lead
    classification = classify_renewal(lead)
    lead["renewal_classification"] = classification["classification"]
    lead["renewal_urgency"] = classification["urgency"]

    backlog.append(lead)
    save_backlog(backlog)

    return lead
