<?php
/**
 * voipms-comms.php
 *
 * Copyright 2026 by The Finest Of Lines Tattoo Co.
 * 17080 Superior Road, St. Robert, MO 65584
 *
 * SMS/MMS communications proxy for voip.ms API.
 * Persistent storage in comms_2026 database.
 * Validates incoming JWT, manages conversations/messages/media.
 *
 * Actions (via ?action= query param):
 *   conversations  GET   List conversations by last_message_date DESC
 *                        ?show=active|archived|spam|blocked|all (default: active)
 *   messages       GET   Messages for a conversation with joined media URLs
 *   send           POST  Send SMS/MMS via voip.ms, store in DB
 *   sync           POST  Pull recent from voip.ms, dedup, store, download media
 *   mark_read      POST  Reset unread_count for a conversation
 *   set_contact_name POST Set display name for a conversation
 *   today_count    GET   Count of today's messages (dashboard card)
 *   archive        POST  Soft-archive a conversation (admin only)
 *   unarchive      POST  Restore a conversation from archive (admin only)
 *   spam           POST  Flag a conversation as spam (admin only)
 *   unspam         POST  Unflag a conversation from spam (admin only)
 *   block          POST  Block a conversation — drops inbound silently (admin only)
 *   unblock        POST  Unblock a conversation (admin only)
 *   archive_by_phone POST Archive all active conversations for a phone number
 *
 * Code blocks by Claude Opus 4.6
 * Concept & Tuck-pointing by Raydog.
 */

require 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

header('Content-Type: application/json');

// ── voip.ms credentials (same as voipms-cdr.php) ──
define('VOIPMS_API_USERNAME', 'solaceraze@gmail.com');
define('VOIPMS_API_PASSWORD', '*AH=Zw[?OFT8Z[!xAN66wzIAiY)H1a#1');
define('VOIPMS_API_URL', 'https://voip.ms/api/v1/rest.php');

// ── Database ──
define('DB_HOST', 'localhost');
define('DB_NAME', 'comms_2026');
define('DB_USER', 'data');
define('DB_PASS', 'ncc1701D');

// ── Media cache ──
define('MEDIA_DIR', '/var/www/data.finestoflines.net/comms_media');
define('MEDIA_URL_BASE', 'https://data.finestoflines.net/comms_media');

// ── JWT secret (same as auth-api.php) ──
$jwt_secret = getenv('CITADEL_2025_JWT_SECRET') ?: 'bXlzZWN1cmVzdHJpbmcxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMw==';

// ── Validate Bearer token ──
$auth_header = $_SERVER['HTTP_AUTHORIZATION']
    ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
    ?? getallheaders()['Authorization']
    ?? '';

if (!preg_match('/Bearer\s(\S+)/', $auth_header, $matches)) {
    http_response_code(401);
    echo json_encode(['status' => 'error', 'error' => 'No token provided']);
    exit;
}

try {
    $payload = JWT::decode($matches[1], new Key($jwt_secret, 'HS256'));
} catch (Exception $e) {
    http_response_code(401);
    echo json_encode(['status' => 'error', 'error' => 'Invalid token']);
    exit;
}

// ── Database connection ──
try {
    $pdo = new PDO(
        "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
        DB_USER,
        DB_PASS,
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
    );
} catch (PDOException $e) {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'error' => 'Database connection failed']);
    exit;
}

// ── Route by action ──
$action = $_GET['action'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];

// Handle OPTIONS preflight (belt-and-suspenders — Apache handles this too)
if ($method === 'OPTIONS') {
    http_response_code(200);
    exit;
}

