"""Drafting capability - generates LLM email drafts for leads.

Principles-based prompt assembly. Temporal data goes into the lead profile;
the LLM decides how to use it. Scenario tag is stored as metadata only.
Equipment talk track from enrichment is the substance source.
"""

import re
import yaml
from pathlib import Path
from datetime import datetime
from typing import Dict, Optional, List

from integrations.llm import call_llm, LLMError
from integrations.ctsos import FILING_SERVICE_DOMAINS
from state.backlog import get_lead, update_lead
from capabilities.scoring import classify_email_domain


SIGNATURE = """Mark Mazza
Inside Sales
ABS | Xerox
www.xeroxne.com
CT 203-777-0011 x743"""

SERVICES_KNOWLEDGE_PATH = Path("./config/services-knowledge.yaml")

COMPETITOR_BRANDS = [
    "toshiba", "ricoh", "canon", "konica", "minolta",
    "kyocera", "lexmark", "brother", "sharp", "lanier",
    "savin", "gestetner", "epson", "hewlett-packard", "hewlett",
    "e-studio", "imagerunner", "bizhub", "ecosys", "taskalfa",
]

RESTRICTED_MODELS = [
    "workcentre",
]

FABRICATED_CONVERSATION_PHRASES = [
    "we discussed",
    "as we discussed",
    "as i mentioned",
    "when we spoke",
    "from our conversation",
    "you mentioned",
]

LLM_THINKING_LEAK_PATTERNS = [
    "thinking process:",
    "here's a thinking process:",
    "here is a thinking process:",
    "let me structure",
    "deconstruct the request",
    "analyze the user input",
]

PLACEHOLDER_PATTERNS = [
    r"\[Last\s*Name\]",
    r"\[First\s*Name\]",
    r"\[FirstName\]",
    r"\[LastName\]",
    r"\[Company\s*Name\]",
    r"\[Company\]",
    r"\[Business\s*Name\]",
    r"\[Business\]",
    r"\[Name\]",
    r"\[Title\]",
    r"\[\s*Name\s*\]",
    r"\[\s*Last\s+Name\s*\]",
    r"\[\s*First\s+Name\s*\]",
    r"\[\s*Company\s*\]",
    r"\[\s*Business\s*\]",
]


def validate_no_placeholders(text: str) -> tuple[bool, str | None]:
    """Check draft for bracket placeholders like [Last Name], [FirstName], etc.

    Returns (passed, matched_pattern_or_None).
    """
    for pattern in PLACEHOLDER_PATTERNS:
        if re.search(pattern, text, re.IGNORECASE):
            return (False, pattern)
    return (True, None)


def validate_no_competitor_brands(text: str) -> tuple[bool, str | None]:
    """Check draft for competitor brand mentions.
    Returns (passed, matched_brand_or_None)."""
    lower = text.lower()
    for brand in COMPETITOR_BRANDS:
        if brand in lower:
            return (False, brand)
    return (True, None)


def validate_no_restricted_models(text: str) -> tuple[bool, str | None]:
    """Check draft for restricted/end-of-life model mentions.
    Returns (passed, matched_model_or_None)."""
    lower = text.lower()
    for model in RESTRICTED_MODELS:
        if model in lower:
            return (False, model)
    return (True, None)


def validate_no_fabricated_conversations(text: str) -> tuple[bool, str | None]:
    """Check draft for phrases implying prior conversation.
    Returns (passed, matched_phrase_or_None)."""
    lower = text.lower()
    for phrase in FABRICATED_CONVERSATION_PHRASES:
        if phrase in lower:
            return (False, phrase)
    return (True, None)


def validate_no_thinking_leak(text: str) -> tuple[bool, str | None]:
    """Check for LLM thinking/reasoning process leaked into output.
    Returns (passed, matched_pattern_or_None)."""
    lower = text.lower()
    for pattern in LLM_THINKING_LEAK_PATTERNS:
        if pattern in lower:
            return (False, pattern)
    return (True, None)


def validate_subject_not_empty(subject: str, body: str) -> tuple[bool, str | None]:
    """Check that subject line is not empty.
    Returns (passed, None) or (False, 'empty subject')."""
    if not subject.strip():
        return (False, "empty subject")
    return (True, None)


