"""Smartlead cold-email control — in-process MCP so Jarvis runs Ahmed's outreach.

Smartlead (server.smartlead.ai) is the platform where Ahmed's cold-email
campaigns live — sender inboxes (warmed), campaigns, email sequences, the leads
in each campaign, and the reply/open analytics. This module gives Jarvis hands
on ALL of it by voice: report how campaigns are doing, spin one up, drop the
Ultron-scraped verified leads into it, write the follow-up sequence, and
start/pause it.

Auth: a single admin API key in .env as SMARTLEAD_API_KEY, passed as the
`?api_key=` query param on every call (Smartlead's scheme). MCP_SMARTLEAD=0
disables the whole server. No extra deps — stdlib urllib, same as the other
_control.py modules.

Tools (Claude sees them as mcp__smartlead__<name>):
  smartlead_campaigns        list campaigns + status
  smartlead_campaign_stats   analytics for one campaign (sent/open/reply/bounce)
  smartlead_senders          sender inboxes + warmup reputation/health
  smartlead_create_campaign  make a new campaign, returns its id
  smartlead_set_sequence     write the email steps/follow-ups for a campaign
  smartlead_add_leads        push leads (e.g. Ultron verified emails) into one
  smartlead_campaign_control start / pause / stop a campaign
  smartlead_api              generic passthrough for ANY other endpoint (full control)

The named tools cover the common voice actions; smartlead_api is the escape
hatch so "full control" really means full — any REST endpoint Smartlead exposes.
"""
from __future__ import annotations

import json
import os
import urllib.error
import urllib.request

_BASE = os.environ.get("SMARTLEAD_URL",
                       "https://server.smartlead.ai/api/v1").rstrip("/")
_KEY = os.environ.get("SMARTLEAD_API_KEY", "")


def enabled() -> bool:
    """On when a key is present and not explicitly disabled."""
    if os.environ.get("MCP_SMARTLEAD", "1") == "0":
        return False
    return bool(_KEY)


def _text(msg: str) -> dict:
    return {"content": [{"type": "text", "text": msg}]}