switch ($action) {
    case 'conversations':
        if ($method !== 'GET') { method_not_allowed(); }
        action_conversations($pdo);
        break;
    case 'messages':
        if ($method !== 'GET') { method_not_allowed(); }
        action_messages($pdo);
        break;
    case 'send':
        if ($method !== 'POST') { method_not_allowed(); }
        action_send($pdo);
        break;
    case 'sync':
        if ($method !== 'POST') { method_not_allowed(); }
        action_sync($pdo);
        break;
    case 'mark_read':
        if ($method !== 'POST') { method_not_allowed(); }
        action_mark_read($pdo);
        break;
    case 'set_contact_name':
        if ($method !== 'POST') { method_not_allowed(); }
        action_set_contact_name($pdo);
        break;
    case 'today_count':
        if ($method !== 'GET') { method_not_allowed(); }
        action_today_count($pdo);
        break;
    case 'find_or_create':
        if ($method !== 'POST') { method_not_allowed(); }
        action_find_or_create($pdo);
        break;
    case 'unread_total':
        if ($method !== 'GET') { method_not_allowed(); }
        action_unread_total($pdo);
        break;
    case 'archive':
        if ($method !== 'POST') { method_not_allowed(); }
        require_admin($payload);
        action_set_flag($pdo, 'archived_at');
        break;
    case 'unarchive':
        if ($method !== 'POST') { method_not_allowed(); }
        require_admin($payload);
        action_clear_flag($pdo, 'archived_at');
        break;
    case 'spam':
        if ($method !== 'POST') { method_not_allowed(); }
        require_admin($payload);
        action_set_flag($pdo, 'spam_at');
        break;
    case 'unspam':
        if ($method !== 'POST') { method_not_allowed(); }
        require_admin($payload);
        action_clear_flag($pdo, 'spam_at');
        break;
    case 'block':
        if ($method !== 'POST') { method_not_allowed(); }
        require_admin($payload);
        action_set_flag($pdo, 'blocked_at');
        break;
    case 'unblock':
        if ($method !== 'POST') { method_not_allowed(); }
        require_admin($payload);
        action_clear_flag($pdo, 'blocked_at');
        break;
    case 'archive_by_phone':
        if ($method !== 'POST') { method_not_allowed(); }
        action_archive_by_phone($pdo);
        break;
    default:
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'Unknown action', 'valid_actions' => [
            'conversations', 'messages', 'send', 'sync', 'mark_read', 'set_contact_name',
            'today_count', 'unread_total', 'find_or_create',
            'archive', 'unarchive', 'spam', 'unspam', 'block', 'unblock',
            'archive_by_phone'
        ]]);
        exit;
}

// ═══════════════════════════════════════════════════════════
// Action handlers
// ═══════════════════════════════════════════════════════════

function action_conversations(PDO $pdo) {
    $did = $_GET['did'] ?? null;
    $show = $_GET['show'] ?? 'active';
    $limit = min((int)($_GET['limit'] ?? 50), 200);
    $offset = max((int)($_GET['offset'] ?? 0), 0);

    $sql = "SELECT * FROM conversations";
    $conditions = [];
    $params = [];

    if ($did) {
        $conditions[] = "did = ?";
        $params[] = $did;
    }

    switch ($show) {
        case 'archived':
            $conditions[] = "archived_at IS NOT NULL AND spam_at IS NULL AND blocked_at IS NULL";
            break;
        case 'spam':
            $conditions[] = "spam_at IS NOT NULL";
            break;
        case 'blocked':
            $conditions[] = "blocked_at IS NOT NULL";
            break;
        case 'all':
            break;
        case 'active':
        default:
            $conditions[] = "archived_at IS NULL AND spam_at IS NULL AND blocked_at IS NULL";
            break;
    }

    if (!empty($conditions)) {
        $sql .= " WHERE " . implode(" AND ", $conditions);
    }

    $sql .= " ORDER BY last_message_date DESC LIMIT ? OFFSET ?";

    $stmt = $pdo->prepare($sql);
    $bind_idx = 1;
    foreach ($params as $p) {
        $stmt->bindValue($bind_idx++, $p, PDO::PARAM_STR);
    }
    $stmt->bindValue($bind_idx++, $limit, PDO::PARAM_INT);
    $stmt->bindValue($bind_idx++, $offset, PDO::PARAM_INT);
    $stmt->execute();
    $rows = $stmt->fetchAll();

    // Build engaged array per conversation (last 50 messages, oldest first)
    $engaged_stmt = $pdo->prepare(
        "SELECT direction, body FROM (
            SELECT direction, body, sent_date FROM messages
            WHERE conversation_id = ?
            ORDER BY sent_date DESC LIMIT 50
        ) sub ORDER BY sent_date ASC"
    );
    foreach ($rows as &$row) {
        $engaged_stmt->execute([$row['id']]);
        $msgs = $engaged_stmt->fetchAll();
        $engaged = [];
        foreach ($msgs as $m) {
            if ($m['direction'] === 'inbound') {
                $engaged[] = 'client';
            } else {
                $prefix = substr($m['body'] ?? '', 0, 3);
                $engaged[] = ctype_digit($prefix) ? $prefix : 'unknown';
            }
        }
        $row['engaged'] = $engaged;
    }

    // messages_today: per-DID count of messages whose sent_date falls on the current date.
    // Derived on read rather than maintained as a counter — a missed increment would be silent
    // and permanent, and a daily reset job that fails to run is invisible. This cannot drift.
    $mt_sql = "SELECT c.did, COUNT(*) AS n
               FROM messages m
               JOIN conversations c ON c.id = m.conversation_id
               WHERE DATE(m.sent_date) = CURDATE()";
    $mt_params = [];
    if ($did) {
        $mt_sql .= " AND c.did = ?";
        $mt_params[] = $did;
    }
    $mt_sql .= " GROUP BY c.did";
    $mt_stmt = $pdo->prepare($mt_sql);
    $mt_stmt->execute($mt_params);
    $messages_today = [];
    foreach ($mt_stmt->fetchAll() as $r) {
        $messages_today[$r['did']] = (int)$r['n'];
    }

    echo json_encode([
        'status'          => 'success',
        'conversations'   => $rows,
        'messages_today'  => $messages_today,
    ]);
}