def _build_base_drafting_rules(ctx: Dict) -> list[str]:
    """Build the complete set of base drafting rules shared by
    single-touch and sequence drafting.

    This is the canonical source of all drafting constraints.
    Both _build_prompt() and _build_sequence_prompt() should start
    with these rules, then add their own context on top.
    """
    services_context = _get_services_context()
    window = ctx.get("outreach_window") or {}

    lines = [
        "YOU ARE writing as Mark Mazza, who reads CT Secretary of State filings",
        "and reaches out to businesses he thinks he might be able to help. He",
        "is not a closer. He is the person you talk to early, before you need",
        "anything, because by the time you need something you want to already",
        "know who to call. His emails are short because the recipient is busy",
        "and because saying less is harder than saying more.",
        "",
    ]

    if services_context:
        lines.append(services_context)

    lines.extend([
        "DRAFTING RULES:",
        "",
        "DO NOT write any of these - they read as mail-merge tells:",
        '  - "Congratulations" on anything',
        '  - "No pressure" / "No pressure at all"',
        '  - "I\'m here as a resource" / "Feel free to reach out"',
        '  - Generic industry compliments',
        '  - "I read the state filing for"',
        '  - "I read your state filing."',
        "",
        "PRODUCT ANCHOR:",
        "  You sell copiers, printers, and multifunction devices. Physical machines",
        "  that print, copy, scan, and fax. Scan-to-email, scan-to-folder, and Follow",
        "  You Print are features OF the hardware, not standalone services. You do not",
        "  sell document management software, workflow consulting, digital",
        "  transformation, or IT services. Every problem you describe must be solved by",
        "  a physical machine in their office. If the sentence would still make sense",
        "  without a copier involved, cut it.",
        "",
        "EQUIPMENT REFERENCES: You sell Xerox equipment through ABS. If you",
        "  reference a specific machine, it must be a Xerox model. Never mention",
        "  competitor equipment by brand or model name. Never invent model",
        "  numbers. Never reference a prior conversation that didn't happen --",
        "  you have not spoken to this person before.",
        "",
        "  Competitor brands that must NEVER appear in any draft:",
        "  Toshiba, Ricoh, Canon, HP, Hewlett-Packard, Brother, Sharp,",
        "  Konica Minolta, Kyocera, Lexmark, Lanier, Savin, Gestetner, Epson",
        "",
        "  Restricted models that must NEVER appear in outbound outreach:",
        "  WorkCentre (any model) -- end of life. Only mention in replies when",
        "  the prospect brings it up first.",
        "",
        "NO FABRICATED CONVERSATIONS:",
        "  Never write any of these -- they imply prior contact that didn't happen:",
        '  - "we discussed"',
        '  - "as we discussed"',
        '  - "as I mentioned"',
        '  - "when we spoke"',
        '  - "from our conversation"',
        '  - "you mentioned"',
        "",
        "NO EM DASHES:",
        "  Never use em dashes (\u2014). Use two hyphens (--) instead. Non-negotiable.",
        "",
        "FLEXIBILITY FRAMING:",
        "  ABS offers BOTH leasing and rental/month-to-month programs. The rental",
        "  program is aimed at businesses without 2 years of credit history. Frame",
        "  ABS as having multiple paths (lease, rental, month-to-month), NOT as",
        "  rental-only. Never say 'We rent, we don't lease.'",
        "",
        "GREETING: Open with the recipient's name plus a natural lead-in.",
        "  Never the name alone on a line.",
        "",
        "  TIME OF DAY (optional, not required on every email):",
        "  This email is being sent in the " + str(ctx.get("send_period", "afternoon")) + " (" + str(ctx.get("send_time", "")) + ").",
        '  You may use time-appropriate greetings like "Good morning" or',
        '  "Good afternoon" based on when this email is being sent. Do not',
        "  overthink this -- use it when it feels natural, skip it when it doesn't.",
        "",
        "  FORMALITY (based on available data):",
        "  If the recipient has a professional title (Dr., Esq., or a designation",
        "  like President, Director, Managing Partner):",
        "    - Use their title: \"Dr. Riccio,\" or \"Dr. Freeman,\"",
        "    - Never use \"Hey\" with a professional title",
        "    - Tone: respectful, peer-to-peer, not stiff",
        "  If no professional title:",
        "    - First name is fine: \"Andrew,\" or \"Hey Andrew,\"",
        "    - \"Hey\" is appropriate for casual businesses (wellness, creative, retail, trades)",
        "    - Skip \"Hey\" for finance, insurance, accounting",
        "",
        "  MILESTONE ACKNOWLEDGMENT (for historical leads only):",
        "  You may acknowledge business milestones naturally. NEVER use",
        "  \"congratulations\" or \"congrats.\" Instead, recognize the achievement",
        "  like a peer would:",
        "    Good: \"Five years in Fairfield -- that's no small thing.\"",
        "    Good: \"Your practice has been going strong since 2021.\"",
        "    Good: \"Half a decade in business says a lot.\"",
        "    Bad: \"Congratulations on your five-year anniversary!\"",
        "    Bad: \"Congrats on the milestone!\"",
        "",
        "  RESEARCH-INFORMED OPENER (when research note has a standout finding):",
        "  If the research note contains something specific and notable",
        "  (expansion, hiring, award, new service), you may lead with it:",
        '    \"Andrew, saw that Sas Strategic is getting off the ground --\"',
        '    \"Dr. Freeman, noticed your Milford expansion --\"',
        "  Only use this when the finding is genuinely noteworthy. Do not force",
        "  it -- a simple warm opener is better than a forced research reference.",
        "",
        "  SIMPLE OPENERS (always available as fallbacks):",
        '    \"[Name], quick note from Mark at ABS Xerox.\"',
        '    \"[Name], reaching out about your practice.\"',
        '    \"Dr. [Name], wanted to introduce myself.\"',
        "",
        "  DO NOT use:",
        "  - Name alone on a line with nothing after it",
        '  - \"I hope this finds you well\"',
        '  - "I wanted to reach out" / "I\'m reaching out"',
        '  - \"Just wanted to\"',
        '  - \"Congratulations\" / \"Congrats\"',
        '  - \"Hey\" with any professional title (Dr., Esq., etc.)',
        "",
        "NO BRACKET PLACEHOLDERS:",
        "  Never use [LastName], [First Name], [Company], [Name], [Title], or any",
        "  other bracket-delimited placeholder in the draft output. If the greeting_name",
        "  is available, use the actual name. If it is not available, write the email",
        "  without a salutation -- just begin with the first sentence. A bracket",
        "  placeholder is a rejected output.",
        "",
        "TEMPORAL FRAMING PRINCIPLE:",
        "  Let the business's age inform your framing naturally. A recent filing",
        "  means you noticed it -- saying 'saw your filing' is honest and natural.",
        "  For a business that filed years ago, your reason for reaching out is their",
        "  business age and the natural equipment lifecycle, not that you found their",
        "  filing. Don't explain how you found them. Don't make claims about their",
        "  practice that imply research they didn't consent to. State their age or",
        "  milestone naturally and connect it to equipment timing. If they're near a",
        "  milestone anniversary, it's worth acknowledging. Don't force a",
        "  celebration.",
        "",
        "  For historical leads specifically: NEVER open with 'Saw [Business]',",
        "  'noticed [Business]', 'I saw', or any phrasing that narrates the act of",
        "  observing them. Say 'your practice is about five years in' not 'saw your",
        "  practice is coming up on five years.' State the age, don't narrate the",
        "  observation.",
        "",
        "  For historical leads, you MAY open by acknowledging a common equipment",
        "  frustration -- slow machines, expensive service calls, contracts that",
        "  auto-renewed without notice -- before pivoting to what you offer. This",
        "  works because established businesses have lived with these frustrations",
        "  and recognizing them builds credibility. Do not fabricate frustrations",
        "  the business hasn't experienced -- keep it general enough to resonate",
        "  without being presumptuous. This is a tool, not a requirement.",
        "",
        "CLOSE -- depends on the lead's age:",
        "",
        "  For fresh filings (temporal_category is 'fresh' or 'recent'):",
        "    End by stating simply who you are and what you do. You are planting a seed,",
        "    not pitching. They should remember your name when equipment becomes relevant.",
        "    Keep it to one sentence. Do not offer to 'brief' them, 'outline options,'",
        "    'discuss their setup,' or schedule a call.",
        "",
        "  For historical/milestone leads (temporal_category is 'historical'):",
        "    End with ONE specific, concrete thing about ABS or Xerox equipment that the",
        "    recipient probably doesn't know and would find genuinely useful. Pick the one",
        "    most relevant to their business type from the close differentiators above. This",
        "    is not a pitch -- it's a piece of information that earns the reply by being worth",
        "    knowing. Do not list multiple differentiators. One specific thing, stated plainly.",
        "    Some differentiators have alternative phrasings -- use whichever fits the email's",
        "    flow naturally.",
        "",
        "  For ALL leads:",
        "    Never end with 'I can outline' / 'I can brief you' / 'let's discuss' /",
        "    'when you're ready' / 'you'll know who to call' / 'happy to chat' /",
        "    'let me know if you'd like to explore.' These are all variations of asking",
        "    for a meeting the recipient hasn't expressed interest in.",
        "",
        "    The strongest close is the last substantive sentence followed by the",
        "    signature. No separate closing pitch line.",
        "",
    ])

    if ctx.get("has_substance_context"):
        lines.extend([
            "ABOUT THE BUSINESS CONTEXT BELOW:",
            "The description is for YOU to understand who you're writing to.",
            "Do not list it back - reference at most ONE specific detail naturally.",
            "",
        ])

    lines.extend([
        'BEFORE WRITING: answer "ANCHOR:" - what is one specific thing about',
        'THIS business that connects to a real document/print workflow problem?',
        'Use that anchor as the foundation. Include the ANCHOR line before the email.',
        "",
    ])

    lines.extend([
        "SUBJECT LINE: 4-8 words. Must reference one specific detail about THIS",
        "business's likely workflow or situation. Could this subject have been sent",
        "to any other business in the same city and industry? If yes, rewrite.",
        "Never use 'For [Name] in [City]'. Never start with the business name.",
        "Never start with 'A note' or 'Setting up'.",
        "",
        "TONE: confident, brief, specific. Warmth comes from knowing their industry.",
        "",
    ])

    return lines

