#!/usr/bin/env python3
"""
iMessage Campaign Dashboard
Reads contacts from a CSV and auto-sends image + text messages via iMessage.
"""

import csv
import json
import subprocess
import sys
import os
import shutil
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse

# --- Configuration ---
MESSAGE_TEXT = (
    "Your message text here."
)
IMAGE_PATH = ""   # Absolute path to campaign image (PNG or JPG). Leave blank to send text only.
PORT = 8765
# ---------------------

contacts = []
sent_status = {}
previous_campaign = {}
STATUS_FILE = None
CONFIG_FILE = None


def load_config():
    global MESSAGE_TEXT, IMAGE_PATH
    if CONFIG_FILE and os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE) as f:
            cfg = json.load(f)
            MESSAGE_TEXT = cfg.get("message_text", MESSAGE_TEXT)
            IMAGE_PATH = cfg.get("image_path", IMAGE_PATH)


def save_config():
    with open(CONFIG_FILE, "w") as f:
        json.dump({"message_text": MESSAGE_TEXT, "image_path": IMAGE_PATH}, f, indent=2)


def load_contacts(csv_path):
    global contacts
    with open(csv_path, newline="", encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        for row in reader:
            first = row.get("first_name", "").strip()
            last = row.get("last_name", "").strip()
            name = (first + " " + last).strip() if last else first
            phone = row.get("phone_number", "").strip()
            if phone:
                phone = phone.replace(" ", "").replace("(", "").replace(")", "").replace("-", "")
                if not phone.startswith("+"):
                    if phone.startswith("1") and len(phone) == 11:
                        phone = "+" + phone
                    else:
                        phone = "+1" + phone
                contacts.append({"name": name, "phone": phone})


def load_status():
    global sent_status
    if os.path.exists(STATUS_FILE):
        with open(STATUS_FILE) as f:
            sent_status = json.load(f)


def save_status():
    with open(STATUS_FILE, "w") as f:
        json.dump(sent_status, f, indent=2)


def stage_image_for_messages(image_path):
    """Copy image to Pictures; newer macOS Messages may not deliver files from other folders."""
    pictures_dir = os.path.join(os.path.expanduser("~"), "Pictures", "CampaignMessenger")
    os.makedirs(pictures_dir, exist_ok=True)
    ext = os.path.splitext(image_path)[1].lower() or ".png"
    staged_path = os.path.join(pictures_dir, "campaign_image" + ext)
    shutil.copyfile(image_path, staged_path)
    return staged_path


def compose_message(phone):
    """Auto-send image (if configured) + message text via iMessage."""
    has_image = bool(IMAGE_PATH and os.path.exists(IMAGE_PATH))

    phone_as = json.dumps(phone)
    message_as = json.dumps(MESSAGE_TEXT)

    if has_image:
        # Use Messages' native AppleScript send command for attachments. The
        # clipboard/paste approach can paste a color value like #293030 on
        # newer macOS instead of the image. Also stage the file in Pictures;
        # Sequoia-era Messages may create Not Delivered attachments from other
        # folders, including Dropbox locations.
        staged_image = stage_image_for_messages(IMAGE_PATH)
        image_as = json.dumps(staged_image)
        script = f'''
tell application "Messages"
    set targetService to 1st service whose service type = iMessage
    set targetBuddy to buddy {phone_as} of targetService
    send (POSIX file {image_as}) to targetBuddy
    delay 1
    send {message_as} to targetBuddy
end tell
'''
    else:
        # Send text through Messages directly instead of typing keystrokes. This
        # avoids keyboard-layout/text-injection glitches such as stray symbols.
        script = f'''
tell application "Messages"
    set targetService to 1st service whose service type = iMessage
    set targetBuddy to buddy {phone_as} of targetService
    send {message_as} to targetBuddy
end tell
'''

    result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True, timeout=30)
    return result.returncode == 0, result.stderr


def format_phone_display(phone):
    digits = phone.replace("+1", "")
    if len(digits) == 10:
        return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
    return phone