function action_messages(PDO $pdo) {
    $conversation_id = $_GET['conversation_id'] ?? null;
    if (!$conversation_id) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'conversation_id required']);
        exit;
    }

    $limit = min((int)($_GET['limit'] ?? 100), 500);
    $offset = max((int)($_GET['offset'] ?? 0), 0);

    // Fetch messages
    $stmt = $pdo->prepare(
        "SELECT * FROM messages WHERE conversation_id = ? ORDER BY sent_date DESC LIMIT ? OFFSET ?"
    );
    $stmt->bindValue(1, (int)$conversation_id, PDO::PARAM_INT);
    $stmt->bindValue(2, $limit, PDO::PARAM_INT);
    $stmt->bindValue(3, $offset, PDO::PARAM_INT);
    $stmt->execute();
    $messages = $stmt->fetchAll();

    // Fetch media for these messages in one query
    if (!empty($messages)) {
        $msg_ids = array_column($messages, 'id');
        $placeholders = implode(',', array_fill(0, count($msg_ids), '?'));
        $media_stmt = $pdo->prepare(
            "SELECT * FROM media WHERE message_id IN ($placeholders) ORDER BY id"
        );
        $media_stmt->execute($msg_ids);
        $all_media = $media_stmt->fetchAll();

        // Group media by message_id
        $media_map = [];
        foreach ($all_media as $m) {
            $m['url'] = MEDIA_URL_BASE . '/' . $m['cached_filename'];
            $media_map[$m['message_id']][] = $m;
        }

        // Attach media to messages
        foreach ($messages as &$msg) {
            $msg['media'] = $media_map[$msg['id']] ?? [];
        }
        unset($msg);
    }

    echo json_encode(['status' => 'success', 'messages' => $messages]);
}