_GREETING_CREDENTIALS = [
    "M.D.", "MD", "D.M.D.", "DMD", "D.D.S.", "DDS", "D.O.", "DO",
    "Ph.D.", "PhD", "J.D.", "JD", "Esq.", "Esq",
    "MBA", "CPA", "RN", "NP", "PA", "Psy.D.", "PsyD",
    "DPT", "OTD", "Au.D.", "AuD",
]


def _normalize_greeting_name(name: str) -> str:
    """Normalize a name for use as a greeting.

    1. Title-case: "BUHLE MHLONGO" → "Buhle Mhlongo"
    2. Strip credential suffixes: "Anisha Singh, M.D." → "Anisha Singh"
    3. Extract first name only: "Anisha Singh" → "Anisha"
    """
    if not name:
        return ""

    s = name.strip()

    # Title-case normalization (handles ALL CAPS and mixed case)
    if s and any(c.isalpha() for c in s):
        s = s.title()

    # Strip credential suffixes
    for cred in _GREETING_CREDENTIALS:
        s = re.sub(rf',\s*{re.escape(cred)}\b', '', s, flags=re.I)
        s = re.sub(rf'\s+{re.escape(cred)}\b', '', s, flags=re.I)

    s = s.strip()

    # Extract first name only
    parts = s.split()
    if parts:
        return parts[0]

    return s


GENERIC_PREFIXES = {
    "info", "admin", "office", "contact", "hello", "team",
    "support", "billing", "sales", "service", "noreply",
    "accounting", "hr", "mail", "help", "general", "enquiry",
    "inquiry", "reception", "frontdesk", "ops", "operations",
    "staff", "management", "director", "manager", "exec",
    "treasurer", "secretary", "webmaster", "postmaster",
}

def _determine_greeting(lead: Dict) -> Optional[str]:
    """Determine greeting name using strict priority chain.

    If lead has a pre-resolved greeting set (e.g., by preview_sequence()),
    use that instead of re-resolving. This ensures consistency across
    all touches in a sequence.

    Priority order:
    0. Pre-resolved greeting (set by sequence engine)
    1. Primary principal first name (from enrichment, if Individual type)
    2. Agent name (if agent_type is "individual")
    3. Email prefix (if it looks like a person's name)
    4. First name from business name (if personal-name PLLC)
    5. Short company name (if <= 35 chars)
    6. None (no salutation)
    """
    # 0. Pre-resolved greeting (sequence engine sets this once)
    pre_resolved = lead.get("_resolved_greeting")
    if pre_resolved is not None:
        lead["greeting_name_source"] = "pre_resolved"
        return pre_resolved

    # 1. Primary principal first name (set during enrichment)
    primary = (lead.get("primary_principal_name") or "").strip()
    if primary:
        lead["greeting_name_source"] = "principal"
        return _normalize_greeting_name(primary)

    # 2. Agent name — check if agent_type is "individual"
    agent_type = (lead.get("agent_type") or "").lower().strip()
    agent_name = (lead.get("agent_name") or "").strip()
    if agent_name and agent_type == "individual":
        lead["greeting_name_source"] = "agent"
        # Normalize: title-case, strip credentials, extract first name
        return _normalize_greeting_name(agent_name)

    # 3. Email prefix — extract first name from local part
    email = lead.get("email", "")
    if email and "@" in email:
        prefix = email.split("@")[0].lower()
        clean = prefix.split(".")[0].strip("0123456789_-")
        if len(clean) >= 3 and clean not in GENERIC_PREFIXES:
            lead["greeting_name_source"] = "email_prefix"
            return clean.capitalize()

    # 4. First name from business name (personal-name PLLC pattern)
    name = (lead.get("name") or "").strip()
    if name:
        match = re.match(
            r'^([A-Z][a-z]+)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s*,?\s*(PLLC|P\.L\.L\.C|LLC|L\.L\.C|P\.C\.|PC)\s*$',
            name, re.I
        )
        if match:
            lead["greeting_name_source"] = "business_name"
            return match.group(1)

        match = re.match(
            r'^([A-Z][a-z]+)\s+([A-Z][a-z]+)\s*,?\s*(LLC|INC|CORP|LTD)\s*$',
            name, re.I
        )
        if match:
            lead["greeting_name_source"] = "business_name"
            return match.group(1)

    # 5. Short company name (usable as address)
    if name and len(name) <= 35:
        cleaned = re.sub(
            r'\s*,?\s*(PLLC|P\.L\.L\.C|LLC|L\.L\.C|INC\.?|CORP\.?|LTD\.?|LLP|P\.C\.?)\s*$',
            '', name, flags=re.I
        ).strip()
        if cleaned and len(cleaned) <= 35:
            lead["greeting_name_source"] = "business_name"
            return cleaned
        if len(name) <= 35:
            lead["greeting_name_source"] = "business_name"
            return name

    # 6. No greeting
    lead["greeting_name_source"] = "none"
    return None