HTML_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Campaign Message Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: #0a0a0a; color: #e0e0e0;
    padding: 20px; max-width: 800px; margin: 0 auto;
  }
  h1 { font-size: 22px; margin-bottom: 4px; color: #fff; }
  .subtitle { color: #888; font-size: 14px; margin-bottom: 20px; }
  .stats {
    display: flex; gap: 16px; margin-bottom: 20px;
    background: #1a1a1a; border-radius: 12px; padding: 16px;
  }
  .stat { text-align: center; flex: 1; }
  .stat-num { font-size: 28px; font-weight: 700; color: #fff; }
  .stat-label { font-size: 12px; color: #888; margin-top: 2px; }
  .stat-num.sent { color: #34c759; }
  .stat-num.remaining { color: #007aff; }
  .search {
    width: 100%; padding: 10px 14px; border-radius: 10px;
    border: 1px solid #333; background: #1a1a1a; color: #fff;
    font-size: 15px; margin-bottom: 16px; outline: none;
  }
  .search:focus { border-color: #007aff; }
  .filters { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
  .filter-btn {
    padding: 6px 14px; border-radius: 8px; border: 1px solid #333;
    background: #1a1a1a; color: #aaa; cursor: pointer; font-size: 13px;
  }
  .filter-btn.active { background: #007aff; color: #fff; border-color: #007aff; }
  .contact-list { list-style: none; }
  .contact {
    display: flex; align-items: center; justify-content: space-between;
    padding: 14px 16px; background: #1a1a1a; border-radius: 12px;
    margin-bottom: 8px; transition: opacity 0.2s;
  }
  .contact.status-sent { opacity: 0.5; }
  .contact-info { flex: 1; }
  .contact-name { font-weight: 600; font-size: 15px; color: #fff; }
  .contact-phone { font-size: 13px; color: #888; margin-top: 2px; }
  .contact-actions { display: flex; gap: 8px; align-items: center; }
  .btn {
    padding: 8px 18px; border-radius: 8px; border: none;
    font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.15s;
  }
  .btn-send { background: #34c759; color: #000; }
  .btn-send:hover { background: #28a745; }
  .btn-send:disabled { background: #333; color: #666; cursor: not-allowed; }
  .btn-skip {
    background: transparent; color: #888; border: 1px solid #333;
    padding: 8px 12px; font-size: 13px;
  }
  .btn-skip:hover { border-color: #666; color: #aaa; }
  .btn-undo {
    background: transparent; color: #ff9500; border: 1px solid #664400;
    padding: 8px 12px; font-size: 13px;
  }
  .badge {
    font-size: 11px; padding: 3px 8px; border-radius: 6px;
    font-weight: 600; margin-right: 8px;
  }
  .badge-sent { background: #1a3a1a; color: #34c759; }
  .badge-skipped { background: #3a3a1a; color: #ff9500; }
  .badge-invited { background: #2a1a3a; color: #bf5af2; }
  .composing { pointer-events: none; opacity: 0.7; }
  .spinner {
    display: inline-block; width: 16px; height: 16px;
    border: 2px solid #333; border-top-color: #34c759;
    border-radius: 50%; animation: spin 0.6s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  .toast {
    position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
    background: #34c759; color: #000; padding: 10px 20px; border-radius: 10px;
    font-weight: 600; font-size: 14px; display: none; z-index: 100;
  }
  .toast.error { background: #ff3b30; color: #fff; }
  .settings-panel {
    background: #1a1a1a; border-radius: 12px; margin-bottom: 20px;
    border: 1px solid #2a2a2a;
  }
  .settings-panel summary {
    padding: 12px 16px; cursor: pointer; font-size: 14px; color: #888;
    list-style: none; user-select: none;
  }
  .settings-panel summary::-webkit-details-marker { display: none; }
  .settings-panel[open] summary { color: #fff; border-bottom: 1px solid #2a2a2a; }
  .settings-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
  .settings-body label { font-size: 12px; color: #888; margin-bottom: 2px; }
  .settings-body textarea, .settings-body input[type=text] {
    width: 100%; background: #111; border: 1px solid #333; border-radius: 8px;
    color: #fff; font-size: 14px; padding: 10px 12px; outline: none;
    font-family: inherit; resize: vertical;
  }
  .settings-body textarea:focus, .settings-body input[type=text]:focus { border-color: #007aff; }
  .btn-save { background: #007aff; color: #fff; align-self: flex-start; }
  .image-pick-row { display: flex; align-items: center; gap: 10px; }
  .btn-pick { background: #2a2a2a; color: #ddd; border: 1px solid #444; padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; white-space: nowrap; }
  .btn-pick:hover { border-color: #666; color: #fff; }
  .image-filename { font-size: 13px; color: #aaa; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
  .image-preview { margin-top: 8px; max-width: 100%; max-height: 140px; border-radius: 8px; display: none; object-fit: contain; }
  #imageFileInput { display: none; }
</style>
</head>
<body>

<h1>Campaign Messenger</h1>
<p class="subtitle">Jenna Dukes for Horry County Council</p>

<div class="stats">
  <div class="stat">
    <div class="stat-num" id="total">0</div>
    <div class="stat-label">Total</div>
  </div>
  <div class="stat">
    <div class="stat-num sent" id="sentCount">0</div>
    <div class="stat-label">Sent</div>
  </div>
  <div class="stat">
    <div class="stat-num remaining" id="remainingCount">0</div>
    <div class="stat-label">Remaining</div>
  </div>
</div>

<details class="settings-panel" id="settingsPanel">
  <summary>&#9881; Edit Message &amp; Image</summary>
  <div class="settings-body">
    <div>
      <label>Message Text</label>
      <textarea id="cfgMessage" rows="4"></textarea>
    </div>
    <div>
      <label>Campaign Image (optional)</label>
      <div class="image-pick-row">
        <button class="btn-pick" onclick="document.getElementById('imageFileInput').click()">Choose Image...</button>
        <span class="image-filename" id="imageFilename">No image selected</span>
      </div>
      <input type="file" id="imageFileInput" accept="image/png,image/jpeg,image/jpg" onchange="handleImagePick(this)">
      <img id="imagePreview" class="image-preview" alt="Preview">
    </div>
    <button class="btn btn-save" onclick="saveConfig()">Save</button>
  </div>
</details>

<input type="text" class="search" id="search" placeholder="Search by name or phone...">

<div class="filters">
  <button class="filter-btn active" data-filter="all">All</button>
  <button class="filter-btn" data-filter="pending">Pending</button>
  <button class="filter-btn" data-filter="sent">Sent</button>
  <button class="filter-btn" data-filter="skipped">Skipped</button>
  <button class="filter-btn" data-filter="invited">Invited</button>
  <button class="filter-btn" data-filter="not_invited">Not Invited</button>
</div>

<ul class="contact-list" id="contactList"></ul>

<div class="toast" id="toast"></div>

<script>
let contacts = __CONTACTS_JSON__;
let statuses = __STATUS_JSON__;
let previousCampaign = __PREVIOUS_JSON__;
let currentFilter = "all";
let sendingPhone = null;

function updateStats() {
  const total = contacts.length;
  const sent = Object.values(statuses).filter(s => s === "sent").length;
  const skipped = Object.values(statuses).filter(s => s === "skipped").length;
  document.getElementById("total").textContent = total;
  document.getElementById("sentCount").textContent = sent;
  document.getElementById("remainingCount").textContent = total - sent - skipped;
}

function showToast(msg, isError) {
  const t = document.getElementById("toast");
  t.textContent = msg;
  t.className = "toast" + (isError ? " error" : "");
  t.style.display = "block";
  setTimeout(() => t.style.display = "none", 2500);
}

function getStatus(phone) { return statuses[phone] || "pending"; }

function renderContacts() {
  const query = document.getElementById("search").value.toLowerCase();
  const list = document.getElementById("contactList");
  list.innerHTML = "";

  contacts.forEach((c, i) => {
    const status = getStatus(c.phone);
    const wasInvited = !!previousCampaign[c.phone];
    if (currentFilter === "invited" && !wasInvited) return;
    if (currentFilter === "not_invited" && wasInvited) return;
    if (currentFilter !== "all" && currentFilter !== "invited" && currentFilter !== "not_invited" && status !== currentFilter) return;
    if (query && !c.name.toLowerCase().includes(query) && !c.phone.includes(query) && !c.phone_display.includes(query)) return;

    const isSending = sendingPhone === c.phone;
    const li = document.createElement("li");
    li.className = "contact" + (status === "sent" ? " status-sent" : "") + (isSending ? " composing" : "");

    let actions = "";
    if (isSending) {
      actions = '<span class="spinner"></span>';
    } else if (status === "pending") {
      actions = `
        <button class="btn btn-send" onclick="sendMsg('${c.phone}')">Send</button>
        <button class="btn btn-skip" onclick="markStatus('${c.phone}','skipped')">Skip</button>`;
    } else if (status === "sent") {
      actions = `
        <span class="badge badge-sent">Sent</span>
        <button class="btn btn-undo" onclick="markStatus('${c.phone}','pending')">Undo</button>`;
    } else if (status === "skipped") {
      actions = `
        <span class="badge badge-skipped">Skipped</span>
        <button class="btn btn-send" onclick="sendMsg('${c.phone}')">Send</button>
        <button class="btn btn-undo" onclick="markStatus('${c.phone}','pending')">Undo</button>`;
    }

    li.innerHTML = `
      <div class="contact-info">
        <div class="contact-name">${c.name || 'Unknown'}${previousCampaign[c.phone] ? ' <span class="badge badge-invited">Invited</span>' : ''}</div>
        <div class="contact-phone">${c.phone_display}</div>
      </div>
      <div class="contact-actions">${actions}</div>`;
    list.appendChild(li);
  });
  updateStats();
}

async function sendMsg(phone) {
  sendingPhone = phone;
  renderContacts();
  try {
    const res = await fetch("/compose", {
      method: "POST",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({phone})
    });
    const data = await res.json();
    if (data.ok) {
      showToast("✓ Sent!");
      statuses[phone] = "sent";
    } else {
      showToast("Error: " + (data.error || "Unknown"), true);
    }
  } catch(e) {
    showToast("Connection error", true);
  }
  sendingPhone = null;
  renderContacts();
}

async function markStatus(phone, status) {
  if (status === "pending") delete statuses[phone];
  else statuses[phone] = status;
  await fetch("/status", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({phone, status})
  });
  renderContacts();
}

document.getElementById("search").addEventListener("input", renderContacts);
document.querySelectorAll(".filter-btn").forEach(btn => {
  btn.addEventListener("click", () => {
    document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
    btn.classList.add("active");
    currentFilter = btn.dataset.filter;
    renderContacts();
  });
});

document.getElementById("settingsPanel").addEventListener("toggle", async function() {
  if (this.open) {
    const res = await fetch("/config");
    const cfg = await res.json();
    document.getElementById("cfgMessage").value = cfg.message_text;
    if (cfg.image_path) {
      const name = cfg.image_path.split("/").pop();
      document.getElementById("imageFilename").textContent = name;
    }
  }
});

let pendingImageFile = null;

function handleImagePick(input) {
  const file = input.files[0];
  if (!file) return;
  pendingImageFile = file;
  document.getElementById("imageFilename").textContent = file.name;
  const preview = document.getElementById("imagePreview");
  preview.src = URL.createObjectURL(file);
  preview.style.display = "block";
}

async function saveConfig() {
  const message_text = document.getElementById("cfgMessage").value.trim();
  if (!message_text) { showToast("Message cannot be empty", true); return; }

  if (pendingImageFile) {
    const formData = new FormData();
    formData.append("image", pendingImageFile, pendingImageFile.name);
    const uploadRes = await fetch("/upload-image", { method: "POST", body: formData });
    const uploadData = await uploadRes.json();
    if (!uploadData.ok) { showToast("Image upload failed", true); return; }
    pendingImageFile = null;
  }

  const res = await fetch("/config", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({message_text})
  });
  const data = await res.json();
  if (data.ok) {
    showToast("Settings saved!");
    document.getElementById("settingsPanel").removeAttribute("open");
  } else {
    showToast("Save failed", true);
  }
}

renderContacts();
</script>
</body>
</html>"""


class DashboardHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        pass

    def do_GET(self):
        if self.path == "/" or self.path == "/index.html":
            contacts_json = json.dumps([
                {**c, "phone_display": format_phone_display(c["phone"])}
                for c in contacts
            ])
            status_json = json.dumps(sent_status)
            prev_json = json.dumps(previous_campaign)
            html = HTML_TEMPLATE.replace("__CONTACTS_JSON__", contacts_json)
            html = html.replace("__STATUS_JSON__", status_json)
            html = html.replace("__PREVIOUS_JSON__", prev_json)
            self.send_response(200)
            self.send_header("Content-Type", "text/html")
            self.end_headers()
            self.wfile.write(html.encode())
        elif self.path == "/config":
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"message_text": MESSAGE_TEXT, "image_path": IMAGE_PATH}).encode())
        else:
            self.send_response(404)
            self.end_headers()

    def send_json(self, payload, status=200):
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(payload).encode())

    def read_json_body(self):
        length = int(self.headers.get("Content-Length", 0))
        if not length:
            return {}
        raw = self.rfile.read(length)
        return json.loads(raw.decode("utf-8"))

    def do_POST(self):
        if self.path == "/compose":
            body = self.read_json_body()
            phone = body.get("phone", "")
            ok, err = compose_message(phone)
            if ok:
                sent_status[phone] = "sent"
                save_status()
            self.send_json({"ok": ok, "error": err})

        elif self.path == "/status":
            body = self.read_json_body()
            phone = body.get("phone", "")
            status = body.get("status", "pending")
            if status == "pending":
                sent_status.pop(phone, None)
            else:
                sent_status[phone] = status
            save_status()
            self.send_json({"ok": True})

        elif self.path == "/config":
            global MESSAGE_TEXT
            body = self.read_json_body()
            MESSAGE_TEXT = body.get("message_text", MESSAGE_TEXT)
            save_config()
            self.send_json({"ok": True})

        elif self.path == "/upload-image":
            global IMAGE_PATH
            content_type = self.headers.get("Content-Type", "")
            if "multipart/form-data" not in content_type:
                self.send_response(400)
                self.end_headers()
                return
            length = int(self.headers.get("Content-Length", 0))
            raw = self.rfile.read(length)
            boundary = content_type.split("boundary=")[-1].encode()
            parts = raw.split(b"--" + boundary)
            saved_path = None
            for part in parts:
                if b"filename=" not in part:
                    continue
                header_end = part.find(b"\r\n\r\n")
                if header_end == -1:
                    continue
                headers_raw = part[:header_end].decode(errors="ignore")
                file_data = part[header_end + 4:]
                if file_data.endswith(b"\r\n"):
                    file_data = file_data[:-2]
                ext = ".png"
                if 'filename="' in headers_raw:
                    fname = headers_raw.split('filename="')[1].split('"')[0]
                    ext = os.path.splitext(fname)[1].lower() or ".png"
                dest = os.path.join(os.path.dirname(STATUS_FILE), "campaign_image" + ext)
                with open(dest, "wb") as f:
                    f.write(file_data)
                IMAGE_PATH = dest
                save_config()
                saved_path = dest
                break
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            if saved_path:
                self.wfile.write(json.dumps({"ok": True, "path": saved_path}).encode())
            else:
                self.wfile.write(json.dumps({"ok": False, "error": "No file found"}).encode())

        else:
            self.send_response(404)
            self.end_headers()


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 send_dashboard.py contacts.csv")
        sys.exit(1)

    csv_path = sys.argv[1]
    if not os.path.exists(csv_path):
        print(f"Error: File not found: {csv_path}")
        sys.exit(1)

    global STATUS_FILE, CONFIG_FILE
    _csv_dir = os.path.dirname(os.path.abspath(csv_path))
    STATUS_FILE = os.path.join(_csv_dir, "send_status.json")
    CONFIG_FILE = os.path.join(_csv_dir, "message_config.json")

    global previous_campaign
    prev_file = os.path.join(_csv_dir, "previous_campaign.json")
    if os.path.exists(prev_file):
        with open(prev_file) as f:
            previous_campaign = json.load(f)

    load_contacts(csv_path)
    load_status()
    load_config()
    invited_count = sum(1 for c in contacts if c["phone"] in previous_campaign)
    print(f"Loaded {len(contacts)} contacts from {csv_path}")
    print(f"Previously invited: {invited_count}")
    print(f"Already sent: {len(sent_status)}")
    print(f"\nDashboard running at: http://localhost:{PORT}")
    print("Press Ctrl+C to stop\n")

    server = HTTPServer(("localhost", PORT), DashboardHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nShutting down.")
        server.server_close()


if __name__ == "__main__":
    main()