function action_send(PDO $pdo) {
    $data = json_decode(file_get_contents('php://input'), true);

    $did     = $data['did']     ?? '';
    $dst     = $data['dst']     ?? '';
    $message = $data['message'] ?? '';
    $media   = $data['media']   ?? null; // base64 data URL for MMS

    // Message required for SMS; for MMS, media alone is sufficient
    if ($did === '' || $dst === '' || ($message === '' && !$media)) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'Missing required fields: did, dst, and message or media']);
        exit;
    }

    // Normalize destination — strip non-digits, drop leading 1 if 11 digits
    $dst_clean = preg_replace('/\D/', '', $dst);
    if (strlen($dst_clean) === 11 && $dst_clean[0] === '1') {
        $dst_clean = substr($dst_clean, 1);
    }

    if (strlen($dst_clean) !== 10) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'Destination must be a 10-digit US/CA number']);
        exit;
    }

    $has_media = !empty($media);

    // Default to MMS to avoid 160-char SMS limit.
    // SMS fallback only when explicitly requested via force_sms flag.
    // A client texting via SMS does NOT mean they can't receive MMS —
    // virtually all modern phones support MMS receipt.
    $force_sms = !empty($data['force_sms']);
    $message_type = ($has_media || !$force_sms) ? 'mms' : 'sms';

    if ($has_media) {
        // Decode base64 data URL and save to media cache
        $media_binary = null;
        $media_mime = 'image/png';
        if (preg_match('/^data:([^;]+);base64,(.+)$/', $media, $m)) {
            $media_mime = $m[1];
            $media_binary = base64_decode($m[2]);
        } else {
            $media_binary = base64_decode($media);
        }

        if (!$media_binary || strlen($media_binary) < 100) {
            http_response_code(400);
            echo json_encode(['status' => 'error', 'error' => 'Invalid media data']);
            exit;
        }

        // Save to media cache for voip.ms to fetch
        $ext_map = [
            'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif',
            'image/webp' => 'webp', 'video/mp4' => 'mp4',
        ];
        $ext = $ext_map[$media_mime] ?? 'png';
        $media_filename = 'outbound_' . time() . '_' . mt_rand(1000, 9999) . '.' . $ext;
        $media_path = MEDIA_DIR . '/' . $media_filename;
        file_put_contents($media_path, $media_binary);
        $media_url = MEDIA_URL_BASE . '/' . $media_filename;

        // Send MMS with media via voip.ms
        $voip_params = http_build_query([
            'api_username' => VOIPMS_API_USERNAME,
            'api_password' => VOIPMS_API_PASSWORD,
            'method'       => 'sendMMS',
            'did'          => $did,
            'dst'          => $dst_clean,
            'message'      => $message,
            'media1'       => $media_url,
        ]);
    } elseif ($force_sms) {
        // Client appears SMS-only — use sendSMS
        $voip_params = http_build_query([
            'api_username' => VOIPMS_API_USERNAME,
            'api_password' => VOIPMS_API_PASSWORD,
            'method'       => 'sendSMS',
            'did'          => $did,
            'dst'          => $dst_clean,
            'message'      => $message,
        ]);
    } else {
        // Default: send text-only MMS (no 160-char limit)
        $voip_params = http_build_query([
            'api_username' => VOIPMS_API_USERNAME,
            'api_password' => VOIPMS_API_PASSWORD,
            'method'       => 'sendMMS',
            'did'          => $did,
            'dst'          => $dst_clean,
            'message'      => $message,
        ]);
    }

    $voip_response = voipms_call(VOIPMS_API_URL . "?" . $voip_params);
    if ($voip_response === null) { return; } // error already sent

    // Log all send attempts for diagnostics
    $log_entry = date('Y-m-d H:i:s') . " SEND did=$did dst=$dst_clean type=$message_type status=" . ($voip_response['status'] ?? 'unknown') . " response=" . json_encode($voip_response) . "\n";
    @file_put_contents('/var/www/logs/comms-send.log', $log_entry, FILE_APPEND | LOCK_EX);

    // Store in DB on success
    if (($voip_response['status'] ?? '') === 'success') {
        $conv_id = find_or_create_conversation($pdo, $did, $dst_clean);
        $preview = $has_media ? ($message ? mb_substr($message, 0, 160) : '[MMS Image]') : mb_substr($message, 0, 160);

        $stmt = $pdo->prepare(
            "INSERT INTO messages (conversation_id, voipms_id, direction, message_type, body, sent_date, status)
             VALUES (?, ?, 'outbound', ?, ?, NOW(), 'sent')"
        );
        $synthetic_id = 'sent_' . $did . '_' . $dst_clean . '_' . time() . '_' . mt_rand(1000, 9999);
        $stmt->execute([$conv_id, $synthetic_id, $message_type, $message]);
        $message_id = (int)$pdo->lastInsertId();

        // If MMS, store media record
        if ($has_media && isset($media_filename)) {
            $file_size = strlen($media_binary);
            $pdo->prepare(
                "INSERT INTO media (message_id, voipms_url, cached_filename, mime_type, file_size)
                 VALUES (?, ?, ?, ?, ?)"
            )->execute([$message_id, $media_url, $media_filename, $media_mime, $file_size]);
        }

        // Update conversation
        $pdo->prepare(
            "UPDATE conversations SET last_message_date = NOW(), last_message_preview = ? WHERE id = ?"
        )->execute([$preview, $conv_id]);

        // Emit SSE event so other devices refresh
        $sender_device = $data["device_id"] ?? "server";
        emit_sse_event("sms_sent", [
            "conversation_id" => $conv_id,
            "contact" => $dst_clean,
            "did" => $did
        ], $sender_device);
    }

    echo json_encode($voip_response);
}
function action_sync(PDO $pdo) {
    $data = json_decode(file_get_contents('php://input'), true) ?? [];

    $did  = $data['did']  ?? null;
    $days = (int)($data['days'] ?? 7);
    $days = max(1, min($days, 90)); // clamp 1–90

    $date_from = date('Y-m-d', strtotime("-{$days} days"));
    $date_to   = date('Y-m-d');

    $synced_sms = 0;
    $synced_mms = 0;
    $media_downloaded = 0;

    // ── Sync SMS ──
    $sms_params = [
        'api_username' => VOIPMS_API_USERNAME,
        'api_password' => VOIPMS_API_PASSWORD,
        'method'       => 'getSMS',
        'from'         => $date_from,
        'to'           => $date_to,
    ];
    if ($did) { $sms_params['did'] = $did; }

    $sms_response = voipms_call(VOIPMS_API_URL . '?' . http_build_query($sms_params));
    if ($sms_response !== null && ($sms_response['status'] ?? '') === 'success') {
        $sms_list = $sms_response['sms'] ?? [];
        foreach ($sms_list as $sms) {
            $vid = 'sms_' . $sms['id'];

            // Dedup check
            $exists = $pdo->prepare("SELECT 1 FROM messages WHERE voipms_id = ?");
            $exists->execute([$vid]);
            if ($exists->fetch()) { continue; }

            $msg_did     = $sms['did'] ?? '';
            $msg_contact = $sms['contact'] ?? '';
            $msg_body    = $sms['message'] ?? '';
            $msg_date    = $sms['date'] ?? date('Y-m-d H:i:s');
            $direction   = ($sms['type'] == '1') ? 'inbound' : 'outbound';

            $conv_id = find_or_create_conversation($pdo, $msg_did, $msg_contact);

            // Block enforcement: skip inbound messages for blocked conversations
            if ($direction === 'inbound') {
                $blk = $pdo->prepare("SELECT blocked_at FROM conversations WHERE id = ?");
                $blk->execute([$conv_id]);
                $conv_row = $blk->fetch();
                if ($conv_row && $conv_row['blocked_at'] !== null) {
                    @file_put_contents('/var/www/logs/comms-blocked.log',
                        date('Y-m-d H:i:s') . " BLOCKED sms from=$msg_contact did=$msg_did vid=$vid\n",
                        FILE_APPEND | LOCK_EX);
                    continue;
                }
            }

            // Content dedup: skip if a sent_ message already exists with same content
            $sent_dup = $pdo->prepare(
                "SELECT id FROM messages WHERE conversation_id = ? AND sent_date = ? AND direction = ? AND voipms_id LIKE 'sent\_%' LIMIT 1"
            );
            $sent_dup->execute([$conv_id, $msg_date, $direction]);
            $existing = $sent_dup->fetch();
            if ($existing) {
                // Update the synthetic ID to the real voip.ms ID
                $pdo->prepare("UPDATE messages SET voipms_id = ? WHERE id = ?")->execute([$vid, $existing['id']]);
                $synced_sms++;
                continue;
            }

            $stmt = $pdo->prepare(
                "INSERT INTO messages (conversation_id, voipms_id, direction, message_type, body, sent_date, status)
                 VALUES (?, ?, ?, 'sms', ?, ?, 'received')"
            );
            $stmt->execute([$conv_id, $vid, $direction, $msg_body, $msg_date]);

            update_conversation_after_insert($pdo, $conv_id, $msg_date, $msg_body, $direction);
            $synced_sms++;
        }
    }

    // ── Sync MMS ──
    $mms_params = [
        'api_username' => VOIPMS_API_USERNAME,
        'api_password' => VOIPMS_API_PASSWORD,
        'method'       => 'getMMS',
        'from'         => $date_from,
        'to'           => $date_to,
    ];
    if ($did) { $mms_params['did'] = $did; }

    $mms_response = voipms_call(VOIPMS_API_URL . '?' . http_build_query($mms_params));
    if ($mms_response !== null && ($mms_response['status'] ?? '') === 'success') {
        $mms_list = $mms_response['sms'] ?? [];
        foreach ($mms_list as $mms) {
            $vid = 'mms_' . $mms['id'];

            // Dedup check
            $exists = $pdo->prepare("SELECT 1 FROM messages WHERE voipms_id = ?");
            $exists->execute([$vid]);
            if ($exists->fetch()) { continue; }

            $msg_did     = $mms['did'] ?? '';
            $msg_contact = $mms['contact'] ?? '';
            $msg_body    = $mms['message'] ?? '';
            $msg_date    = $mms['date'] ?? date('Y-m-d H:i:s');
            $direction   = ($mms['type'] == '1') ? 'inbound' : 'outbound';

            $conv_id = find_or_create_conversation($pdo, $msg_did, $msg_contact);

            // Block enforcement: skip inbound messages for blocked conversations
            if ($direction === 'inbound') {
                $blk = $pdo->prepare("SELECT blocked_at FROM conversations WHERE id = ?");
                $blk->execute([$conv_id]);
                $conv_row = $blk->fetch();
                if ($conv_row && $conv_row['blocked_at'] !== null) {
                    @file_put_contents('/var/www/logs/comms-blocked.log',
                        date('Y-m-d H:i:s') . " BLOCKED mms from=$msg_contact did=$msg_did vid=$vid\n",
                        FILE_APPEND | LOCK_EX);
                    continue;
                }
            }

            // Content dedup: skip if a sent_ message already exists with same content
            $sent_dup = $pdo->prepare(
                "SELECT id FROM messages WHERE conversation_id = ? AND sent_date = ? AND direction = ? AND voipms_id LIKE 'sent\_%' LIMIT 1"
            );
            $sent_dup->execute([$conv_id, $msg_date, $direction]);
            $existing = $sent_dup->fetch();
            if ($existing) {
                $pdo->prepare("UPDATE messages SET voipms_id = ? WHERE id = ?")->execute([$vid, $existing['id']]);
                $synced_mms++;
                continue;
            }

            $stmt = $pdo->prepare(
                "INSERT INTO messages (conversation_id, voipms_id, direction, message_type, body, sent_date, status)
                 VALUES (?, ?, ?, 'mms', ?, ?, 'received')"
            );
            $stmt->execute([$conv_id, $vid, $direction, $msg_body, $msg_date]);
            $message_id = (int)$pdo->lastInsertId();

            // Download MMS media
            $media_urls = collect_mms_media_urls($mms);
            $media_idx = 0;
            foreach ($media_urls as $media_url) {
                $downloaded = download_media($media_url, $message_id, $media_idx, $pdo);
                if ($downloaded) { $media_downloaded++; }
                $media_idx++;
            }

            update_conversation_after_insert($pdo, $conv_id, $msg_date, $msg_body ?: '[MMS]', $direction);
            $synced_mms++;
        }
    }

    echo json_encode([
        'status'           => 'success',
        'synced_sms'       => $synced_sms,
        'synced_mms'       => $synced_mms,
        'media_downloaded' => $media_downloaded,
    ]);
}