def _looks_like_person_name(name: str) -> bool:
    """Check if a name looks like a person (not a business entity)."""
    name_lower = name.lower()
    company_keywords = [
        "llc", "inc", "corp", "corporation", "company", "registered agent",
        "associates", "group", "services", "agency", "trust", "holdings",
        "investments", "capital", "ventures", "properties", "management",
        "secretary", "state", "division", "department",
    ]
    if any(kw in name_lower for kw in company_keywords):
        return False

    if re.match(r'^[A-Z][a-z]+\s+[A-Z][a-z]+', name):
        return True

    return False


def _compute_signal_tier(lead: Dict) -> tuple:
    """Compute signal tier for display in review queue and brief.

    Returns:
        Tuple of (stars_string, tier_label)
        ★★★ Strong: custom email + phone + live website
        ★★ Good: custom email + phone OR custom email + live website
        ★ Basic: custom email only OR phone only
        ⚠ Weak: filing-service email, no phone, no website
    """
    email = lead.get("email", "")
    phone = lead.get("phone", "") or lead.get("domain_phone", "") or lead.get("brave_phone", "")
    has_website = lead.get("website_exists", False)

    # Check if email is usable (not filing service, not free provider)
    has_real_email = False
    if email and "@" in email:
        domain = email.split("@")[-1].lower().strip()
        is_filing = lead.get("email_is_filing_service", False)
        is_free = domain in {"gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "aol.com", "icloud.com"}
        has_real_email = not is_filing and not is_free

    # Filing-service with no phone = weak
    if lead.get("email_is_filing_service", False) and not phone:
        return ("⚠", "Weak")

    if has_real_email and phone and has_website:
        return ("★★★", "Strong")
    elif (has_real_email and phone) or (has_real_email and has_website):
        return ("★★", "Good")
    elif has_real_email or phone:
        return ("★", "Basic")
    else:
        return ("⚠", "Weak")


def _select_scenario(lead: Dict) -> str:
    """Select scenario tag for lead metadata (not used for prompt branching)."""
    if lead.get("temporal_category") == "fresh":
        return "new_filing"
    elif lead.get("temporal_category") == "recent":
        return "recent_operating"
    elif lead.get("milestone_window_hit"):
        yr = lead.get("milestone_year", "?")
        return f"milestone_{yr}yr"
    else:
        return "established_business"


def _get_services_context() -> str:
    """Load services knowledge and return compact context for prompt."""
    if not SERVICES_KNOWLEDGE_PATH.exists():
        return ""

    try:
        with open(SERVICES_KNOWLEDGE_PATH) as f:
            knowledge = yaml.safe_load(f)
    except Exception:
        return ""

    company = knowledge.get("company", {})
    company_name = company.get("name", "ABS Xerox")

    lines = [
        "YOUR COMPANY:",
        f"- You're with {company_name}",
        "- You help businesses with document and print setup",
        "",
    ]

    # Pain points as category-level reference only
    pain_points = knowledge.get("pain_points", {})
    if pain_points:
        lines.append("CATEGORY-LEVEL PAIN POINTS (reference only — these are common for these")
        lines.append("verticals, NOT necessarily true for this specific business):")
        for vertical, points in pain_points.items():
            lines.append(f"  {vertical}: {points}")
        lines.append("")

    never_say = knowledge.get("never_say", [])
    if never_say:
        lines.append("NEVER:")
        for rule in never_say:
            lines.append(f"- {rule}")
        lines.append("")

    # Close differentiators -- for historical leads, LLM picks ONE
    differentiators = knowledge.get("close_differentiators", {})
    if differentiators:
        lines.append("CLOSE DIFFERENTIATORS (for historical/milestone leads only -- pick ONE):")
        for key, diff in differentiators.items():
            lines.append(f"  {key}:")
            lines.append(f"    Fact: {diff['fact']}")
            if diff.get("alt_phrasing"):
                lines.append(f"    Alternative phrasing: {diff['alt_phrasing']}")
            lines.append(f"    Relevant for: {diff['relevant_for']}")
            lines.append(f"    Use when: {diff['use_when']}")
        lines.append("")

    return "\n".join(lines)


def _proper_case_city(city: str) -> str:
    """Convert city to proper case if all caps."""
    if not city:
        return city
    if city == city.upper():
        return city.title()
    return city


def _clean_name_for_greeting(name: str) -> str:
    """Extract greeting-friendly name: 'Moxie Therapy PLLC' -> 'Moxie Therapy'."""
    if not name:
        return name

    cleaned = re.sub(
        r'\s*,?\s*(PLLC|P\.L\.L\.C|LLC|L\.L\.C|INC\.?|Inc\.?|CORP\.?|Corp\.?|LTD\.?|LLP|L\.P\.?|P\.C\.?|P\.A\.?)\s*$',
        '', name, flags=re.I
    ).strip().rstrip(',').strip()

    return cleaned or name


def _build_context_object(lead: Dict) -> Dict:
    """Build rich context object from lead signals."""
    name = lead.get("name", "")
    city = _proper_case_city(lead.get("city", ""))
    state = lead.get("state", "CT")
    email = lead.get("email", "")
    phone = lead.get("phone", "")
    filing_date = lead.get("filing_date", "")
    naics_raw = lead.get("naics", "")
    business_type = lead.get("entity_type", "LLC")
    citizenship = lead.get("citizenship", "Domestic")
    agent_name = lead.get("agent_name", "")
    score = lead.get("score", 0)
    naics_score = lead.get("naics_score", 0)
    name_score = lead.get("name_score", 0)
    tier = lead.get("tier", "unknown")
    is_shell = lead.get("is_shell", False)
    website_exists = lead.get("website_exists", False)
    website_url = lead.get("website_url", "")
    domain = lead.get("domain", "")
    temporal_category = lead.get("temporal_category", "")
    milestone_year = lead.get("milestone_year")
    milestone_window_hit = lead.get("milestone_window_hit", False)

    email_tier, _ = classify_email_domain(email)

    # Calculate business age
    business_age_months = None
    age_years = None
    if filing_date:
        try:
            filed = datetime.strptime(filing_date[:10], "%Y-%m-%d")
            days_old = (datetime.utcnow() - filed).days
            business_age_months = int(days_old / 30.44)
            age_years = int(days_old / 365.25)
        except Exception:
            pass

    outreach_window = None
    if filing_date:
        try:
            filed = datetime.strptime(filing_date[:10], "%Y-%m-%d")
            days_old = (datetime.utcnow() - filed).days

            if days_old <= 3:
                window_name = "silent"
            elif days_old <= 7:
                window_name = "first_contact"
            elif days_old <= 14:
                window_name = "follow_up"
            elif days_old <= 30:
                window_name = "nurture"
            else:
                window_name = "content"

            outreach_window = {
                "window": window_name,
                "days_since_filing": days_old,
            }
        except Exception:
            pass

    equipment = lead.get("equipment") or {}
    brave_summary = lead.get("brave_summary", "")

    has_substance_context = bool(brave_summary or equipment.get("talk_track"))

    greeting_name = _determine_greeting(lead)
    email_is_filing_service = lead.get("email_is_filing_service", False)
    signal_stars, signal_label = _compute_signal_tier(lead)

    # Research note (from website + public sources)
    research_note = lead.get("research_note") or {}
    research_findings = research_note.get("findings") or []
    research_angle = research_note.get("suggested_angle") or ""

    return {
        "name": name,
        "greeting_name": greeting_name,
        "city": city,
        "state": state,
        "email": email,
        "phone": phone,
        "filing_date": filing_date[:10] if filing_date else "",
        "naics": naics_raw[:100] if naics_raw else "",
        "business_type": business_type,
        "citizenship": citizenship,
        "agent_name": agent_name,
        "score": score,
        "naics_score": naics_score,
        "name_score": name_score,
        "tier": tier,
        "is_shell": is_shell,
        "email_tier": email_tier,
        "email_domain": domain,
        "email_is_filing_service": email_is_filing_service,
        "website_exists": website_exists,
        "website_url": website_url,
        "outreach_window": outreach_window,
        "brave_summary": brave_summary,
        "equipment_talk_track": (equipment or {}).get("talk_track", ""),
        "equipment_typical_volume": (equipment or {}).get("typical_volume", ""),
        "has_substance_context": has_substance_context,
        "source": lead.get("source", "recent"),
        "temporal_category": temporal_category,
        "business_age_months": business_age_months,
        "age_years": age_years,
        "milestone_year": milestone_year,
        "milestone_window_hit": milestone_window_hit,
        "signal_stars": signal_stars,
        "signal_label": signal_label,
        "has_research": bool(research_findings),
        "research_findings": research_findings,
        "research_suggested_angle": research_angle,
        "send_time": datetime.now().strftime("%I:%M %p").lstrip("0"),
        "send_period": _get_send_period(),
    }


def _get_send_period() -> str:
    """Return morning, afternoon, or evening based on current hour."""
    hour = datetime.now().hour
    if hour < 12:
        return "morning"
    elif hour < 17:
        return "afternoon"
    else:
        return "evening"


def _load_global_intel() -> dict:
    """Load global intel from config/intel.yaml.
    Returns {'active': bool, 'message': str, 'set_at': str} or empty defaults."""
    config_path = Path(__file__).parent.parent / "config" / "intel.yaml"
    if not config_path.exists():
        return {"active": False, "message": "", "set_at": None}
    try:
        import yaml
        with open(config_path) as f:
            data = yaml.safe_load(f) or {}
        return data.get("global_intel", {"active": False, "message": "", "set_at": None})
    except Exception:
        return {"active": False, "message": "", "set_at": None}


def _build_intel_section(lead: dict, batch_intel: str | None = None) -> str:
    """Build the operator intel section for the prompt.

    Combines per-lead, per-batch, and global intel.
    Returns empty string if no intel is active.
    """
    sections = []

    lead_intel = (lead.get("operator_intel") or "").strip()
    if lead_intel:
        sections.append(f"[LEAD-SPECIFIC]\n{lead_intel}")

    if batch_intel and batch_intel.strip():
        sections.append(f"[BATCH]\n{batch_intel.strip()}")

    global_config = _load_global_intel()
    if global_config.get("active") and global_config.get("message", "").strip():
        sections.append(f"[GLOBAL]\n{global_config['message'].strip()}")

    if not sections:
        return ""

    return (
        "\nOPERATOR INTEL:\n\n"
        + "\n\n".join(sections)
        + "\n\n"
        + "Use these naturally where relevant. Do not force all of them\n"
        + "into every email. Pick the one or two that connect most\n"
        + "naturally to this touch's purpose and the lead's situation.\n"
        + "If none are relevant to this specific touch, ignore them.\n"
    )


def _build_prompt(ctx: Dict, lead: dict | None = None, batch_intel: str | None = None) -> str:
    """Build email draft prompt with principles-based framing.

    Uses _build_base_drafting_rules() as the canonical source of all
    drafting constraints, then adds lead-specific profile and signature.

    Args:
        ctx: Context object from _build_context_object
        lead: Lead dict (for operator_intel access)
        batch_intel: Optional per-batch intel (not stored on lead)

    Returns:
        Complete prompt string
    """
    window = ctx.get("outreach_window") or {}

    lines = _build_base_drafting_rules(ctx)

    lines.extend([
        "LENGTH: 90-120 words. Signature block does NOT count.",
        "",
        "NO vague calls to action.",
        "",
    ])

    # WHO THEY ARE — includes temporal data
    lines.append("WHO THEY ARE:")
    lines.append(f"  Business: {ctx['name']}")

    greeting = ctx.get("greeting_name")
    if greeting:
        lines.append(f"  Greeting name: {greeting}")
    else:
        lines.append("  Greeting name: (none -- start email without salutation)")

    lines.append(f"  Industry: {ctx['naics']}")
    lines.append(f"  Location: {ctx['city']}, {ctx['state']}")
    lines.append(f"  Filed: {ctx['filing_date']}")

    if ctx.get("business_age_months") is not None:
        lines.append(f"  Business age: {ctx['business_age_months']} months")

    if ctx.get("age_years") is not None:
        lines.append(f"  Years in operation: {ctx['age_years']}")

    if ctx.get("temporal_category"):
        lines.append(f"  Temporal category: {ctx['temporal_category']}")

    if ctx.get("milestone_window_hit") and ctx.get("milestone_year"):
        lines.append(f"  Near milestone: {ctx['milestone_year']}-year anniversary")

    lines.append("")

    # HOW THEY OPERATE — equipment talk track is the substance source
    if ctx.get("has_substance_context"):
        lines.append("HOW THEY OPERATE - for your understanding:")
        if ctx.get("brave_summary"):
            lines.append(f"  Business description: {ctx['brave_summary']}")
        if ctx.get("equipment_talk_track"):
            lines.append(f"  Workflow context: {ctx['equipment_talk_track']}")
        lines.append("")

    # RESEARCH NOTE — surfaced from website and public sources
    if ctx.get("has_research"):
        lines.append("RESEARCH NOTE (from website and public sources):")
        for finding in ctx["research_findings"]:
            lines.append(f"  - {finding}")
        if ctx.get("research_suggested_angle"):
            lines.append(f"  Suggested angle: {ctx['research_suggested_angle']}")
        lines.append("")
        lines.append("  Use ONE or TWO of these details naturally in the email.")
        lines.append("  Do not list them. Do not reference all of them.")
        lines.append("  Pick the detail that connects most naturally to")
        lines.append("  why their document/print setup matters right now.")
        lines.append("  If none of the details are relevant to equipment,")
        lines.append("  ignore them and draft from the other context.")
        lines.append("")

    # OPERATOR INTEL — injected if any scope has intel
    lead_for_intel = lead or ctx.get("_lead")
    if not lead_for_intel:
        lead_for_intel = {}
    intel_section = _build_intel_section(lead_for_intel, batch_intel=batch_intel)
    if intel_section:
        for line in intel_section.strip().split("\n"):
            lines.append(line)
        lines.append("")

    # CONTACT REALITY
    lines.append("CONTACT REALITY:")

    if ctx["email_tier"] == "professional_tld":
        lines.append(f"  Email: {ctx['email']} (professional TLD)")
    elif ctx["email_tier"] == "custom_business_domain":
        lines.append(f"  Email: {ctx['email']} (custom domain)")
    elif ctx["email_tier"] == "free_provider":
        lines.append(f"  Email: {ctx['email']} (free email)")
    else:
        lines.append(f"  Email: not provided")

    if ctx["website_exists"]:
        lines.append(f"  Website: {ctx['website_url']} - live")

    if ctx.get("phone"):
        lines.append(f"  Phone found: {ctx['phone']}")

    if window:
        lines.append(f"  Days since filing: {window.get('days_since_filing', '?')} days")

    lines.extend([
        "",
        "SIGNATURE - include EXACTLY:",
        SIGNATURE,
        "",
        "Generate ONLY the email. Start with 'SUBJECT:' on first line,",
        "then blank line, then body. No preamble.",
    ])

    return "\n".join(lines)


def _parse_llm_response(response: str) -> Dict:
    """Parse LLM response into subject/body/anchor.

    Applies post-processing:
    - Replaces all em dashes (\u2014) with double hyphens (--)
    - Validates phone number presence in signature
    """
    raw_lines = response.strip().split("\n")
    anchor = ""
    subject = ""
    body_start = 0

    for i, line in enumerate(raw_lines):
        if line.upper().startswith("ANCHOR:"):
            anchor = line.split(":", 1)[1].strip()
        elif line.upper().startswith("SUBJECT:"):
            subject = line.split(":", 1)[1].strip()
            body_start = i + 1
            break

    while body_start < len(raw_lines) and not raw_lines[body_start].strip():
        body_start += 1

    body = "\n".join(raw_lines[body_start:]).strip()

    if not body:
        body = response.strip()

    # Em dash replacement -- guarantee zero em dashes in output.
    # Step 1: Replace em dashes with double dashes
    # Step 2: Ensure spaces around double dashes using regex
    #   (?<! )--(?! ) matches -- not already flanked by spaces
    _dash_spacer = re.compile(r"(?<! )--(?! )")
    for field in ("subject", "body", "anchor"):
        text = locals()[field]
        text = text.replace("\u2014", "--")
        text = _dash_spacer.sub(" -- ", text)
        locals()[field] = text
    subject = locals()["subject"]
    body = locals()["body"]
    anchor = locals()["anchor"]

    # Phone validation
    if "203-777-0011" not in body:
        raise ValueError("Draft rejected: signature missing phone number 203-777-0011")

    # Congratulation ban
    blocked = [w for w in ["congratulations", "congrats"] if w in body.lower()]
    if blocked:
        raise ValueError(f"Draft rejected: contains '{blocked[0]}' -- never congratulate")

    # Hey Dr. / Hey [Title] ban
    if re.search(r"\b[Hh]ey\s+Dr\.", body):
        raise ValueError("Draft rejected: 'Hey Dr.' -- never use Hey with a professional title")

    # Banned opener phrases
    banned_openers = ["i hope this finds you well", "i wanted to reach out", "i'm reaching out",
                      "just wanted to"]
    body_lower = body.lower().strip()
    for phrase in banned_openers:
        if phrase in body_lower:
            raise ValueError(f"Draft rejected: contains banned opener '{phrase}'")

    return {
        "subject": subject,
        "body": body,
        "anchor": anchor,
    }


def draft_touch(lead_id: str, prompt_variant: Optional[str] = None, batch_intel: str | None = None) -> Dict:
    """Generate an email draft for a lead.

    Args:
        lead_id: Lead ID to draft for
        prompt_variant: Optional variant override (ignored)
        batch_intel: Optional per-batch intel (not stored on lead)

    Returns:
        Touch dict with subject, body, anchor
    """
    lead = get_lead(lead_id)
    if not lead:
        raise KeyError(f"Lead {lead_id} not found")

    if not lead.get("enrichment_status"):
        raise ValueError(
            f"Lead {lead_id} has not been enriched. "
            f"Run 'absel enrich {lead_id}' before drafting."
        )

    ctx = _build_context_object(lead)
    ctx["_lead"] = lead
    scenario = _select_scenario(lead)

    prompt = _build_prompt(ctx, lead=lead, batch_intel=batch_intel)

    try:
        response, metadata = call_llm(prompt)
    except LLMError as e:
        now = datetime.utcnow().isoformat() + "Z"
        update_lead(lead_id, {
            "draft_status": "failed",
            "draft_model": e.model or "",
            "draft_model_position": "",
            "drafted_at": now,
            "draft_error": f"LLMError({e.error_type}): {e.detail}",
            "last_updated": now,
        })
        raise ValueError(
            f"Draft failed: LLM unavailable ({e.error_type}). "
            f"All {len(e.attempts)} attempts across {e.attempts[0].get('position','?') if e.attempts else '?'} to "
            f"{e.attempts[-1].get('position','?') if e.attempts else '?'} failed. Retry later."
        ) from e

    parsed = _parse_llm_response(response)

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

    update_fields = {
        "draft_subject": parsed["subject"],
        "draft_body": parsed["body"],
        "draft_anchor": parsed["anchor"],
        "draft_scenario": scenario,
        "draft_model": metadata.get("model_used", ""),
        "draft_model_position": metadata.get("model_position", ""),
        "drafted_at": now,
        "draft_status": "drafted",
        "last_updated": now,
    }

    update_lead(lead_id, update_fields)

    return parsed


# ── Renewal prompt mode ─────────────────────────────────────────────

RENEWAL_URGENCY_PHRASES = [
    "act now", "act soon",
    "don't miss", "dont miss",
    "about to expire",
    "time is running out",
    "limited time",
    "last chance",
    "expires soon",
    "hurry",
]

RENEWAL_LAPSED_BANNED_PHRASES = [
    "plenty of time",
    "no rush",
]


RENEWAL_COLD_INTRO_PHRASES = [
    "saw your filing",
    "saw your recent filing",
    "i read your filing",
    "your state filing",
    "reaching out about your practice",
    "reaching out about your business",
    "wanted to introduce myself",
    "plant the seed",
    "wanted to reach out about",
    "noticed your business",
]


def validate_renewal_draft(text: str, equipment_model: str = "",
                            is_expired: bool = False) -> tuple[bool, str | None]:
    """Validate a renewal draft for renewal-specific rules.

    Checks (in order):
    1. Cold-outreach intro language (must not be present)
    2. Urgency/panic language (must not be present)
    3. Equipment model mention (must be present if model known)
    4. Lapsed-contract banned phrases (if expired)
    5. All standard validators still apply (competitor brands,
       fabricated conversations, placeholders, etc.)

    Returns (passed, reason_or_None).
    """
    lower = text.lower()

    # 1. No cold-outreach intro language
    for phrase in RENEWAL_COLD_INTRO_PHRASES:
        if phrase in lower:
            return (False, f"cold-intro language: '{phrase}'")

    # 2. No urgency/panic language
    for phrase in RENEWAL_URGENCY_PHRASES:
        if phrase in lower:
            return (False, f"urgency language: '{phrase}'")

    # 3. Equipment model mention required
    if equipment_model:
        model_lower = equipment_model.lower()
        if model_lower not in lower:
            return (False, f"missing equipment model mention: '{equipment_model}'")

    # 4. Lapsed-contract banned phrases
    if is_expired:
        for phrase in RENEWAL_LAPSED_BANNED_PHRASES:
            if phrase in lower:
                return (False, f"lapsed-contract language: '{phrase}' — contract is expired, do not suggest there is plenty of time")

    # 5. Standard validators still apply — delegate to existing ones
    passed, reason = validate_no_competitor_brands(text)
    if not passed:
        return (False, f"competitor brand: {reason}")

    passed, reason = validate_no_fabricated_conversations(text)
    if not passed:
        return (False, f"fabricated conversation: {reason}")

    passed, reason = validate_no_restricted_models(text)
    if not passed:
        return (False, f"restricted model: {reason}")

    passed, reason = validate_no_placeholders(text)
    if not passed:
        return (False, f"placeholder: {reason}")

    return (True, None)


def _build_lapsed_framing(lead: Dict) -> list[str]:
    """Build lapsed-contract framing lines for the prompt.

    Returns a list of string lines to inject into the renewal prompt.
    """
    days_until = lead.get("days_until_expiration", 0) or 0
    months_expired = abs(days_until) // 30
    classification = lead.get("renewal_classification", "")
    urgency = lead.get("renewal_urgency", "")

    lines = [
        "",
        "--- EXPIRED CONTRACT ---",
        f"This customer's service contract EXPIRED {months_expired} month(s) ago.",
        "They are currently operating WITHOUT service coverage.",
        "If their machine needs service, they are paying out of pocket.",
        "",
        "TONE: Direct, service-recovery. Not a sales pitch — a heads-up.",
        "  You are helping them realize they're unprotected and offering",
        "  to fix it. This is a service recovery, not a standard renewal.",
        "",
    ]

    if classification == "expired_critical":
        lines.extend([
            "URGENCY: Critical. This contract has been expired for months.",
            "  The customer is exposed and likely doesn't realize the risk.",
            "  Frame reinstatement as protecting their business, not as you",
            "  trying to sell something.",
            "",
        ])
    elif classification == "expired_recent":
        lines.extend([
            "URGENCY: Urgent. The contract just lapsed.",
            "  Act fast before they find another vendor or decide they don't",
            "  need coverage. Window to reinstate is closing.",
            "",
        ])

    lines.extend([
        "DO NOT SAY:",
        '  - "Plenty of time" — the contract is already expired',
        '  - "No rush" — they need coverage now',
        '  - "When you\'re ready" — you need to tell them they\'re unprotected',
        "",
    ])

    return lines


def _build_renewal_prompt(ctx: Dict, lead: Dict, batch_intel: str | None = None) -> str:
    """Build renewal-mode email draft prompt.

    Inherits the FULL base drafting rules (_build_base_drafting_rules)
    — includes the never-say list, competitor bans, placeholder bans,
    and all other guardrails.

    Adds renewal-specific context on top, plus lapsed-contract framing
    when applicable. Meter assessment context (XDA, partial data) is
    injected for the appropriate touch positions.
    """
    from capabilities.renewals import _assess_meter_data

    equipment_model = lead.get("equipment_model", "")
    equipment_serial = lead.get("equipment_serial", "")
    contract_expiration = lead.get("contract_expiration", "")
    days_until = lead.get("days_until_expiration")
    upgrade = lead.get("recommended_upgrade", "")
    upgrade_reason = lead.get("upgrade_reason", "")
    meter_insight = lead.get("meter_insight", "")
    classification = lead.get("renewal_classification", "")

    meter_assessment = _assess_meter_data(lead)

    # STEP 1: Base rules (never-say list, competitor bans, etc.)
    lines = list(_build_base_drafting_rules(ctx))

    # STEP 2: Replace the cold-outreach intro with renewal context
    # The base rules say "reads CT Secretary of State filings" —
    # this is wrong for renewal leads. Wipe the identity intro.
    # Find and replace the first few lines.
    for i, line in enumerate(lines):
        if "CT Secretary of State" in line:
            lines[i] = "YOU ARE writing as Mark Mazza to an EXISTING CUSTOMER."
            lines[i + 1] = "This is NOT a cold outreach."
            break

    # Replace the outreach window with renewal specific instructions
    lines.append("")
    lines.append("--- RENEWAL CONTEXT ---")
    lines.append("")
    lines.append("The recipient is already an ABS Xerox customer with equipment")
    lines.append("on a service contract. You have their account history, equipment")
    lines.append("details, and contract status. That is the substance of this email.")
    lines.append("")
    lines.append("You do NOT need to:")
    lines.append("  - Introduce yourself or explain what Xerox sells")
    lines.append("  - 'Plant the seed' or build awareness")
    lines.append("  - Explain why you're reaching out")
    lines.append("  - Reference CT Secretary of State filings")
    lines.append("")
    lines.append("You DO need to:")
    lines.append("  - Reference their specific equipment model naturally")
    lines.append("  - Mention the contract expiration date informatively")
    lines.append("  - Include a soft meeting ask")
    lines.append("")
    lines.append("TONE: Warm, professional, service-oriented. Treat this like a")
    lines.append("conversation with someone you already have a relationship with.")
    lines.append("Not a sales pitch. Not a cold email.")
    lines.append("")
    lines.append("DO NOT write any of these — they read as manufactured urgency:")
    lines.append('  - "Act now" / "Act soon"')
    lines.append('  - "Don\'t miss out"')
    lines.append('  - "About to expire"')
    lines.append('  - "Time is running out"')
    lines.append('  - "Limited time"')
    lines.append('  - "Last chance"')
    lines.append("")
    lines.append("DO NOT write cold-outreach intro language:")
    lines.append('  - "I saw your filing"')
    lines.append('  - "I read about your business"')
    lines.append('  - "Reaching out about your practice"')
    lines.append('  - "Wanted to introduce myself"')
    lines.append('  - "Noticed your business"')
    lines.append("")

    # STEP 3: Lapsed-specific framing
    if classification in ("expired_critical", "expired_recent"):
        lines.extend(_build_lapsed_framing(lead))

    # STEP 4: Meter assessment prompt context (for XDA suggestions)
    if meter_assessment["suggest_xda"]:
        lines.append("--- REMOTE MONITORING OPPORTUNITY ---")
        lines.append(meter_assessment["prompt_context"])
        lines.append("")

    # STEP 5: Customer profile
    lines.append("")
    lines.append("--- CUSTOMER PROFILE ---")
    lines.append(f"  Business: {ctx.get('name', '')}")

    greeting = ctx.get("greeting_name")
    if greeting:
        lines.append(f"  Greeting name: {greeting}")
    else:
        lines.append("  Greeting name: (none -- start email without salutation)")

    if equipment_model:
        lines.append(f"  Equipment model: {equipment_model}")
    if equipment_serial:
        lines.append(f"  Serial number: {equipment_serial}")
    lines.append(f"  Location: {ctx.get('city', '')}, {ctx.get('state', 'CT')}")

    mono = lead.get("equipment_mono_meter")
    color = lead.get("equipment_color_meter")
    if mono is not None or color is not None:
        if meter_assessment["meters_available"]:
            mono_str = f"{mono:,}" if mono else "0"
            color_str = f"{color:,}" if color else "0"
            lines.append(f"  Meter reads: {mono_str} mono / {color_str} color")
        else:
            lines.append("  Meter reads: (not available — remote monitoring should be set up)")

    if contract_expiration:
        lines.append(f"  Contract expiration: {contract_expiration[:10]}")
    if days_until is not None:
        lines.append(f"  Days until expiration: {days_until}")

    if meter_assessment["meters_available"] and meter_insight:
        lines.append(f"  Usage insight: {meter_insight}")
    elif not meter_assessment["meters_available"]:
        lines.append(f"  Usage insight: {meter_assessment['meter_insight']}")

    if upgrade:
        lines.append(f"  Recommended upgrade: {upgrade}")
    if upgrade_reason:
        lines.append(f"  Upgrade reason: {upgrade_reason}")
    lines.append("")

    # Operator intel
    intel_section = _build_intel_section(lead, batch_intel=batch_intel)
    if intel_section:
        for line in intel_section.strip().split("\n"):
            lines.append(line)
        lines.append("")

    # Signature
    lines.extend([
        "SIGNATURE - include EXACTLY:",
        SIGNATURE,
        "",
        "LENGTH: 80-110 words. Signature block does NOT count.",
        "",
        "Generate ONLY the email. Start with 'SUBJECT:' on first line,",
        "then blank line, then body. No preamble.",
    ])

    return "\n".join(lines)


def draft_batch(lead_ids: List[str], prompt_variant: Optional[str] = None) -> Dict:
    """Draft for multiple leads. Returns counts and errors.

    Args:
        lead_ids: List of lead IDs
        prompt_variant: Optional variant (ignored)

    Returns:
        Dict with drafted_count, error_count, errors
    """
    result = {
        "drafted_count": 0,
        "error_count": 0,
        "errors": [],
    }

    for lead_id in lead_ids:
        try:
            draft_touch(lead_id, prompt_variant)
            result["drafted_count"] += 1
        except Exception as e:
            result["error_count"] += 1
            result["errors"].append(f"{lead_id}: {str(e)}")

    return result
