#!/usr/bin/env python3
"""LimeChat MBR Server v7 — CRM-enriched AI analysis.

Production-ready: all credentials are sourced from environment variables and
the process fails fast at startup if any required key is missing. Adds a
``/health`` endpoint for Kubernetes readiness probes and reads ``PORT`` from
the environment so the container can be probed at the Runtime Contract port.
"""

import http.server
import json
import os
import socketserver
import sys
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

# ── Required at startup — fail loudly if missing ─────────────────────────────
PORT          = int(os.environ.get("PORT", "8000"))
SERVE_DIR     = Path(__file__).parent
METABASE_KEY  = os.environ["METABASE_KEY"]
LC_API_TOKEN  = os.environ["LC_API_TOKEN"]
GROQ_KEY      = os.environ["GROQ_KEY"]

# ── Optional providers — empty string disables the corresponding code path ──
ANTHROPIC_KEY = os.environ.get("ANTHROPIC_KEY", "")
GEMINI_KEY    = os.environ.get("GEMINI_KEY", "")
SMTP_EMAIL    = os.environ.get("SMTP_EMAIL", "")
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")

# ── Model names / endpoints — config, not secrets ───────────────────────────
GROQ_MODEL    = os.environ.get("GROQ_MODEL", "llama-3.3-70b-versatile")
GEMINI_MODEL  = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash")
CRM_BASE      = os.environ.get("CRM_BASE", "https://app.limechat.ai")

LIMECHAT_KNOWLEDGE = """
LimeChat Platform — Complete Product & Actionable Knowledge

SUPPORT / BOT:
- Level 3 AI BOT: Automates up to 90% of customer queries on WhatsApp/Instagram 24/7.
  Target automation rate: >70% excellent | 55-70% good | <55% needs immediate attention.
  Fix low automation: Open Bot Overview → check top fallback intents → retrain utterances → add quick-reply buttons for top intents.
- Agentic Bot Builder: No-code visual builder. Can upload PDFs, CSVs, DOCX, URLs as Knowledge Base.
  Use for: adding product FAQs, brand SOPs, return policies without engineering.
- AI Copilot for Agents: Drafts responses, multilingual translation, smart bot-to-human handoff.
  When to recommend: FRT is high, agent CSAT is low, or agents handling repetitive queries.
- AI Audits & Analytics: Audits 100% of human + AI conversations. Utterance report shows exact queries bot failed on.
  CSM can request Bot Audit Report or Agent Audit Report add-on for deeper insights.

HELPDESK / CRM:
- CSAT Management: Agent CSAT and Bot CSAT tracked separately. BOTH must be enabled independently.
  Configurable: 2/3/5-point scale, auto-reminders post-resolution, AMP-powered email CSAT.
  Fix low CSAT: Audit all tickets rated <3 this week → identify patterns → retrain bot or coach agents.
  Fix missing CSAT: Enable in CRM settings → configure trigger (post-resolution) → set reminder.
- Canned Responses / Templates: Pre-written replies for top query types. Fastest FRT fix.
  When to recommend: FRT > 10 min, repetitive greeting queries, high agent load.
- SLA Monitoring: Set response time targets, track per-agent performance weekly.
- Smart Routing: Route queries to specialist agents by product category or query type.
  When to recommend: Multiple product lines, quality issues in specific category, FRT varies by type.
- Omnichannel CRM: WhatsApp, Instagram, FB, email in one inbox.
  Order management: cancel, refund, address change, add items directly from helpdesk (no Shopify needed).

MARKETING / FLOWS:
- WhatsApp Broadcasts: 2-way campaigns, up to 4 audience segments merged per broadcast, Gen AI content.
  Typical ROI: 3-5x. If ₹0 broadcast revenue → channel completely untapped, highest priority to activate.
  Fix low engagement: Re-segment audience, refresh creative, test new CTA, add re-engagement hooks.
  Fix missing broadcasts: Start with 1 segment (e.g. past buyers of top product) → schedule monthly.
- Automation Flows (Flow Builder):
  * Abandoned Cart Flow: Trigger 1-2h after cart drop → recover 15-25% of lost orders.
  * WISMO / Post-Purchase Flow: Proactive order status updates → reduce WISMO queries by 30-50%.
  * COD Confirmation Flow: Verify COD intent → reduce RTO (Return to Origin) by 20-40%.
  * COD→Prepaid Conversion Flow: Incentivize prepaid → reduce logistics risk and costs.
  * Cancel Order Intervention Flow: Intercept cancellation → offer alternative/discount before confirming.
  * Reorder / Replenishment Flow: Trigger 20-30 days post-purchase → drives repeat revenue for consumables.
  * Post-Purchase Cross-Sell Flow: After order confirmed → recommend complementary product.
  * Welcome / Onboarding Flow: New subscriber → brand education + first purchase nudge.
  * Complaint / Delay Notification Flow: Proactively notify about delays → reduce inbound complaint volume.
- Customer Segmentation: Segment by purchase history, behavior, product interest, creation date.
  Use for: targeted broadcasts, personalized flows, revival campaigns for dormant users.

RTO REDUCTION:
- COD Verification Flow: Automated WhatsApp message to confirm COD order intent.
- NDR Management: Manage Non-Delivery Reports to reduce logistics losses.
  When to recommend: High cancellation rate, high RTO complaints, COD order volume significant.

EXPANSION / UPSELL FEATURES:
- Voice AI (WhatsApp + Phone): Lead recovery, cart conversion, instant query resolution via voice.
  When to recommend: Ticket volume >5,000/month, brand gets many phone/voice inquiries.
- Instagram Suite: Automate DMs, Stories, Comments. Drive Instagram commerce.
  When to recommend: Brand has active Instagram presence, high comment/DM volume.
- Click-to-WhatsApp Ads (CTWA): Direct ad traffic to WhatsApp → lower CAC, better RoAS.
  When to recommend: Brand running Meta ads, wants better lead-to-purchase conversion.
- AI-Powered Conversation Audit: Audit 100% of human + AI interactions for quality.
  When to recommend: Agent performance inconsistent, CSAT variance high.
- Email/SMS Marketing: Extend campaigns beyond WhatsApp.

BENCHMARKS (what "good" looks like at LimeChat):
- Bot automation: >70% excellent | 55-70% acceptable | <55% broken
- Agent CSAT: >4.0/5 good | 3.5-4.0 watch | <3.5 urgent fix
- Bot CSAT: >4.0/5 good | 0 = not enabled (fix this first)
- FRT: <5 min excellent | <15 min acceptable | >30 min critical
- Broadcast ROI: >5x excellent | 3-5x good | <2x refresh creative/segments
- Bot-attributed revenue: Should grow MoM with good automation

PROBLEM → ACTIONABLE MAPPING (use these when you detect a signal):
- CSAT dropping → Audit low-CSAT tickets, improve fallback messaging, train BOT, introduce escalation SOP
- Bot automation falling → Retrain on repetitive queries, add FAQs/buttons, optimize flows for top intents (cancel, complaint, refund)
- Campaign revenue declining → Refresh creatives, test CTAs, re-segment, scale proven flows, add UTMs, introduce new journeys
- Ticket spikes / high FRT → Add canned responses, balance queues, set SLA targets, shift queries to BOT via flows
- Conversion dropping → Audit drop-offs, add nudges, optimize checkout flow in BOT, trigger to marketing platform
- Rising complaints/refunds → Analyze root causes, set up automated complaint handling flows, request BOT audit report
- Marketing gaps → Launch new flows (welcome, post-purchase, upsell), optimize broadcast cadence, run revival campaigns
- Bot CSAT stuck low → Improve response copy, audit utterance report, add smart nudges in negative paths, align BOT tone to brand voice
- Broadcast frequency low → Restore weekly/monthly cadence by segment, add seasonal/launch campaigns
- Delivery complaints → Build delayed delivery notification flow; WISMO flow for proactive updates
- Category complaints → Add FAQ + quick-reply button for that category; broadcast to reassure buyers
"""


class MBRHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *a, **kw):
        super().__init__(*a, directory=str(SERVE_DIR), **kw)

    def do_GET(self):
        # Runtime Contract: health endpoint for Kubernetes readiness probes.
        if self.path == "/health" or self.path == "/healthz":
            self._json({"status": "ok"})
            return
        if self.path.startswith('/lc-api/'):
            self.handle_lc_api()
        else:
            super().do_GET()

    def do_POST(self):
        p = self.path
        if   p.startswith('/metabase/'):   self.handle_metabase()
        elif p.startswith('/lc-api/'):     self.handle_lc_api()
        elif p == '/claude-full':          self.handle_claude_full()
        elif p == '/crm-sample':           self.handle_crm_sample()
        elif p == '/send-email':           self.handle_send_email()
        else: self.send_error(404)

    def do_OPTIONS(self):
        self.send_response(200); self.end_headers()

    # ── METABASE ──────────────────────────────────────────────────────────────
    def handle_metabase(self):
        try:
            card_id = self.path.split('/')[2]
            body = self._read_body()
            req = urllib.request.Request(
                f"https://metabase.limechat.ai/api/card/{card_id}/query",
                data=body,
                headers={"Content-Type": "application/json",
                         "X-API-KEY": METABASE_KEY,
                         "Cache-Control": "no-store"}
            )
            with urllib.request.urlopen(req, timeout=30) as r:
                self._raw(r.read(), 'application/json')
        except urllib.error.HTTPError as e:
            self._raw(e.read(), 'application/json', e.code)
        except Exception as e:
            self._json({"error": str(e)}, 500)

    # ── LIMECHAT API ──────────────────────────────────────────────────────────
    def handle_lc_api(self):
        try:
            lc_path = self.path[len('/lc-api'):]
            body = self._read_body() or None
            req = urllib.request.Request(
                f"https://app.limechat.ai{lc_path}",
                data=body,
                method="POST" if body else "GET",
                headers={"Content-Type": "application/json",
                         "api_access_token": LC_API_TOKEN,
                         "Cache-Control": "no-store"}
            )
            with urllib.request.urlopen(req, timeout=25) as r:
                self._raw(r.read(), 'application/json')
        except urllib.error.HTTPError as e:
            self._raw(e.read(), 'application/json', e.code)
        except Exception as e:
            self._json({"error": str(e)}, 500)

    # ── CLAUDE FULL (web search + one structured call) ────────────────────────
    def handle_claude_full(self):
        try:
            body = json.loads(self._read_body())
            client_name = body.get('clientName', '')
            month1      = body.get('m1Str', '')
            month2      = body.get('m2Str', '')

            data = {
                'month1': body.get('month1Data') or {},
                'month2': body.get('month2Data') or {},
                'intent':        body.get('intent', []),
                'intent_m1':     body.get('intent_m1', []),
                'complaints':    body.get('complaints', []),
                'complaints_m1': body.get('complaints_m1', []),
                'concerns':      body.get('concerns', []),
                'concerns_m1':   body.get('concerns_m1', []),
                'products':      body.get('products', []),
                'campaigns':     body.get('campaigns', []),
                'flows':         body.get('flows', []),
            }
            crm_data = body.get('crmEnrichment') or None
            prompt = self._build_prompt(client_name, month1, month2, data, crm_data)
            result = self._claude_call(prompt, max_tokens=5000)
            self._json(result)
        except Exception as e:
            print(f"  ❌ Claude full error: {e}")
            self._json({"error": str(e)}, 500)

    # ── CRM SAMPLE (Auth0-authenticated conversation sampling) ────────────────
    def handle_crm_sample(self):
        try:
            body = json.loads(self._read_body())
            account_id = body.get('accountId')
            token      = body.get('token')
            if not account_id or not token:
                self._json({'error': 'Missing accountId or token',
                            'lowCsatVerbatims': [], 'botHandoffUtterances': [], 'topComplaintMessages': []})
                return
            print(f"  🔍 CRM sample: account {account_id}")
            result = self._fetch_crm_enrichment(account_id, token)
            print(f"  ✅ CRM: {len(result['lowCsatVerbatims'])} CSAT, "
                  f"{len(result['botHandoffUtterances'])} handoffs, "
                  f"{len(result['topComplaintMessages'])} complaints")
            self._json(result)
        except Exception as e:
            print(f"  ❌ CRM sample error: {e}")
            self._json({'error': str(e), 'lowCsatVerbatims': [], 'botHandoffUtterances': [], 'topComplaintMessages': []})

    def _crm_get(self, path, token):
        req = urllib.request.Request(
            CRM_BASE + path,
            headers={
                'accept': 'application/json',
                'authorization': token,
                'x-app-platform-type': 'browser'
            }
        )
        with urllib.request.urlopen(req, timeout=12) as r:
            return json.loads(r.read())

    def _fetch_crm_enrichment(self, account_id, token):
        convs = []
        for page in [1, 2]:
            try:
                resp = self._crm_get(
                    f'/api/v1/accounts/{account_id}/conversations'
                    f'?status=resolved&assignee_type=all&page={page}&sort_by=last_activity_at&oldest=false',
                    token
                )
                page_convs = resp.get('data', {}).get('payload', [])
                convs.extend(page_convs)
                if len(page_convs) < 25:
                    break
            except Exception as e:
                print(f"    ⚠️  CRM page {page} failed: {e}")
                break

        if not convs:
            return {'lowCsatVerbatims': [], 'botHandoffUtterances': [], 'topComplaintMessages': []}

        sample = convs[:40]

        def fetch_msgs(conv):
            cid = conv.get('id')
            if not cid:
                return cid, []
            try:
                resp = self._crm_get(
                    f'/api/v1/accounts/{account_id}/conversations/{cid}/messages',
                    token
                )
                return cid, resp.get('payload', [])
            except Exception:
                return cid, []

        conv_messages = {}
        with ThreadPoolExecutor(max_workers=5) as ex:
            futures = {ex.submit(fetch_msgs, c): c for c in sample}
            for f in as_completed(futures, timeout=45):
                try:
                    cid, msgs = f.result()
                    conv_messages[cid] = msgs
                except Exception:
                    pass

        low_csat, handoff_triggers, complaint_msgs = [], [], []
        COMPLAINT_KW = ['wrong', 'defective', 'damaged', 'not received', "haven't received",
                        'not delivered', 'cancel', 'refund', 'missing', 'broken', 'leaking',
                        'expired', 'fake', 'pathetic', 'worst', 'terrible', 'unacceptable',
                        'fraud', 'cheating', 'rotten', 'spoiled']

        for conv in sample:
            cid  = conv.get('id')
            msgs = conv_messages.get(cid, [])
            if not msgs:
                continue
            msgs_sorted = sorted(msgs, key=lambda m: m.get('created_at', 0))

            for msg in msgs_sorted:
                ctype = msg.get('content_type', '')
                if ctype == 'input_csat':
                    attrs = msg.get('content_attributes', {})
                    sv    = (attrs.get('submitted_values') or {})
                    csat_resp = sv.get('csat_survey_response', {})
                    if isinstance(csat_resp, dict):
                        rating   = csat_resp.get('rating', 5)
                        feedback = (csat_resp.get('feedback_message') or '').strip()
                        if rating <= 2 and feedback and len(feedback) > 5:
                            low_csat.append(f'[{rating}/5] "{feedback}"')

            for i, msg in enumerate(msgs_sorted):
                if msg.get('message_type') == 2:
                    activity_content = (msg.get('content') or '').lower()
                    if 'bot_hand_off' in activity_content or 'bot added bot_hand_off' in activity_content:
                        for j in range(i - 1, -1, -1):
                            prev = msgs_sorted[j]
                            if prev.get('message_type') == 0:
                                text = (prev.get('content') or '').strip()
                                if text and 10 < len(text) < 500:
                                    handoff_triggers.append(f'"{text}"')
                                break

            for msg in msgs_sorted:
                if msg.get('message_type') == 0:
                    text = (msg.get('content') or '').strip()
                    if text and len(text) > 20:
                        text_lower = text.lower()
                        if any(kw in text_lower for kw in COMPLAINT_KW):
                            complaint_msgs.append(f'"{text[:250]}"')

        def dedup(lst, limit):
            seen, out = set(), []
            for item in lst:
                key = item[:80]
                if key not in seen:
                    seen.add(key)
                    out.append(item)
                if len(out) >= limit:
                    break
            return out

        return {
            'lowCsatVerbatims':     dedup(low_csat, 8),
            'botHandoffUtterances': dedup(handoff_triggers, 12),
            'topComplaintMessages': dedup(complaint_msgs, 10),
        }

    def _diagnose_metrics(self, m1, m2, d):
        broken, unused, working = [], [], []

        def val(data, key):
            v = data.get(key)
            if v is None or v == '': return None
            try: return float(v)
            except Exception: return None

        def pct(curr, prev):
            try:
                c, p = float(curr), float(prev)
                if p == 0: return None
                return ((c - p) / p) * 100
            except Exception: return None

        frt1,  frt2  = val(m1, 'frt'),          val(m2, 'frt')
        csat1, csat2 = val(m1, 'agentCsat'),     val(m2, 'agentCsat')
        bcsat2       = val(m2, 'botCsat')
        bot1,  bot2  = val(m1, 'botAutomation'), val(m2, 'botAutomation')
        brev2        = val(m2, 'broadcastRev')
        frev2        = val(m2, 'flowRev')
        tick1, tick2 = val(m1, 'totalTickets'),  val(m2, 'totalTickets')
        intents    = d.get('intent', [])
        complaints = d.get('complaints', [])
        flows      = d.get('flows', [])
        active_flows = list({r[4].lower() for r in flows if len(r) > 4 and r[4]})

        if frt2:
            if frt2 > 30:
                broken.append(f"FRT critical at {frt2:.0f} min (target <5 min) — customers waiting too long for first response")
            elif frt2 > 10:
                broken.append(f"FRT high at {frt2:.0f} min (target <5 min) — agents need canned responses + triaging improvement")
            elif frt1 and pct(frt2, frt1) and pct(frt2, frt1) > 20:
                broken.append(f"FRT worsened {pct(frt2,frt1):.0f}% MoM: {frt1:.0f} → {frt2:.0f} min — add canned responses for top query types")
            else:
                working.append(f"FRT healthy at {frt2:.0f} min")

        if not csat2 or csat2 == 0:
            unused.append("Agent CSAT not enabled — zero visibility into satisfaction; enable in CRM Settings → configure post-resolution trigger")
        elif csat2 < 3.5:
            broken.append(f"Agent CSAT low: {csat2:.1f}/5 — audit all tickets rated <3 this week, identify dissatisfaction drivers, introduce escalation SOP")
            if csat1 and csat2 < csat1:
                broken.append(f"Agent CSAT declining: {csat1:.1f} → {csat2:.1f}/5 MoM — coach agents, add human fallback for sensitive flows")
        elif csat2 >= 4.0:
            working.append(f"Agent CSAT strong: {csat2:.1f}/5")

        if not bcsat2 or bcsat2 == 0:
            unused.append("Bot CSAT not configured — enable separately from agent CSAT in Bot Settings to measure bot response quality")
        elif bcsat2 < 3.5:
            broken.append(f"Bot CSAT low: {bcsat2:.1f}/5 — audit utterance + CSAT report, improve response copy, add smart nudges in negative feedback paths, align bot tone to brand voice")

        if bot2 is not None:
            if bot2 < 55:
                broken.append(f"Bot automation low: {bot2:.1f}% (target >70%) — open Bot Overview → check top fallback intents → retrain utterances → add FAQs and quick-reply buttons")
            elif bot2 < 70:
                broken.append(f"Bot automation at {bot2:.1f}% (target >70%) — audit top unresolved intents; add quick-reply buttons for repeat queries")
            else:
                working.append(f"Bot automation strong: {bot2:.1f}%")
            chg = pct(bot2, bot1)
            if chg and chg < -10:
                broken.append(f"Bot automation declining {abs(chg):.0f}% MoM ({bot1:.1f}% → {bot2:.1f}%) — investigate recent flow changes, retrain on new query patterns")

        if not brev2 or brev2 == 0:
            unused.append("WhatsApp Broadcasts: ₹0 revenue — marketing channel completely untapped; start with 1 segment (past buyers of top product) → typical ROI 3-5x")
        else:
            working.append(f"Broadcasts generating revenue: ₹{brev2:,.0f}")
            chg = pct(brev2, val(m1,'broadcastRev'))
            if chg and chg < -20:
                broken.append(f"Broadcast revenue declined {abs(chg):.0f}% MoM — refresh creatives, re-segment audience, test stronger CTA, restore send cadence")

        if not frev2 or frev2 == 0:
            if not active_flows:
                unused.append("Automation Flows: none active, ₹0 revenue — highest impact first: build Abandoned Cart flow (recovers 15-25% of lost orders) or WISMO flow (reduces 30-50% of track order queries)")
            else:
                broken.append(f"Flows active ({', '.join(active_flows[:3])}) but ₹0 revenue — check flow triggers, attribution setup, and delivery rates in Flow Builder")

        for r in intents:
            if len(r) >= 3 and r[1]:
                name_l = r[1].lower()
                count  = int(r[2]) if r[2] else 0
                if count > 50 and any(kw in name_l for kw in ['track','order status','where is','wismo','delivery status']):
                    has_flow = any(kw in ' '.join(active_flows) for kw in ['wismo','post purchase','post-purchase','order track'])
                    if not has_flow:
                        broken.append(f"'{r[1]}' intent: {count:,} sessions/month with no WISMO flow — post-purchase flow reduces this by 30-50%")
                    break

        chg_t = pct(tick2, tick1)
        if chg_t and chg_t > 20 and bot2 and bot2 < 70:
            broken.append(f"Ticket volume up {chg_t:.0f}% MoM ({int(tick1):,} → {int(tick2):,}) while bot is below target — shift more volume to bot via proactive flows")

        for r in complaints[:3]:
            if len(r) >= 3 and r[1] and int(r[2] or 0) > 5:
                name, count = r[1], int(r[2])
                nl = name.lower()
                if any(kw in nl for kw in ['delay','not deliver','not received','pending']):
                    broken.append(f"Delivery complaints: '{name}' = {count} cases — build Delay Notification flow in Flow Builder to proactively update customers and reduce inbound complaint volume")
                elif any(kw in nl for kw in ['wrong','damage','defect','quality','different']):
                    broken.append(f"Product quality complaints: '{name}' = {count} cases — add FAQ entry + quick-reply button in bot; flag to ops team for root cause")
                elif any(kw in nl for kw in ['cancel','refund','return']):
                    broken.append(f"Cancel/refund complaints: '{name}' = {count} cases — build Cancel Order Intervention flow to offer retention discount before confirming cancellation")
                else:
                    broken.append(f"Top complaint '{name}': {count} cases — add FAQ + quick-reply button in bot; build automated response flow")
                if len(broken) >= 7: break

        if tick2 and tick2 > 5000:
            unused.append(f"High ticket volume ({int(tick2):,}/month) — consider Voice AI for call deflection; IG Suite if brand has active Instagram presence")

        return {'broken': broken[:6], 'unused': unused[:3], 'working': working[:3]}

    def _build_prompt(self, client_name, month1, month2, d, crm_data=None):
        def fmt_num(v, prefix='', suffix=''):
            if v is None or v == '' or v == 0: return 'N/A'
            try:
                n = float(v)
                if n >= 100000: return f"{prefix}₹{n/100000:.2f}L{suffix}" if prefix == '₹' else f"{prefix}{n:,.0f}{suffix}"
                return f"{prefix}{n:,.0f}{suffix}"
            except Exception: return str(v)

        def pct_change(curr, prev):
            try:
                c, p = float(curr), float(prev)
                if p == 0: return 'New'
                chg = ((c - p) / p) * 100
                sign = '+' if chg >= 0 else ''
                return f"{sign}{chg:.1f}%"
            except Exception: return 'N/A'

        def row(label, v1, v2, currency=False, reverse=False):
            p = '₹' if currency else ''
            chg = pct_change(v2, v1)
            direction = ''
            try:
                c, pr = float(v2), float(v1)
                up = c > pr
                good = up if not reverse else not up
                direction = ' ✅' if good else ' ⚠️'
            except Exception: pass
            v1_fmt = fmt_num(v1, p) if currency else (f"{v1:.2f}" if isinstance(v1, float) else str(v1 or 'N/A'))
            v2_fmt = fmt_num(v2, p) if currency else (f"{v2:.2f}" if isinstance(v2, float) else str(v2 or 'N/A'))
            return f"  {label}: {v1_fmt} → {v2_fmt} ({chg}{direction})"

        def _n(v, default=0):
            try: return float(v) if v not in (None, '', 'N/A') else default
            except Exception: return default

        m1 = d.get('month1', {})
        m2 = d.get('month2', {})

        campaigns = d.get('campaigns', [])
        camp_text = ''
        if campaigns:
            camp_lines = []
            for c in campaigns[:8]:
                name = c[0] if len(c) > 0 else 'Unknown'
                sent = _n(c[1] if len(c) > 1 else 0)
                ctr  = _n(c[2] if len(c) > 2 else 0)
                rev  = _n(c[3] if len(c) > 3 else 0)
                camp_lines.append(f"  - {name}: {sent:,.0f} sent, CTR {ctr:.1f}%, Rev ₹{rev:,.0f}")
            camp_text = '\n'.join(camp_lines)

        intent = d.get('intent', [])
        intent_m1_map = {r[1]: r[2] for r in d.get('intent_m1', []) if len(r) >= 3}
        intent_text = ''
        if intent:
            lines = []
            for r in intent[:15]:
                if len(r) >= 3 and r[1]:
                    prev = intent_m1_map.get(r[1], 0)
                    chg = pct_change(r[2], prev) if prev else 'New'
                    lines.append(f"  - {r[1]}: {int(_n(r[2])):,} sessions ({chg} vs {month1})")
            intent_text = '\n'.join(lines)

        concerns = d.get('concerns', [])
        concerns_m1 = {r[1]: r[2] for r in d.get('concerns_m1', []) if len(r) >= 3}
        concern_text = ''
        if concerns:
            lines = []
            for r in concerns[:10]:
                if len(r) >= 3:
                    prev_val = concerns_m1.get(r[1], 0)
                    chg = pct_change(r[2], prev_val) if prev_val else 'New'
                    lines.append(f"  - {r[1]}: {int(_n(r[2])):,} sessions ({chg} vs {month1})")
            concern_text = '\n'.join(lines)

        complaints   = d.get('complaints', [])
        complaints_m1 = {r[1]: r[2] for r in d.get('complaints_m1', []) if len(r) >= 3}
        complaint_text = ''
        if complaints:
            lines = []
            for r in complaints[:10]:
                if len(r) >= 3:
                    prev_val = complaints_m1.get(r[1], 0)
                    chg = pct_change(r[2], prev_val) if prev_val else 'New'
                    lines.append(f"  - {r[1]}: {int(_n(r[2])):,} ({chg})")
            complaint_text = '\n'.join(lines)

        products = d.get('products', [])
        product_text = '\n'.join(f"  - {r[2]}: {int(_n(r[4])):,} units, ₹{_n(r[3]):,.0f}" for r in products[:8] if len(r) >= 5) or 'No product data.'

        flows = d.get('flows', [])
        flow_names = list({r[4] for r in flows if len(r) > 4 and r[4]})
        flow_text = ', '.join(flow_names[:15]) or 'None active'

        diagnosis = self._diagnose_metrics(m1, m2, d)
        broken_text  = '\n'.join(f"  🔴 {b}" for b in diagnosis['broken'])  or "  (No critical issues detected)"
        unused_text  = '\n'.join(f"  🟡 {u}" for u in diagnosis['unused'])  or "  (All core features appear active)"
        working_text = '\n'.join(f"  ✅ {w}" for w in diagnosis['working']) or ""

        return f"""You are a senior LimeChat Customer Success Manager writing a Monthly Business Review for {client_name}.
Use your knowledge about {client_name} (brand, products, category) to make insights specific and relevant.

PERIOD: {month1} (previous) vs {month2} (current/reporting month)
ACTIVE FLOWS: {flow_text}

=== RAW METRICS ===
HELPDESK:
{row('Total Tickets', m1.get('totalTickets'), m2.get('totalTickets'))}
{row('Active Agents', m1.get('activeAgents'), m2.get('activeAgents'), reverse=True)}
{row('First Response Time (min)', m1.get('frt'), m2.get('frt'), reverse=True)}
{row('Resolution Time (min)', m1.get('resolutionTime'), m2.get('resolutionTime'), reverse=True)}
{row('Agent CSAT', m1.get('agentCsat'), m2.get('agentCsat'))}

BOT:
{row('Bot Automation %', m1.get('botAutomation'), m2.get('botAutomation'))}
{row('Bot Tickets Handled', m1.get('botTickets'), m2.get('botTickets'))}
{row('Bot CSAT', m1.get('botCsat'), m2.get('botCsat'))}

REVENUE:
{row('Flow Revenue', m1.get('flowRev'), m2.get('flowRev'), currency=True)}
{row('Broadcast Revenue', m1.get('broadcastRev'), m2.get('broadcastRev'), currency=True)}
{row('Total Revenue', m1.get('totalRev'), m2.get('totalRev'), currency=True)}
{row('Bot Direct Revenue', m1.get('botRev'), m2.get('botRev'), currency=True)}
{row('Pre-Purchase Sessions', m1.get('prePurchaseSessions'), m2.get('prePurchaseSessions'))}

CAMPAIGNS ({month2}):
{camp_text or 'No campaign data.'}

USER INTENT / INTERESTS:
{intent_text or 'No intent data.'}

PRODUCT CONCERNS:
{concern_text or 'No concern data.'}

PRODUCT-WISE SALES:
{product_text}

COMPLAINTS:
{complaint_text or 'No complaint data.'}
{self._fmt_crm_section(crm_data)}
=== PRE-ANALYSIS: WHAT THE DATA SAYS ===
🔴 WHAT'S NOT WORKING / NEEDS FIXING:
{broken_text}

🟡 WHAT'S UNUSED (LimeChat features never activated for this brand):
{unused_text}

✅ WHAT'S WORKING WELL:
{working_text}

=== LIMECHAT PLATFORM KNOWLEDGE ===
{LIMECHAT_KNOWLEDGE}

=== YOUR TASK ===
Write a complete, professional MBR for {client_name}. Customer-facing tone. No generic advice — every sentence must reference a specific number or observation from the data above.

OUTPUT FORMAT (return ONLY this JSON, no other text):
{{
  "executiveSummary": "3-4 paragraphs as a single string with \\n\\n between them. Lead with the biggest win, then biggest risk, then revenue story, then what to watch next month.",
  "keyObservations": {{
    "helpdesk": "2-3 sentences.",
    "bot": "2-3 sentences.",
    "revenue": "2-3 sentences.",
    "intent": "2-3 sentences.",
    "complaints": "2-3 sentences."
  }},
  "nextSteps": [
    {{ "section": "fix", "action": "Verb-led title (max 10 words)", "detail": "2-3 sentences citing actual numbers + exact LimeChat menu paths + outcome.", "priority": "high" }},
    {{ "section": "activate", "action": "Verb-led title (max 10 words)", "detail": "2-3 sentences with missed value + setup steps + outcome.", "priority": "medium" }}
  ]
}}

Generate 3 fix steps and 2 activate steps. Order by urgency. Reference CRM verbatims in at least 1 fix step if present.
Return only the JSON object."""

    def _fmt_crm_section(self, crm_data):
        if not crm_data:
            return ''
        low_csat   = crm_data.get('lowCsatVerbatims', [])
        handoffs   = crm_data.get('botHandoffUtterances', [])
        complaints = crm_data.get('topComplaintMessages', [])
        if not any([low_csat, handoffs, complaints]):
            return ''
        lines = ['\n=== REAL CUSTOMER VERBATIMS (sampled from actual conversations this month) ===',
                 'USE THESE to make insights and next steps specific and quotable.\n']
        if low_csat:
            lines.append('CUSTOMERS WHO RATED SUPPORT 1-2 STARS (verbatim feedback):')
            lines.extend(f'  {v}' for v in low_csat)
            lines.append('')
        if handoffs:
            lines.append('EXACT QUERIES THAT CAUSED BOT-TO-AGENT HANDOFFS:')
            lines.extend(f'  {v}' for v in handoffs)
            lines.append('')
        if complaints:
            lines.append('ACTUAL CUSTOMER COMPLAINT MESSAGES (verbatim):')
            lines.extend(f'  {v}' for v in complaints)
            lines.append('')
        lines.append('=== END VERBATIMS ===\n')
        return '\n'.join(lines)

    def _claude_call(self, prompt, max_tokens=4000):
        text = self._groq_request(prompt, max_tokens=max_tokens)
        if '```json' in text: text = text.split('```json')[1].split('```')[0].strip()
        elif '```' in text:   text = text.split('```')[1].split('```')[0].strip()
        start = text.find('{')
        if start >= 0: text = text[start:text.rfind('}')+1]
        return json.loads(text)

    def _gemini_request(self, prompt, max_tokens=4000, timeout=120):
        if not GEMINI_KEY:
            raise RuntimeError("GEMINI_KEY not set in environment")
        url = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent?key={GEMINI_KEY}"
        payload = {
            "contents": [{"parts": [{"text": prompt}]}],
            "generationConfig": {"maxOutputTokens": max_tokens, "temperature": 0.3},
        }
        req = urllib.request.Request(url, data=json.dumps(payload).encode(),
                                     headers={"Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=timeout) as r:
                data = json.loads(r.read())
                return data['candidates'][0]['content']['parts'][0]['text']
        except urllib.error.HTTPError as e:
            body = e.read().decode('utf-8', errors='replace')
            print(f"  ❌ Gemini {e.code} response: {body[:600]}")
            raise

    def _groq_request(self, prompt, max_tokens=4000, timeout=120):
        import http.client
        import ssl
        payload = {"model": GROQ_MODEL, "messages": [{"role": "user", "content": prompt}],
                   "max_tokens": max_tokens, "temperature": 0.3}
        body_bytes = json.dumps(payload).encode()
        ctx = ssl.create_default_context()
        conn = http.client.HTTPSConnection("api.groq.com", context=ctx, timeout=timeout)
        try:
            conn.request("POST", "/openai/v1/chat/completions", body=body_bytes, headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {GROQ_KEY}",
                "Accept": "application/json",
                "User-Agent": "python-httpx/0.27.0"
            })
            resp = conn.getresponse()
            raw = resp.read().decode('utf-8')
            if resp.status != 200:
                print(f"  ❌ Groq {resp.status} response: {raw[:600]}")
                raise Exception(f"Groq HTTP {resp.status}: {raw[:200]}")
            data = json.loads(raw)
            return data['choices'][0]['message']['content']
        finally:
            conn.close()

    def _anthropic_request(self, payload, timeout=90):
        if not ANTHROPIC_KEY:
            raise RuntimeError("ANTHROPIC_KEY not set in environment")
        req = urllib.request.Request(
            "https://api.anthropic.com/v1/messages",
            data=json.dumps(payload).encode(),
            headers={"Content-Type": "application/json", "x-api-key": ANTHROPIC_KEY,
                     "anthropic-version": "2023-06-01"})
        try:
            with urllib.request.urlopen(req, timeout=timeout) as r:
                return json.loads(r.read())
        except urllib.error.HTTPError as e:
            body = e.read().decode('utf-8', errors='replace')
            print(f"  ❌ Anthropic {e.code} response: {body[:600]}")
            raise

    # ── EMAIL ─────────────────────────────────────────────────────────────────
    def handle_send_email(self):
        import base64
        import smtplib
        from email.mime.application import MIMEApplication
        from email.mime.multipart import MIMEMultipart
        from email.mime.text import MIMEText
        try:
            body = json.loads(self._read_body())
            if not SMTP_EMAIL or not SMTP_PASSWORD:
                self._json({"ok": False, "error": "Email not configured. Set SMTP_EMAIL and SMTP_PASSWORD env vars."})
                return
            msg = MIMEMultipart()
            msg['From']    = SMTP_EMAIL
            msg['To']      = body.get('to', '')
            msg['Subject'] = body.get('subject', 'LimeChat MBR')
            msg.attach(MIMEText(body.get('body', ''), 'html'))
            if body.get('pdf_base64'):
                pdf_bytes = base64.b64decode(body['pdf_base64'])
                att = MIMEApplication(pdf_bytes, _subtype='pdf')
                att.add_header('Content-Disposition', 'attachment', filename=body.get('pdf_filename', 'MBR.pdf'))
                msg.attach(att)
            with smtplib.SMTP_SSL('smtp.gmail.com', 465) as s:
                s.login(SMTP_EMAIL, SMTP_PASSWORD)
                s.sendmail(SMTP_EMAIL, body.get('to', ''), msg.as_string())
            self._json({"ok": True})
        except Exception as e:
            self._json({"ok": False, "error": str(e)}, 500)

    # ── HELPERS ───────────────────────────────────────────────────────────────
    def _read_body(self):
        length = int(self.headers.get('Content-Length', 0))
        return self.rfile.read(length) if length else b''

    def _json(self, data, status=200):
        resp = json.dumps(data).encode()
        self.send_response(status)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(resp)))
        self.end_headers(); self.wfile.write(resp)

    def _raw(self, data, ctype, status=200):
        self.send_response(status)
        self.send_header('Content-Type', ctype)
        self.send_header('Content-Length', str(len(data)))
        self.end_headers(); self.wfile.write(data)

    def end_headers(self):
        if hasattr(self, 'path') and self.path and ('.html' in self.path or self.path == '/'):
            self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
            self.send_header('Pragma', 'no-cache')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        super().end_headers()

    def log_message(self, fmt, *args):
        status = str(args[1]) if len(args) > 1 else ''
        path   = str(args[0]).split(' ')[1] if args and ' ' in str(args[0]) else ''
        if   '/metabase/' in path: print(f"  📊 Metabase {path.split('/')[2].split('?')[0]} → {status}")
        elif '/lc-api/'   in path: print(f"  🔌 LC API → {status}")
        elif path == '/claude-full':  print(f"  🧠 Claude AI → {status}")
        elif path == '/crm-sample':   print(f"  🔍 CRM sample → {status}")
        elif path == '/health':       pass  # quiet — probe spam
        elif path.endswith('.html'):  print(f"  🌐 {path} → {status}")


if __name__ == '__main__':
    socketserver.TCPServer.allow_reuse_address = True
    with socketserver.TCPServer(("", PORT), MBRHandler) as httpd:
        print(f"\n{'─'*55}")
        print(f"  🟢  LimeChat MBR Server v7 on port {PORT}")
        print(f"  📊  Open: http://localhost:{PORT}/limechat_mbr.html")
        print(f"  🧠  AI backend: Groq / {GROQ_MODEL}")
        print(f"{'─'*55}\n")
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print("\n  Stopped.")
            sys.exit(0)