function action_mark_read(PDO $pdo) {
    $data = json_decode(file_get_contents('php://input'), true);
    $conversation_id = $data['conversation_id'] ?? null;

    if (!$conversation_id) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'conversation_id required']);
        exit;
    }

    $stmt = $pdo->prepare("UPDATE conversations SET unread_count = 0 WHERE id = ?");
    $stmt->execute([(int)$conversation_id]);


    // Emit SSE event so other devices update their unread counts
    $sender_device = $data["device_id"] ?? "server";
    emit_sse_event("sms_read", [
        "conversation_id" => (int)$conversation_id
    ], $sender_device);

    echo json_encode(['status' => 'success']);
}

function action_set_contact_name(PDO $pdo) {
    $data = json_decode(file_get_contents('php://input'), true);
    $conversation_id = $data['conversation_id'] ?? null;
    $name = $data['name'] ?? null;

    if (!$conversation_id || $name === null) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'conversation_id and name required']);
        exit;
    }

    $stmt = $pdo->prepare("UPDATE conversations SET contact_name = ? WHERE id = ?");
    $stmt->execute([$name, (int)$conversation_id]);

    echo json_encode(['status' => 'success']);
}

function action_today_count(PDO $pdo) {
    $today = date('Y-m-d');
    $stmt = $pdo->prepare("SELECT COUNT(*) AS count FROM messages WHERE DATE(sent_date) = ?");
    $stmt->execute([$today]);
    $row = $stmt->fetch();

    echo json_encode(['status' => 'success', 'count' => (int)$row['count']]);
}