def _api(method: str, path: str, body: dict | list | None = None,
         timeout: int = 30):
    """One Smartlead REST call. api_key is appended as a query param. Raises
    RuntimeError with the server's message on a non-2xx so the tool can speak it."""
    if not path.startswith("/"):
        path = "/" + path
    sep = "&" if "?" in path else "?"
    url = f"{_BASE}{path}{sep}api_key={_KEY}"
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    # Smartlead is behind Cloudflare, which rejects the default "Python-urllib"
    # User-Agent with a 403 "error code: 1010". A normal browser UA sails through.
    req.add_header("User-Agent",
                   "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                   "AppleWebKit/537.36 (KHTML, like Gecko) "
                   "Chrome/124.0.0.0 Safari/537.36")
    req.add_header("Accept", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            raw = r.read()
    except urllib.error.HTTPError as e:
        detail = e.read()[:400].decode("utf-8", "ignore")
        raise RuntimeError(f"HTTP {e.code}: {detail}")
    return json.loads(raw) if raw else {}


def _campaign_id(ref: str) -> str | None:
    """Resolve a campaign id from an id or a (case-insensitive) name match."""
    ref = str(ref).strip()
    if not ref:
        return None
    if ref.isdigit():
        return ref
    try:
        for c in _api("GET", "/campaigns") or []:
            if str(c.get("name", "")).lower() == ref.lower():
                return str(c.get("id"))
        for c in _api("GET", "/campaigns") or []:  # loose contains-match
            if ref.lower() in str(c.get("name", "")).lower():
                return str(c.get("id"))
    except Exception:  # noqa: BLE001
        return None
    return None


def build_server():
    import asyncio
    from claude_agent_sdk import tool, create_sdk_mcp_server

    @tool("smartlead_campaigns",
          "List Ahmed's Smartlead cold-email campaigns with their status "
          "(DRAFTED/ACTIVE/PAUSED/COMPLETED). Use for 'how are my campaigns', "
          "'what campaigns do I have', 'is the outreach running'.", {})
    async def smartlead_campaigns(args: dict) -> dict:
        try:
            camps = await asyncio.to_thread(_api, "GET", "/campaigns")
            if not camps:
                return _text("No campaigns yet — the account has sender inboxes "
                             "ready but nothing is running.")
            lines = [f"- [{c.get('id')}] {c.get('name')!r} — {c.get('status')}"
                     for c in camps]
            return _text(f"{len(camps)} campaign(s):\n" + "\n".join(lines))
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_campaigns failed: {str(e)[:200]}")

    @tool("smartlead_campaign_stats",
          "Get performance analytics for ONE campaign: sent, opens, replies, "
          "bounces, unsubscribes. `campaign` = the id or the campaign name. Use "
          "for 'how is the <name> campaign doing', 'any replies on the outreach'.",
          {"campaign": str})
    async def smartlead_campaign_stats(args: dict) -> dict:
        cid = await asyncio.to_thread(_campaign_id, args.get("campaign", ""))
        if not cid:
            return _text("Couldn't find that campaign — try smartlead_campaigns "
                         "first to get the exact name or id.")
        try:
            a = await asyncio.to_thread(_api, "GET", f"/campaigns/{cid}/analytics")
            g = lambda k: a.get(k, 0)  # noqa: E731
            return _text(
                f"Campaign {a.get('name', cid)}: "
                f"sent {g('sent_count')}, opened {g('open_count')}, "
                f"replied {g('reply_count')}, bounced {g('bounce_count')}, "
                f"unsubscribed {g('unsubscribed_count')}, "
                f"leads {g('campaign_lead_stats') or g('total_count')}.")
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_campaign_stats failed: {str(e)[:200]}")

    @tool("smartlead_senders",
          "List the sender email inboxes and their warmup health (reputation, "
          "daily send limit). Use for 'how are my inboxes', 'are the emails "
          "warmed up', 'what's my sending reputation'.", {})
    async def smartlead_senders(args: dict) -> dict:
        try:
            accs = await asyncio.to_thread(
                _api, "GET", "/email-accounts/?offset=0&limit=100")
            if not accs:
                return _text("No sender inboxes configured.")
            lines = []
            for e in accs:
                wu = e.get("warmup_details") or {}
                lines.append(
                    f"- {e.get('from_email')} — reputation "
                    f"{wu.get('warmup_reputation', 'n/a')}, "
                    f"{e.get('message_per_day', '?')}/day, "
                    f"warmup {'on' if e.get('is_warmup_enabled') else 'off'}")
            return _text(f"{len(accs)} inbox(es):\n" + "\n".join(lines))
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_senders failed: {str(e)[:200]}")

    @tool("smartlead_create_campaign",
          "Create a new (empty, DRAFTED) campaign and return its id. Follow up "
          "with smartlead_set_sequence + smartlead_add_leads, then "
          "smartlead_campaign_control to start it. `name` is the campaign name.",
          {"name": str})
    async def smartlead_create_campaign(args: dict) -> dict:
        name = str(args.get("name", "")).strip()
        if not name:
            return _text("smartlead_create_campaign failed: need a name.")
        try:
            out = await asyncio.to_thread(
                _api, "POST", "/campaigns/create", {"name": name})
            cid = out.get("id") or out.get("campaign_id")
            return _text(f"Created campaign {name!r} (id {cid}). "
                         "Next: set its sequence and add leads.")
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_create_campaign failed: {str(e)[:200]}")

    @tool("smartlead_set_sequence",
          "Set/overwrite a campaign's email sequence (the initial email + "
          "follow-ups). `campaign` = id or name. `sequence_json` = a JSON array "
          "of steps, each {\"seq_number\":1, \"seq_delay_details\":{\"delay_in_"
          "days\":0}, \"subject\":\"...\", \"email_body\":\"<p>...</p>\"}. Use "
          "{{first_name}} etc. for personalisation. Follow-ups usually have "
          "subject:\"\" so they thread under the first email.",
          {"campaign": str, "sequence_json": str})
    async def smartlead_set_sequence(args: dict) -> dict:
        cid = await asyncio.to_thread(_campaign_id, args.get("campaign", ""))
        if not cid:
            return _text("Couldn't find that campaign.")
        try:
            seqs = json.loads(args.get("sequence_json", "[]"))
        except Exception as e:  # noqa: BLE001
            return _text(f"sequence_json isn't valid JSON: {str(e)[:120]}")
        try:
            await asyncio.to_thread(
                _api, "POST", f"/campaigns/{cid}/sequences",
                {"sequences": seqs})
            return _text(f"Saved a {len(seqs)}-step sequence on campaign {cid}.")
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_set_sequence failed: {str(e)[:200]}")

    @tool("smartlead_add_leads",
          "Add leads to a campaign — this is how Ultron's scraped, verified "
          "emails get loaded for outreach. `campaign` = id or name. "
          "`leads_json` = a JSON array of leads, each {\"email\":\"..\", "
          "\"first_name\":\"..\", \"last_name\":\"..\", \"company_name\":\"..\", "
          "\"phone_number\":\"..\", \"custom_fields\":{..}}. email is required; "
          "the rest optional. Max 100 per call.",
          {"campaign": str, "leads_json": str})
    async def smartlead_add_leads(args: dict) -> dict:
        cid = await asyncio.to_thread(_campaign_id, args.get("campaign", ""))
        if not cid:
            return _text("Couldn't find that campaign.")
        try:
            leads = json.loads(args.get("leads_json", "[]"))
        except Exception as e:  # noqa: BLE001
            return _text(f"leads_json isn't valid JSON: {str(e)[:120]}")
        if not leads:
            return _text("No leads to add.")
        try:
            out = await asyncio.to_thread(
                _api, "POST", f"/campaigns/{cid}/leads",
                {"lead_list": leads[:100]})
            up = out.get("upload_count", out.get("total_leads", len(leads)))
            dup = out.get("already_added_to_campaign", 0)
            return _text(f"Added {up} lead(s) to campaign {cid}"
                         + (f" ({dup} were already in it)." if dup else "."))
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_add_leads failed: {str(e)[:200]}")

    @tool("smartlead_campaign_control",
          "Start, pause, or stop a campaign's sending. `campaign` = id or name. "
          "`action` = 'start' | 'pause' | 'stop'. Use for 'start the outreach', "
          "'pause the campaign', 'stop sending'.",
          {"campaign": str, "action": str})
    async def smartlead_campaign_control(args: dict) -> dict:
        cid = await asyncio.to_thread(_campaign_id, args.get("campaign", ""))
        if not cid:
            return _text("Couldn't find that campaign.")
        m = {"start": "START", "pause": "PAUSED", "stop": "STOPPED"}
        status = m.get(str(args.get("action", "")).lower())
        if not status:
            return _text("action must be start, pause, or stop.")
        try:
            await asyncio.to_thread(
                _api, "POST", f"/campaigns/{cid}/status", {"status": status})
            return _text(f"Campaign {cid} → {status}.")
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_campaign_control failed: {str(e)[:200]}")

    @tool("smartlead_api",
          "Escape hatch for FULL control — call ANY Smartlead REST endpoint the "
          "named tools don't cover (webhooks, lead status, schedules, warmup "
          "toggles, delete, etc.). `method` = GET/POST/DELETE. `path` = the path "
          "after /api/v1, e.g. '/campaigns/123/leads?offset=0&limit=20' (api_key "
          "is added for you). `body_json` = JSON string for the request body "
          "(omit for GET). Returns the raw JSON. Prefer a named tool when one "
          "fits; use this for everything else.",
          {"method": str, "path": str, "body_json": str})
    async def smartlead_api(args: dict) -> dict:
        method = str(args.get("method", "GET")).upper()
        path = str(args.get("path", "")).strip()
        if not path:
            return _text("smartlead_api failed: need a path.")
        body = None
        if args.get("body_json"):
            try:
                body = json.loads(args["body_json"])
            except Exception as e:  # noqa: BLE001
                return _text(f"body_json isn't valid JSON: {str(e)[:120]}")
        try:
            out = await asyncio.to_thread(_api, method, path, body)
            s = json.dumps(out, ensure_ascii=False)
            return _text(s[:4000] + (" …[truncated]" if len(s) > 4000 else ""))
        except Exception as e:  # noqa: BLE001
            return _text(f"smartlead_api failed: {str(e)[:300]}")

    return create_sdk_mcp_server(
        name="smartlead", version="1.0.0",
        tools=[smartlead_campaigns, smartlead_campaign_stats, smartlead_senders,
               smartlead_create_campaign, smartlead_set_sequence,
               smartlead_add_leads, smartlead_campaign_control, smartlead_api],
    )