function action_unread_total(PDO $pdo) {
    $stmt = $pdo->query("SELECT COALESCE(SUM(unread_count), 0) AS total FROM conversations");
    $row = $stmt->fetch();
    echo json_encode(['status' => 'success', 'count' => (int)$row['total']]);
}


function action_find_or_create(PDO $pdo) {
    $data = json_decode(file_get_contents('php://input'), true);
    $phone = $data['phone'] ?? '';
    $did = $data['did'] ?? '5572272575'; // Default to main desk; art desk sends '5572272576'

    // Normalize to 10 digits
    $digits = preg_replace('/\D/', '', $phone);
    if (strlen($digits) === 11 && $digits[0] === '1') {
        $digits = substr($digits, 1);
    }
    if (strlen($digits) !== 10) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'Phone must be a 10-digit US/CA number']);
        exit;
    }

    $stmt = $pdo->prepare("SELECT * FROM conversations WHERE did = ? AND contact = ?");
    $stmt->execute([$did, $digits]);
    $conv = $stmt->fetch();

    if ($conv) {
        // Auto-unarchive when chat icon opens an archived conversation
        // (only if archived, NOT if spam or blocked)
        if ($conv['archived_at'] !== null && $conv['spam_at'] === null && $conv['blocked_at'] === null) {
            $pdo->prepare("UPDATE conversations SET archived_at = NULL WHERE id = ?")->execute([$conv['id']]);
            $conv['archived_at'] = null;
        }
        $conv['engaged'] = [];
        echo json_encode(['status' => 'success', 'conversation' => $conv, 'created' => false]);
        return;
    }

    $stmt = $pdo->prepare("INSERT INTO conversations (did, contact, created_at) VALUES (?, ?, NOW())");
    $stmt->execute([$did, $digits]);
    $new_id = (int)$pdo->lastInsertId();

    $stmt = $pdo->prepare("SELECT * FROM conversations WHERE id = ?");
    $stmt->execute([$new_id]);
    $conv = $stmt->fetch();
    $conv['engaged'] = [];

    emit_sse_event('sms_new_conversation', ['conversation_id' => $new_id, 'contact' => $digits, 'did' => $did], 'server');

    echo json_encode(['status' => 'success', 'conversation' => $conv, 'created' => true]);
}
// ═══════════════════════════════════════════════════════════
// Archive / Spam / Block actions
// ═══════════════════════════════════════════════════════════

function require_admin(object $payload) {
    if (!isset($payload->role) || !in_array($payload->role, ['super_admin', 'admin'])) {
        http_response_code(403);
        echo json_encode(['status' => 'error', 'error' => 'Admin role required']);
        exit;
    }
}

function action_set_flag(PDO $pdo, string $column) {
    $data = json_decode(file_get_contents('php://input'), true);
    $conversation_id = $data['conversation_id'] ?? null;

    if (!$conversation_id) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'conversation_id required']);
        exit;
    }

    $allowed = ['archived_at', 'spam_at', 'blocked_at'];
    if (!in_array($column, $allowed)) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'Invalid flag']);
        exit;
    }

    $stmt = $pdo->prepare("UPDATE conversations SET $column = NOW() WHERE id = ? AND $column IS NULL");
    $stmt->execute([(int)$conversation_id]);

    echo json_encode(['status' => 'success', 'action' => 'set', 'flag' => $column, 'conversation_id' => (int)$conversation_id]);
}

function action_clear_flag(PDO $pdo, string $column) {
    $data = json_decode(file_get_contents('php://input'), true);
    $conversation_id = $data['conversation_id'] ?? null;

    if (!$conversation_id) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'conversation_id required']);
        exit;
    }

    $allowed = ['archived_at', 'spam_at', 'blocked_at'];
    if (!in_array($column, $allowed)) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'Invalid flag']);
        exit;
    }

    $stmt = $pdo->prepare("UPDATE conversations SET $column = NULL WHERE id = ?");
    $stmt->execute([(int)$conversation_id]);

    echo json_encode(['status' => 'success', 'action' => 'clear', 'flag' => $column, 'conversation_id' => (int)$conversation_id]);
}

function action_archive_by_phone(PDO $pdo) {
    $data = json_decode(file_get_contents('php://input'), true);
    $phone = $data['phone'] ?? '';

    // Normalize to 10 digits
    $digits = preg_replace('/\D/', '', $phone);
    if (strlen($digits) === 11 && $digits[0] === '1') {
        $digits = substr($digits, 1);
    }
    if (strlen($digits) !== 10) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'error' => 'Phone must be a 10-digit US/CA number']);
        exit;
    }

    $stmt = $pdo->prepare(
        "UPDATE conversations SET archived_at = NOW()
         WHERE contact = ? AND archived_at IS NULL AND spam_at IS NULL AND blocked_at IS NULL"
    );
    $stmt->execute([$digits]);
    $count = $stmt->rowCount();

    if ($count > 0) {
        @file_put_contents('/var/www/logs/comms-archive-hook.log',
            date('Y-m-d H:i:s') . " ARCHIVE_BY_PHONE phone=$digits convs=$count\n",
            FILE_APPEND | LOCK_EX);
    }

    echo json_encode(['status' => 'success', 'archived' => $count, 'phone' => $digits]);
}

// ═══════════════════════════════════════════════════════════
// Helper functions
// ═══════════════════════════════════════════════════════════

function emit_sse_event(string $type, array $data, string $device_id = "server"): void {
    $dir = "/dev/shm/sse_lineup";
    if (!is_dir($dir)) { @mkdir($dir, 0777, true); }
    $event = json_encode([
        "device_id" => $device_id,
        "type" => $type,
        "data" => $data,
        "timestamp" => time()
    ]);
    $filename = $dir . "/" . uniqid("sms_", true) . ".json";
    file_put_contents($filename, $event, LOCK_EX);

    // Communicator Mini alert (added 2026-08-05) — bare SIP NOTIFY, no payload. The engine
    // reacts by re-fetching from this file's own endpoints, so a dropped/failed poke only
    // delays pickup rather than losing data. Backgrounded so a slow/unreachable PBX never
    // blocks the request.
    // Repointed same day from forge -> pegasus relay: pegasus is the Art Desk PBX (Forge has
    // no inbound routes and stamps Zora's CID on outbound). See /var/www/.ssh/config.
    exec('timeout 8 ssh comm-mini-notify > /dev/null 2>&1 &');
}

function method_not_allowed() {
    http_response_code(405);
    echo json_encode(['status' => 'error', 'error' => 'Method not allowed']);
    exit;
}

function find_or_create_conversation(PDO $pdo, string $did, string $contact): int {
    // Normalize contact — digits only
    $contact = preg_replace('/\D/', '', $contact);

    $stmt = $pdo->prepare("SELECT id FROM conversations WHERE did = ? AND contact = ?");
    $stmt->execute([$did, $contact]);
    $row = $stmt->fetch();

    if ($row) {
        return (int)$row['id'];
    }

    $stmt = $pdo->prepare(
        "INSERT INTO conversations (did, contact, created_at) VALUES (?, ?, NOW())"
    );
    $stmt->execute([$did, $contact]);
    return (int)$pdo->lastInsertId();
}

function update_conversation_after_insert(PDO $pdo, int $conv_id, string $msg_date, string $body, string $direction) {
    $preview = mb_substr($body, 0, 160);

    // Update last message date/preview if this message is newer
    $pdo->prepare(
        "UPDATE conversations
         SET last_message_date = GREATEST(COALESCE(last_message_date, '1970-01-01'), ?),
             last_message_preview = IF(? >= COALESCE(last_message_date, '1970-01-01'), ?, last_message_preview),
             unread_count = unread_count + IF(? = 'inbound', 1, 0)
         WHERE id = ?"
    )->execute([$msg_date, $msg_date, $preview, $direction, $conv_id]);

    // Auto-unarchive on inbound message (only if archived, NOT if spam or blocked)
    if ($direction === 'inbound') {
        $pdo->prepare(
            "UPDATE conversations
             SET archived_at = NULL
             WHERE id = ? AND archived_at IS NOT NULL AND spam_at IS NULL AND blocked_at IS NULL"
        )->execute([$conv_id]);
    }
}

function collect_mms_media_urls(array $mms): array {
    $urls = [];

    // voip.ms returns media in a media[] array and/or col_media1/2/3 fields
    if (!empty($mms['media']) && is_array($mms['media'])) {
        foreach ($mms['media'] as $url) {
            if (is_string($url) && $url !== '') { $urls[] = $url; }
        }
    }

    for ($i = 1; $i <= 3; $i++) {
        $key = "col_media{$i}";
        if (!empty($mms[$key]) && is_string($mms[$key]) && !in_array($mms[$key], $urls)) {
            $urls[] = $mms[$key];
        }
    }

    return $urls;
}

function download_media(string $url, int $message_id, int $index, PDO $pdo): bool {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $data = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);

    if ($data === false || $http_code !== 200) {
        return false;
    }

    // Determine extension from content type
    $ext_map = [
        'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp',
        'video/mp4' => 'mp4', 'video/3gpp' => '3gp', 'audio/amr' => 'amr', 'audio/mpeg' => 'mp3',
        'application/pdf' => 'pdf',
    ];
    $mime = explode(';', $content_type ?? '')[0];
    $ext = $ext_map[trim($mime)] ?? pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'bin';

    // Build filename: {message_id}_{index}_{8char_hash}.{ext}
    $hash = substr(md5($url . $message_id . $index), 0, 8);
    $filename = "{$message_id}_{$index}_{$hash}.{$ext}";
    $filepath = MEDIA_DIR . '/' . $filename;

    if (file_put_contents($filepath, $data) === false) {
        return false;
    }

    $file_size = strlen($data);

    $stmt = $pdo->prepare(
        "INSERT INTO media (message_id, voipms_url, cached_filename, mime_type, file_size)
         VALUES (?, ?, ?, ?, ?)"
    );
    $stmt->execute([$message_id, $url, $filename, $mime, $file_size]);

    return true;
}

function voipms_call(string $url, ?string $post_data = null, bool $is_post = false) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    if ($is_post && $post_data !== null) {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
    }

    $response = curl_exec($ch);
    $curl_error = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
        http_response_code(502);
        echo json_encode(['status' => 'error', 'error' => 'Failed to reach voip.ms API', 'detail' => $curl_error]);
        return null;
    }

    $decoded = json_decode($response, true);
    if ($decoded === null) {
        http_response_code(502);
        echo json_encode(['status' => 'error', 'error' => 'Invalid response from voip.ms']);
        return null;
    }

    return $decoded;
}
