<?php
// ─────────────────────────────────────────────────────────────
//  api/booking.php — All booking API routes (public + admin)
// ─────────────────────────────────────────────────────────────
require_once dirname(__DIR__) . '/lib.php';
require_once dirname(__DIR__) . '/lib/BookingEngine.php';
if (class_exists('\Stripe\Stripe') || file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
    if (file_exists(dirname(__DIR__) . '/lib/StripeBooking.php'))
        require_once dirname(__DIR__) . '/lib/StripeBooking.php';
}

cors();
session_start_safe();
init_booking_db();
init_sys_db();

$method = $_SERVER['REQUEST_METHOD'];

if (!empty($GLOBALS['BOOKING_ROUTE_SEGS']) && is_array($GLOBALS['BOOKING_ROUTE_SEGS'])) {
    // Routed via api/index.php with normalized segments.
    $segs = array_values(array_filter($GLOBALS['BOOKING_ROUTE_SEGS'], 'strlen'));
} else {
    $uri  = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    $base = rtrim(dirname(dirname($_SERVER['SCRIPT_NAME'])), '/');
    $path = '/' . trim(substr($uri, strlen($base)), '/');
    $segs = array_values(array_filter(explode('/', $path)));

    // Direct hit or fallback include: trim /api/booking prefix when present.
    if (isset($segs[0]) && $segs[0] === 'api') array_shift($segs);
    if (isset($segs[0]) && $segs[0] === 'booking') array_shift($segs);
}

$res = $segs[0] ?? '';
$id  = isset($segs[1]) ? $segs[1] : null;
$sub = isset($segs[2]) ? $segs[2] : null;

function bk_norm_phone(string $phone): string {
    return substr((string)preg_replace('/[^0-9\+]/', '', trim($phone)), 0, 24);
}
function bk_validate_profile(array $b, bool $require = false): array {
    $name = trim((string)($b['name'] ?? ''));
    $phone = bk_norm_phone((string)($b['phone'] ?? ''));
    $dob = trim((string)($b['dob'] ?? ''));
    $gender = strtolower(trim((string)($b['gender'] ?? '')));
    $addressLine1 = trim((string)($b['address_line1'] ?? ''));
    $city = trim((string)($b['city'] ?? ''));
    $country = trim((string)($b['country'] ?? ''));
    $emergencyName = trim((string)($b['emergency_contact_name'] ?? ''));
    $emergencyPhone = bk_norm_phone((string)($b['emergency_contact_phone'] ?? ''));
    if ($require) {
        foreach (['phone'=>$phone,'dob'=>$dob,'gender'=>$gender,'address_line1'=>$addressLine1,'city'=>$city,'country'=>$country,'emergency_contact_name'=>$emergencyName,'emergency_contact_phone'=>$emergencyPhone] as $k=>$v) {
            if ($v === '') json_out(['ok'=>false,'error'=>"{$k} is required"], 422);
        }
    }
    if ($phone !== '' && !preg_match('/^\+?[0-9]{7,20}$/', $phone)) json_out(['ok'=>false,'error'=>'Invalid phone format'], 422);
    if ($emergencyPhone !== '' && !preg_match('/^\+?[0-9]{7,20}$/', $emergencyPhone)) json_out(['ok'=>false,'error'=>'Invalid emergency contact phone'], 422);
    if ($dob !== '') {
        $d = DateTime::createFromFormat('Y-m-d', $dob);
        if (!$d || $d->format('Y-m-d') !== $dob) json_out(['ok'=>false,'error'=>'Invalid DOB format'], 422);
    }
    if ($gender !== '' && !in_array($gender, ['male','female','other'], true)) json_out(['ok'=>false,'error'=>'Invalid gender'], 422);
    return [$name, $phone, $dob, $gender, $addressLine1, $city, $country, $emergencyName, $emergencyPhone];
}

// ── Stripe webhook (must read raw body before any output) ────
if ($method === 'POST' && $res === 'stripe' && $id === 'webhook') {
    $payload   = file_get_contents('php://input');
    $sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
    try {
        StripeBooking::handleWebhook($payload, $sigHeader);
        json_out(['ok' => true]);
    } catch (\Throwable $e) {
        error_log('Stripe webhook error: ' . $e->getMessage());
        json_out(['ok' => false, 'error' => $e->getMessage()], 400);
    }
    exit;
}

// ── Module gating (single business) ───────────────────────────
// Keep Stripe webhook reachable even if bookings module is disabled.
$bookingsEnabled   = ModuleToggle::isEnabled('bookings');
$membershipEnabled = ModuleToggle::isEnabled('membership');
$isAdminRoute = ($res === 'admin');
if (!$bookingsEnabled && !$isAdminRoute) {
    json_out(['ok' => false, 'error' => 'module_disabled', 'module' => 'bookings'], 404);
}

// ── Parse JSON body for non-webhook POSTs/PUTs ───────────────
$body = [];
if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
    $raw = file_get_contents('php://input');
    if ($raw) $body = json_decode($raw, true) ?? [];
    // Also check $_POST
    if (empty($body) && !empty($_POST)) $body = $_POST;
}

try {

// ════════════════════════════════════════════════════════════
//  PUBLIC ROUTES
// ════════════════════════════════════════════════════════════

// GET /api/booking/config — returns Stripe publishable key + currency
if ($method === 'GET' && $res === 'config' && $id === null) {
    $mods = ModuleToggle::all();
    if (class_exists('StripeBooking')) {
        json_out(array_merge(['ok' => true], StripeBooking::publicConfig(), [
            'app_name' => defined('APP_NAME') ? APP_NAME : 'Booking',
            'show_plans' => (bool)($mods['membership'] ?? true),
            'modules' => $mods,
        ]));
    }
    json_out([
        'ok'          => true,
        'mode'        => defined('STRIPE_MODE') ? STRIPE_MODE : 'test',
        'stripe_key'  => defined('STRIPE_PUBLISHABLE_KEY') ? STRIPE_PUBLISHABLE_KEY : '',
        'currency'    => defined('STRIPE_CURRENCY') ? STRIPE_CURRENCY : 'usd',
        'stripe_ready'=> defined('STRIPE_SECRET_KEY') && STRIPE_SECRET_KEY !== '',
        'app_name'    => defined('APP_NAME') ? APP_NAME : 'Booking',
        'show_plans'  => (bool)($mods['membership'] ?? true),
        'modules'     => $mods,
    ]);
}

// GET /api/booking/plans
if ($method === 'GET' && $res === 'plans' && $id === null) {
    $plans = db()->query(
        'SELECT * FROM bk_plans WHERE is_active=1 ORDER BY price ASC'
    )->fetchAll();

    foreach ($plans as &$p) {
        $svcs = db()->prepare(
            'SELECT s.id, s.name, s.duration_min
             FROM bk_plan_services ps
             JOIN bk_services s ON s.id = ps.service_id
             WHERE ps.plan_id=? AND s.is_active=1'
        );
        $svcs->execute([$p['id']]);
        $p['services'] = $svcs->fetchAll();
        $p['price'] = (float)$p['price'];
    }
    json_out(['ok' => true, 'plans' => $plans]);
}

// GET /api/booking/services
if ($method === 'GET' && $res === 'services' && $id === null) {
    $svcs = db()->query(
        'SELECT * FROM bk_services WHERE is_active=1 ORDER BY name'
    )->fetchAll();
    json_out(['ok' => true, 'services' => $svcs]);
}

// GET /api/booking/providers?service_id=X
if ($method === 'GET' && $res === 'providers' && $id === null) {
    $serviceId = (int)($_GET['service_id'] ?? 0);
    if ($serviceId) {
        $rows = db()->prepare(
            'SELECT p.id, p.name, p.branch_name, p.bio, p.avatar_url,
                    ps.price, ps.capacity_per_slot
             FROM bk_providers p
             JOIN bk_provider_services ps ON ps.provider_id = p.id
             WHERE ps.service_id=? AND p.is_active=1
             ORDER BY p.name'
        );
        $rows->execute([$serviceId]);
    } else {
        $rows = db()->query('SELECT * FROM bk_providers WHERE is_active=1 ORDER BY name');
    }
    $providers = $rows->fetchAll();
    foreach ($providers as &$pr) {
        if (isset($pr['price'])) $pr['price'] = $pr['price'] !== null ? (float)$pr['price'] : null;
    }
    json_out(['ok' => true, 'providers' => $providers]);
}

// GET /api/booking/slots?service_id=X&provider_id=Y&date=YYYY-MM-DD
if ($method === 'GET' && $res === 'slots') {
    $serviceId  = (int)($_GET['service_id'] ?? 0);
    $providerId = (int)($_GET['provider_id'] ?? 0);
    $date       = preg_replace('/[^0-9\-]/', '', $_GET['date'] ?? '');

    if (!$serviceId || !$providerId || !$date) {
        json_out(['ok' => false, 'error' => 'Missing service_id, provider_id, or date'], 400);
    }

    $slots = BookingEngine::getSlots($providerId, $serviceId, $date);
    json_out(['ok' => true, 'slots' => $slots]);
}

// POST /api/booking/check-membership
if ($method === 'POST' && $res === 'check-membership') {
    $email = trim(strtolower($body['email'] ?? ''));
    if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        json_out(['ok' => false, 'error' => 'Valid email required'], 400);
    }

    $cust = db()->prepare('SELECT * FROM bk_customers WHERE email=?');
    $cust->execute([$email]);
    $customer = $cust->fetch();

    if (!$customer) {
        json_out(['ok' => true, 'found' => false]);
    }

    $mem = db()->prepare(
        'SELECT m.*, p.name as plan_name FROM bk_memberships m
         JOIN bk_plans p ON p.id = m.plan_id
         WHERE m.customer_id=? AND m.status="active" AND m.expires_at > NOW()
         ORDER BY m.expires_at DESC LIMIT 1'
    );
    $mem->execute([$customer['id']]);
    $membership = $mem->fetch();

    if (!$membership) {
        json_out(['ok' => true, 'found' => false, 'customer_exists' => true]);
    }

    $services = BookingEngine::getUnlockedServices($email);

    json_out([
        'ok'               => true,
        'found'            => true,
        'customer'         => ['id' => $customer['id'], 'email' => $customer['email'], 'name' => $customer['name'], 'phone' => $customer['phone']],
        'membership'       => ['id' => $membership['id'], 'plan_name' => $membership['plan_name'], 'expires_at' => $membership['expires_at'], 'status' => $membership['status']],
        'unlocked_services'=> $services,
    ]);
}

// POST /api/booking/appointments
if ($method === 'POST' && $res === 'appointments') {
    $email      = trim(strtolower($body['email'] ?? ''));
    $name       = trim($body['name'] ?? '');
    $phone      = trim($body['phone'] ?? '');
    $serviceId  = (int)($body['service_id'] ?? 0);
    $providerId = (int)($body['provider_id'] ?? 0);
    $datetime   = $body['datetime'] ?? '';
    $notes      = trim($body['notes'] ?? '');

    if (!$email || !$name || !$serviceId || !$providerId || !$datetime) {
        json_out(['ok' => false, 'error' => 'Missing required fields'], 400);
    }
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        json_out(['ok' => false, 'error' => 'Invalid email'], 400);
    }

    // Validate datetime format
    $dtObj = DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
    if (!$dtObj) $dtObj = DateTime::createFromFormat('Y-m-d H:i', $datetime);
    if (!$dtObj) json_out(['ok' => false, 'error' => 'Invalid datetime format'], 400);
    $datetime = $dtObj->format('Y-m-d H:i:s');

    // Get service — check restricted flag first
    $svcRow = db()->prepare('SELECT duration_min, is_restricted FROM bk_services WHERE id=? AND is_active=1');
    $svcRow->execute([$serviceId]);
    $svc = $svcRow->fetch();
    if (!$svc) json_out(['ok' => false, 'error' => 'Service not found'], 400);

    // 1) Membership toggle behavior:
    // - membership module OFF => allow bookings (even for restricted services)
    // - membership module ON  => restricted services require active membership
    $access = ['allowed' => true, 'membership' => null];
    if ($membershipEnabled) {
        if ((int)$svc['is_restricted'] === 0) {
            $access = ['allowed' => true, 'membership' => null];
        } else {
            $access = BookingEngine::checkAccess($email, $serviceId);
            if (!$access['allowed']) {
                json_out(['ok' => false, 'error' => 'membership_required', 'reason' => $access['reason']], 403);
            }
        }
    }

    $endDatetime = date('Y-m-d H:i:s', strtotime($datetime) + $svc['duration_min'] * 60);

    // Wrap in transaction to prevent race conditions
    db()->beginTransaction();
    try {
        // Re-check availability inside transaction
        if (!BookingEngine::isSlotAvailable($providerId, $serviceId, $datetime)) {
            db()->rollBack();
            json_out(['ok' => false, 'error' => 'This slot is no longer available'], 409);
        }

        // Get or create customer
        $custRow = db()->prepare('SELECT id FROM bk_customers WHERE email=?');
        $custRow->execute([$email]);
        $cust = $custRow->fetch();
        if (!$cust) {
            db()->prepare('INSERT INTO bk_customers (email, name, phone) VALUES (?, ?, ?)')
                ->execute([$email, $name, $phone]);
            $customerId = (int) db()->lastInsertId();
        } else {
            $customerId = (int) $cust['id'];
            // Update name/phone if provided
            if ($name || $phone) {
                db()->prepare('UPDATE bk_customers SET name=COALESCE(NULLIF(?,\'\'),name), phone=COALESCE(NULLIF(?,\'\'),phone) WHERE id=?')
                    ->execute([$name, $phone, $customerId]);
            }
        }

        $ref = BookingEngine::genRef();
        $membershipId = $access['membership'] ? (int)$access['membership']['id'] : null;

        // 2) Session-limited enforcement at booking time (atomic):
        // If the active plan has a session_limit, require sessions_remaining > 0 and decrement now.
        if ($membershipEnabled && $membershipId) {
            $mRow = db()->prepare(
                'SELECT m.id, m.plan_id, m.sessions_total, m.sessions_remaining,
                        p.session_limit
                 FROM bk_memberships m
                 JOIN bk_plans p ON p.id = m.plan_id
                 WHERE m.id = ? AND m.status="active" AND m.expires_at > NOW()
                 FOR UPDATE'
            );
            $mRow->execute([$membershipId]);
            $m = $mRow->fetch();
            if (!$m) {
                db()->rollBack();
                json_out(['ok' => false, 'error' => 'membership_invalid'], 403);
            }
            $sessionLimit = $m['session_limit'] !== null ? (int)$m['session_limit'] : null;
            if ($sessionLimit !== null) {
                $remaining = $m['sessions_remaining'];
                // Initialize balances if missing (upgrade-safe)
                if ($remaining === null) {
                    db()->prepare('UPDATE bk_memberships SET sessions_total=?, sessions_remaining=? WHERE id=?')
                        ->execute([$sessionLimit, $sessionLimit, $membershipId]);
                    $remaining = $sessionLimit;
                } else {
                    $remaining = (int)$remaining;
                }

                if ($remaining <= 0) {
                    db()->rollBack();
                    json_out(['ok' => false, 'error' => 'sessions_exhausted'], 403);
                }

                // Decrement now (booking-time blocking)
                $dec = db()->prepare('UPDATE bk_memberships SET sessions_remaining = sessions_remaining - 1 WHERE id=? AND sessions_remaining > 0');
                $dec->execute([$membershipId]);
                if ($dec->rowCount() !== 1) {
                    db()->rollBack();
                    json_out(['ok' => false, 'error' => 'sessions_exhausted'], 403);
                }
            }
        }

        db()->prepare(
            'INSERT INTO bk_appointments
             (ref, customer_id, provider_id, service_id, membership_id,
              start_datetime, end_datetime, notes, status)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, "confirmed")'
        )->execute([$ref, $customerId, $providerId, $serviceId, $membershipId,
                    $datetime, $endDatetime, $notes]);

        $apptId = (int) db()->lastInsertId();
        db()->commit();

    } catch (\Throwable $e) {
        db()->rollBack();
        throw $e;
    }

    // Load full appointment for response + email
    $apptRow = db()->prepare(
        'SELECT a.*, c.name as customer_name, c.email as customer_email,
                s.name as service_name, p.name as provider_name, p.email as provider_email
         FROM bk_appointments a
         JOIN bk_customers c ON c.id = a.customer_id
         JOIN bk_services s ON s.id = a.service_id
         JOIN bk_providers p ON p.id = a.provider_id
         WHERE a.id=?'
    );
    $apptRow->execute([$apptId]);
    $appt = $apptRow->fetch();

    // Send confirmation emails
    $bookUrl = APP_URL . '/booking/book.php';
    $apptDate = date('D, d M Y \a\t g:ia', strtotime($datetime));
    $htmlCust = "
        <h2>Booking Confirmed — {$appt['service_name']}</h2>
        <p>Hi {$name},</p>
        <p>Your appointment is confirmed.</p>
        <table>
            <tr><td><strong>Reference:</strong></td><td>{$ref}</td></tr>
            <tr><td><strong>Service:</strong></td><td>{$appt['service_name']}</td></tr>
            <tr><td><strong>Provider:</strong></td><td>{$appt['provider_name']}</td></tr>
            <tr><td><strong>Date &amp; Time:</strong></td><td>{$apptDate}</td></tr>
        </table>
        <p><a href=\"{$bookUrl}\">Book another appointment →</a></p>";
    $textCust = "Booking Confirmed — {$ref}\nService: {$appt['service_name']}\nProvider: {$appt['provider_name']}\nDate: {$apptDate}";

    try {
        send_mail($email, $name, "Booking Confirmed — {$appt['service_name']} with {$appt['provider_name']}", $htmlCust, $textCust);
        db()->prepare('UPDATE bk_appointments SET email_sent=1 WHERE id=?')->execute([$apptId]);
    } catch (\Throwable $e) {
        error_log('Booking confirmation email error: ' . $e->getMessage());
    }

    // Notify provider
    if ($appt['provider_email']) {
        $htmlProv = "
            <h2>New Booking — {$appt['customer_name']}</h2>
            <p>A new appointment has been booked with you.</p>
            <table>
                <tr><td><strong>Reference:</strong></td><td>{$ref}</td></tr>
                <tr><td><strong>Customer:</strong></td><td>{$appt['customer_name']} ({$email})</td></tr>
                <tr><td><strong>Service:</strong></td><td>{$appt['service_name']}</td></tr>
                <tr><td><strong>Date &amp; Time:</strong></td><td>{$apptDate}</td></tr>
            </table>";
        $textProv = "New Booking — {$ref}\nCustomer: {$appt['customer_name']}\nService: {$appt['service_name']}\nDate: {$apptDate}";
        try {
            send_mail($appt['provider_email'], $appt['provider_name'], "New Booking — {$appt['customer_name']} for {$appt['service_name']}", $htmlProv, $textProv);
        } catch (\Throwable $e) {
            error_log('Provider notification email error: ' . $e->getMessage());
        }
    }

    json_out(['ok' => true, 'ref' => $ref, 'appointment' => $appt]);
}

// POST /api/booking/waitlist
if ($method === 'POST' && $res === 'waitlist' && $id === null) {
    $email      = trim(strtolower($body['email'] ?? ''));
    $name       = trim($body['name'] ?? '');
    $serviceId  = (int)($body['service_id'] ?? 0);
    $providerId = (int)($body['provider_id'] ?? 0);
    $slotDt     = $body['slot_datetime'] ?? '';

    if (!$email || !$name || !$serviceId || !$providerId || !$slotDt) {
        json_out(['ok' => false, 'error' => 'Missing required fields'], 400);
    }

    db()->prepare(
        'INSERT INTO bk_waitlist (customer_email, customer_name, provider_id, service_id, slot_datetime)
         VALUES (?, ?, ?, ?, ?)'
    )->execute([$email, $name, $providerId, $serviceId, $slotDt]);

    json_out(['ok' => true]);
}

// POST /api/booking/validate-promo
if ($method === 'POST' && $res === 'validate-promo') {
    $code   = strtoupper(trim($body['code'] ?? ''));
    $planId = (int)($body['plan_id'] ?? 0);
    if (!$code) json_out(['ok' => false, 'error' => 'Code required'], 400);

    $stmt = db()->prepare(
        'SELECT * FROM bk_promo_codes
         WHERE code=? AND is_active=1
           AND (valid_from IS NULL OR valid_from <= NOW())
           AND (valid_until IS NULL OR valid_until >= NOW())
           AND (max_uses IS NULL OR uses_count < max_uses)
         LIMIT 1'
    );
    $stmt->execute([$code]);
    $promo = $stmt->fetch();

    if (!$promo) json_out(['ok' => false, 'error' => 'Invalid or expired promo code'], 404);

    // Check plan restriction
    if ($promo['plan_ids'] && $planId) {
        $allowed = json_decode($promo['plan_ids'], true) ?: [];
        if (!empty($allowed) && !in_array($planId, $allowed)) {
            json_out(['ok' => false, 'error' => 'This code is not valid for the selected plan'], 400);
        }
    }

    json_out([
        'ok'             => true,
        'code'           => $promo['code'],
        'discount_type'  => $promo['discount_type'],
        'discount_value' => (float)$promo['discount_value'],
        'description'    => $promo['description'],
    ]);
}

// POST /api/booking/rate — submit a rating after appointment
if ($method === 'POST' && $res === 'rate') {
    $ref     = trim($body['ref'] ?? '');
    $rating  = (int)($body['rating'] ?? 0);
    $comment = trim($body['comment'] ?? '');
    $email   = trim(strtolower($body['email'] ?? ''));

    if (!$ref || !$rating || !$email) json_out(['ok' => false, 'error' => 'Missing required fields'], 400);
    if ($rating < 1 || $rating > 5) json_out(['ok' => false, 'error' => 'Rating must be 1-5'], 400);

    // Get appointment + verify customer owns it
    $apptRow = db()->prepare(
        'SELECT a.*, c.email as customer_email FROM bk_appointments a
         JOIN bk_customers c ON c.id = a.customer_id
         WHERE a.ref=?'
    );
    $apptRow->execute([$ref]);
    $appt = $apptRow->fetch();
    if (!$appt) json_out(['ok' => false, 'error' => 'Appointment not found'], 404);
    if (strtolower($appt['customer_email']) !== $email) json_out(['ok' => false, 'error' => 'Email does not match booking'], 403);
    if (!in_array($appt['status'], ['completed', 'confirmed'])) {
        json_out(['ok' => false, 'error' => 'Can only rate completed appointments'], 400);
    }

    try {
        db()->prepare(
            'INSERT INTO bk_ratings (appointment_id, customer_id, provider_id, service_id, rating, comment)
             VALUES (?, ?, ?, ?, ?, ?)'
        )->execute([$appt['id'], $appt['customer_id'], $appt['provider_id'], $appt['service_id'], $rating, $comment]);
    } catch (\Throwable $e) {
        json_out(['ok' => false, 'error' => 'You have already rated this appointment'], 409);
    }

    json_out(['ok' => true]);
}

// GET /api/booking/provider-ratings?provider_id=X
if ($method === 'GET' && $res === 'provider-ratings') {
    $providerId = (int)($_GET['provider_id'] ?? 0);
    if (!$providerId) json_out(['ok' => false, 'error' => 'provider_id required'], 400);

    $stmt = db()->prepare(
        'SELECT r.rating, r.comment, r.created_at,
                c.name as customer_name,
                s.name as service_name
         FROM bk_ratings r
         JOIN bk_customers c ON c.id = r.customer_id
         JOIN bk_services s ON s.id = r.service_id
         WHERE r.provider_id=? AND r.is_public=1
         ORDER BY r.created_at DESC LIMIT 20'
    );
    $stmt->execute([$providerId]);
    $reviews = $stmt->fetchAll();

    $avg = db()->prepare('SELECT AVG(rating) as avg, COUNT(*) as cnt FROM bk_ratings WHERE provider_id=? AND is_public=1');
    $avg->execute([$providerId]);
    $stats = $avg->fetch();

    json_out([
        'ok'      => true,
        'reviews' => $reviews,
        'average' => round((float)($stats['avg'] ?? 0), 1),
        'count'   => (int)($stats['cnt'] ?? 0),
    ]);
}


// POST /api/booking/stripe/checkout  (promo-code-aware)
if ($method === 'POST' && $res === 'stripe' && $id === 'checkout') {
    $planId     = (int)($body['plan_id'] ?? 0);
    $email      = trim($body['email'] ?? '');
    $name       = trim($body['name'] ?? '');
    $phone      = trim((string)($body['phone'] ?? ''));
    $dob        = trim((string)($body['dob'] ?? ''));
    $gender     = trim((string)($body['gender'] ?? ''));
    $addressLine1 = trim((string)($body['address_line1'] ?? ''));
    $city       = trim((string)($body['city'] ?? ''));
    $country    = trim((string)($body['country'] ?? ''));
    $emergencyName = trim((string)($body['emergency_contact_name'] ?? ''));
    $emergencyPhone = trim((string)($body['emergency_contact_phone'] ?? ''));
    $promoCode  = strtoupper(trim($body['promo_code'] ?? ''));
    $successUrl = $body['success_url'] ?? APP_URL . '/booking/plans.php';
    $cancelUrl  = $body['cancel_url']  ?? APP_URL . '/booking/plans.php';

    if (!$planId || !$email || !$name) {
        json_out(['ok' => false, 'error' => 'Missing plan_id, email, or name'], 400);
    }
    if (!class_exists('StripeBooking')) {
        json_out(['ok' => false, 'error' => 'Stripe not configured'], 503);
    }

    $discountAmount = 0.0;
    $validPromo     = null;
    if ($promoCode) {
        $ps = db()->prepare(
            'SELECT * FROM bk_promo_codes
             WHERE code=? AND is_active=1
               AND (valid_from IS NULL OR valid_from <= NOW())
               AND (valid_until IS NULL OR valid_until >= NOW())
               AND (max_uses IS NULL OR uses_count < max_uses)
             LIMIT 1'
        );
        $ps->execute([$promoCode]);
        $validPromo = $ps->fetch();
        if ($validPromo) {
            $planRow = db()->prepare('SELECT price FROM bk_plans WHERE id=?');
            $planRow->execute([$planId]);
            $plan = $planRow->fetch();
            if ($plan) {
                $price = (float)$plan['price'];
                $discountAmount = $validPromo['discount_type'] === 'percent'
                    ? round($price * (float)$validPromo['discount_value'] / 100, 2)
                    : min((float)$validPromo['discount_value'], $price);
            }
        }
    }

    $checkoutUrl = StripeBooking::createCheckout(
        $planId,
        $email,
        $name,
        $successUrl,
        $cancelUrl,
        $discountAmount,
        [
            'phone' => $phone,
            'dob' => $dob,
            'gender' => $gender,
            'address_line1' => $addressLine1,
            'city' => $city,
            'country' => $country,
            'emergency_contact_name' => $emergencyName,
            'emergency_contact_phone' => $emergencyPhone,
        ]
    );
    if ($validPromo) {
        db()->prepare('UPDATE bk_promo_codes SET uses_count=uses_count+1 WHERE id=?')->execute([$validPromo['id']]);
    }
    json_out(['ok' => true, 'checkout_url' => $checkoutUrl,
              'discount_applied' => $discountAmount > 0, 'discount_amount' => $discountAmount]);
}

// ════════════════════════════════════════════════════════════
//  ADMIN ROUTES — require admin auth
// ════════════════════════════════════════════════════════════

if ($res === 'admin') {
    api_require_admin();
    $role = current_admin_role();
    $resource = (string)($segs[1] ?? '');
    api_require_role_for($resource, $method);
    $adminRes = $segs[1] ?? '';

    // ── Modules: feature toggles (single business) ─────────────
    if ($adminRes === 'modules') {
        if ($method === 'GET') {
            json_out(['ok' => true, 'modules' => ModuleToggle::all()]);
        }
        if ($method === 'PUT') {
            $allowed = ['forms', 'bookings', 'membership', 'qr_checkin'];
            $incoming = $body['modules'] ?? $body ?? [];
            if (!is_array($incoming)) json_out(['ok' => false, 'error' => 'Invalid modules payload'], 400);
            $toSet = [];
            foreach ($allowed as $k) {
                if (array_key_exists($k, $incoming)) $toSet[$k] = (bool)$incoming[$k];
            }
            if (!$toSet) json_out(['ok' => false, 'error' => 'No module keys provided'], 400);
            ModuleToggle::setMany($toSet);
            json_out(['ok' => true, 'modules' => ModuleToggle::all()]);
        }
        json_out(['ok' => false, 'error' => 'Method not allowed'], 405);
    }

    // ── QR / Check-in logging (attendance for booked sessions) ──
    if ($adminRes === 'checkin' && $method === 'POST') {
        if (!ModuleToggle::isEnabled('qr_checkin')) {
            json_out(['ok' => false, 'error' => 'module_disabled', 'module' => 'qr_checkin'], 404);
        }

        $qr = trim((string)($body['qr_token'] ?? $body['token'] ?? ''));
        $appointmentRef = trim((string)($body['appointment_ref'] ?? ''));
        if ($qr === '') json_out(['ok' => false, 'error' => 'qr_token required'], 400);

        // Expect token format: base64url(payload).base64url(sig)
        $parts = explode('.', $qr);
        if (count($parts) !== 2) json_out(['ok' => false, 'error' => 'Invalid token'], 400);
        [$p64, $s64] = $parts;
        $payloadJson = base64_decode(strtr($p64, '-_', '+/'), true);
        $sig         = base64_decode(strtr($s64, '-_', '+/'), true);
        if ($payloadJson === false || $sig === false) json_out(['ok' => false, 'error' => 'Invalid token'], 400);

        if (!defined('QR_SIGNING_SECRET') || !QR_SIGNING_SECRET) {
            json_out(['ok' => false, 'error' => 'QR signing not configured'], 500);
        }
        $calc = hash_hmac('sha256', $payloadJson, QR_SIGNING_SECRET, true);
        if (!hash_equals($calc, $sig)) json_out(['ok' => false, 'error' => 'Invalid token'], 400);

        $payload = json_decode($payloadJson, true);
        if (!is_array($payload)) json_out(['ok' => false, 'error' => 'Invalid token'], 400);
        $membershipId = (int)($payload['membership_id'] ?? 0);
        $customerId   = (int)($payload['customer_id'] ?? 0);
        $exp          = (int)($payload['exp'] ?? 0);
        if (!$membershipId || !$customerId) json_out(['ok' => false, 'error' => 'Invalid token'], 400);
        if ($exp && time() > $exp) json_out(['ok' => false, 'error' => 'Token expired'], 400);

        $apptId = null;
        $serviceId = null;
        if ($appointmentRef !== '') {
            $a = db()->prepare('SELECT id, service_id, status, membership_id FROM bk_appointments WHERE ref=? AND customer_id=?');
            $a->execute([$appointmentRef, $customerId]);
            $ar = $a->fetch();
            if (!$ar) json_out(['ok' => false, 'error' => 'appointment_not_found'], 404);
            if (in_array($ar['status'], ['cancelled','no_show'], true)) json_out(['ok' => false, 'error' => 'appointment_not_active'], 409);
            if (!empty($ar['membership_id']) && (int)$ar['membership_id'] !== $membershipId) {
                json_out(['ok' => false, 'error' => 'appointment_membership_mismatch'], 409);
            }
            $apptId = (int)$ar['id'];
            $serviceId = (int)$ar['service_id'];
        } else {
            // If no reference is provided, auto-link to the nearest active appointment
            // for this member/customer so check-in can always mark attendance completed.
            $a = db()->prepare(
                'SELECT id, service_id
                 FROM bk_appointments
                 WHERE customer_id=?
                   AND (membership_id IS NULL OR membership_id=?)
                   AND status IN ("confirmed","pending")
                   AND ABS(TIMESTAMPDIFF(MINUTE, start_datetime, NOW())) <= 720
                 ORDER BY ABS(TIMESTAMPDIFF(MINUTE, start_datetime, NOW())) ASC
                 LIMIT 1'
            );
            $a->execute([$customerId, $membershipId]);
            $ar = $a->fetch();
            if ($ar) {
                $apptId = (int)$ar['id'];
                $serviceId = (int)$ar['service_id'];
            }
        }

        // Verify membership is active
        $ms = db()->prepare('SELECT id FROM bk_memberships WHERE id=? AND customer_id=? AND status="active" AND expires_at > NOW()');
        $ms->execute([$membershipId, $customerId]);
        if (!$ms->fetch()) json_out(['ok' => false, 'error' => 'membership_invalid'], 403);

        db()->prepare('INSERT INTO bk_checkins (membership_id, customer_id, service_id, appointment_id, checked_in_by_admin_id, meta_json)
                       VALUES (?,?,?,?,?,?)')
            ->execute([
                $membershipId,
                $customerId,
                $serviceId,
                $apptId,
                (int)($_SESSION['admin_user_id'] ?? 0) ?: null,
                json_encode(['token' => ['exp' => $exp]], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
            ]);

        // If check-in is tied to an appointment, mark it completed (attendance = completed session)
        if ($apptId) {
            db()->prepare('UPDATE bk_appointments SET status="completed" WHERE id=? AND status NOT IN ("cancelled","no_show")')
              ->execute([$apptId]);
        }

        // Return updated membership balance for UI
        $bal = db()->prepare('SELECT sessions_total, sessions_remaining FROM bk_memberships WHERE id=?');
        $bal->execute([$membershipId]);
        $b = $bal->fetch() ?: [];
        json_out(['ok' => true, 'checked_in' => true, 'membership' => [
            'id' => $membershipId,
            'sessions_total' => $b['sessions_total'] ?? null,
            'sessions_remaining' => $b['sessions_remaining'] ?? null,
        ]]);
    }

    // ── Stripe config ──────────────────────────────────────────
    if ($adminRes === 'stripe-config') {
        $cfgFile = dirname(__DIR__) . '/config.php';
        if ($method === 'GET') {
            json_out(['ok' => true, 'stripe' => [
                'mode' => defined('STRIPE_MODE') ? STRIPE_MODE : 'test',
                'currency' => defined('STRIPE_CURRENCY') ? STRIPE_CURRENCY : 'usd',
                'test_publishable_key' => defined('STRIPE_TEST_PUBLISHABLE_KEY') ? STRIPE_TEST_PUBLISHABLE_KEY : (defined('STRIPE_PUBLISHABLE_KEY') ? STRIPE_PUBLISHABLE_KEY : ''),
                'test_secret_key' => defined('STRIPE_TEST_SECRET_KEY') ? STRIPE_TEST_SECRET_KEY : (defined('STRIPE_SECRET_KEY') ? STRIPE_SECRET_KEY : ''),
                'test_webhook_secret' => defined('STRIPE_TEST_WEBHOOK_SECRET') ? STRIPE_TEST_WEBHOOK_SECRET : (defined('STRIPE_WEBHOOK_SECRET') ? STRIPE_WEBHOOK_SECRET : ''),
                'live_publishable_key' => defined('STRIPE_LIVE_PUBLISHABLE_KEY') ? STRIPE_LIVE_PUBLISHABLE_KEY : '',
                'live_secret_key' => defined('STRIPE_LIVE_SECRET_KEY') ? STRIPE_LIVE_SECRET_KEY : '',
                'live_webhook_secret' => defined('STRIPE_LIVE_WEBHOOK_SECRET') ? STRIPE_LIVE_WEBHOOK_SECRET : '',
            ]]);
        }
        if ($method === 'PUT') {
            $mode = strtolower((string)($body['mode'] ?? 'test')) === 'live' ? 'live' : 'test';
            $currency = strtolower(trim((string)($body['currency'] ?? 'usd')));
            if (!preg_match('/^[a-z]{3}$/', $currency)) $currency = 'usd';

            $map = [
                'STRIPE_MODE' => $mode,
                'STRIPE_CURRENCY' => $currency,
                'STRIPE_TEST_PUBLISHABLE_KEY' => (string)($body['test_publishable_key'] ?? ''),
                'STRIPE_TEST_SECRET_KEY' => (string)($body['test_secret_key'] ?? ''),
                'STRIPE_TEST_WEBHOOK_SECRET' => (string)($body['test_webhook_secret'] ?? ''),
                'STRIPE_LIVE_PUBLISHABLE_KEY' => (string)($body['live_publishable_key'] ?? ''),
                'STRIPE_LIVE_SECRET_KEY' => (string)($body['live_secret_key'] ?? ''),
                'STRIPE_LIVE_WEBHOOK_SECRET' => (string)($body['live_webhook_secret'] ?? ''),
                // keep legacy constants aligned for compatibility
                'STRIPE_PUBLISHABLE_KEY' => $mode === 'live' ? (string)($body['live_publishable_key'] ?? '') : (string)($body['test_publishable_key'] ?? ''),
                'STRIPE_SECRET_KEY' => $mode === 'live' ? (string)($body['live_secret_key'] ?? '') : (string)($body['test_secret_key'] ?? ''),
                'STRIPE_WEBHOOK_SECRET' => $mode === 'live' ? (string)($body['live_webhook_secret'] ?? '') : (string)($body['test_webhook_secret'] ?? ''),
            ];

            $src = file_get_contents($cfgFile);
            if ($src === false) json_out(['ok' => false, 'error' => 'Cannot read config.php'], 500);
            foreach ($map as $const => $val) {
                $safe = addslashes($val);
                $replacement = "define('{$const}', '{$safe}')";
                if (preg_match("/define\('{$const}',\s*[^;]+\)/", $src)) {
                    $src = preg_replace("/define\('{$const}',\s*[^;]+\)/", $replacement, $src);
                } else {
                    $src .= "\ndefine('{$const}', '{$safe}');";
                }
            }
            if (file_put_contents($cfgFile, $src) === false) {
                json_out(['ok' => false, 'error' => 'Cannot write config.php'], 500);
            }
            json_out(['ok' => true]);
        }
        json_out(['ok' => false, 'error' => 'Method not allowed'], 405);
    }

    $adminId  = isset($segs[2]) ? $segs[2] : null;
    $adminSub = isset($segs[3]) ? $segs[3] : null;

    // ── Stats ─────────────────────────────────────────────────
    if ($method === 'GET' && $adminRes === 'stats') {
        $today = date('Y-m-d');
        $weekStart = date('Y-m-d', strtotime('monday this week'));
        $totalAppts  = (int) db()->query('SELECT COUNT(*) FROM bk_appointments')->fetchColumn();
        $stToday = db()->prepare('SELECT COUNT(*) FROM bk_appointments WHERE DATE(start_datetime)=?');
        $stToday->execute([$today]);
        $todayCount = (int) $stToday->fetchColumn();

        $stWeek = db()->prepare('SELECT COUNT(*) FROM bk_appointments WHERE DATE(start_datetime) >= ?');
        $stWeek->execute([$weekStart]);
        $weekCount = (int) $stWeek->fetchColumn();

        $stPending = db()->prepare('SELECT COUNT(*) FROM bk_appointments WHERE status="pending" AND DATE(start_datetime)=?');
        $stPending->execute([$today]);
        $pendingToday = (int) $stPending->fetchColumn();

        $activeMem = (int) db()->query('SELECT COUNT(*) FROM bk_memberships WHERE status="active" AND expires_at > NOW()')->fetchColumn();
        $expiredMem = (int) db()->query('SELECT COUNT(*) FROM bk_memberships WHERE status="expired" OR expires_at <= NOW()')->fetchColumn();

        $topSvcs = db()->query(
            'SELECT s.name, COUNT(*) as cnt FROM bk_appointments a
             JOIN bk_services s ON s.id = a.service_id
             GROUP BY a.service_id ORDER BY cnt DESC LIMIT 5'
        )->fetchAll();

        json_out([
            'ok'                  => true,
            'total_appointments'  => $totalAppts,
            'today'               => $todayCount,
            'appointments_today'  => $todayCount,
            'this_week'           => $weekCount,
            'appointments_week'   => $weekCount,
            'pending_today'       => $pendingToday,
            'active_memberships'  => $activeMem,
            'active_members'      => $activeMem,
            'expired_memberships' => $expiredMem,
            'top_services'        => $topSvcs,
            'total_plans'         => (int) db()->query('SELECT COUNT(*) FROM bk_plans WHERE is_active=1')->fetchColumn(),
            'total_services'      => (int) db()->query('SELECT COUNT(*) FROM bk_services WHERE is_active=1')->fetchColumn(),
            'total_providers'     => (int) db()->query('SELECT COUNT(*) FROM bk_providers WHERE is_active=1')->fetchColumn(),
        ]);
    }

    // ── Appointments ──────────────────────────────────────────
    if ($adminRes === 'appointments') {
        if ($method === 'POST' && $adminId === 'bulk') {
            $ids = array_values(array_filter(array_map('intval', (array)($body['ids'] ?? []))));
            if (!$ids) json_out(['ok'=>false,'error'=>'ids required'], 400);
            $action = (string)($body['action'] ?? '');
            $in = implode(',', array_fill(0, count($ids), '?'));
            if ($action === 'set_status') {
                $status = (string)($body['status'] ?? '');
                if (!in_array($status, ['pending','confirmed','cancelled','no_show','completed'], true)) json_out(['ok'=>false,'error'=>'Invalid status'], 400);
                $params = array_merge([$status], $ids);
                db()->prepare("UPDATE bk_appointments SET status=? WHERE id IN ($in)")->execute($params);
                json_out(['ok'=>true,'updated'=>count($ids)]);
            }
            if ($action === 'delete') {
                if (current_admin_role() !== 'admin') json_out(['ok'=>false,'error'=>'Forbidden: only admin can bulk delete'], 403);
                db()->prepare("DELETE FROM bk_appointments WHERE id IN ($in)")->execute($ids);
                json_out(['ok'=>true,'deleted'=>count($ids)]);
            }
            json_out(['ok'=>false,'error'=>'Unknown bulk action'], 400);
        }
        if ($method === 'GET' && $adminId === null) {
            $where = ['1=1'];
            $params = [];
            if (!empty($_GET['date'])) { $where[] = 'DATE(a.start_datetime)=?'; $params[] = $_GET['date']; }
            if (!empty($_GET['provider_id'])) { $where[] = 'a.provider_id=?'; $params[] = (int)$_GET['provider_id']; }
            if (!empty($_GET['service_id'])) { $where[] = 'a.service_id=?'; $params[] = (int)$_GET['service_id']; }
            if (!empty($_GET['status'])) { $where[] = 'a.status=?'; $params[] = $_GET['status']; }
            if (!empty($_GET['date_from'])) { $where[] = 'DATE(a.start_datetime)>=?'; $params[] = $_GET['date_from']; }
            if (!empty($_GET['date_to'])) { $where[] = 'DATE(a.start_datetime)<=?'; $params[] = $_GET['date_to']; }
            if (!empty($_GET['search'])) {
                $like = '%' . trim((string)$_GET['search']) . '%';
                $where[] = '(a.ref LIKE ? OR c.email LIKE ? OR c.name LIKE ? OR s.name LIKE ? OR p.name LIKE ?)';
                array_push($params, $like, $like, $like, $like, $like);
            }
            $page  = max(1, (int)($_GET['page'] ?? 1));
            $limit = 50;
            $offset = ($page - 1) * $limit;
            $whereStr = implode(' AND ', $where);

            $cntStmt = db()->prepare("SELECT COUNT(*) FROM bk_appointments a WHERE {$whereStr}");
            $cntStmt->execute($params);
            $total = (int) $cntStmt->fetchColumn();

            $stmt = db()->prepare(
                "SELECT a.*, c.name as customer_name, c.email as customer_email,
                        s.name as service_name, p.name as provider_name
                 FROM bk_appointments a
                 JOIN bk_customers c ON c.id = a.customer_id
                 JOIN bk_services s ON s.id = a.service_id
                 JOIN bk_providers p ON p.id = a.provider_id
                 WHERE {$whereStr}
                 ORDER BY a.start_datetime DESC LIMIT {$limit} OFFSET {$offset}"
            );
            $stmt->execute($params);
            $rows = $stmt->fetchAll();
            json_out(['ok' => true, 'appointments' => $rows, 'total' => $total, 'page' => $page, 'pages' => ceil($total/$limit)]);
        }

        if ($method === 'GET' && $adminId !== null) {
            $stmt = db()->prepare(
                'SELECT a.*, c.name as customer_name, c.email as customer_email,
                        c.phone as customer_phone,
                        s.name as service_name, s.duration_min,
                        p.name as provider_name
                 FROM bk_appointments a
                 JOIN bk_customers c ON c.id = a.customer_id
                 JOIN bk_services s ON s.id = a.service_id
                 JOIN bk_providers p ON p.id = a.provider_id
                 WHERE a.id=?'
            );
            $stmt->execute([(int)$adminId]);
            $row = $stmt->fetch();
            if (!$row) json_out(['ok' => false, 'error' => 'Not found'], 404);
            json_out(['ok' => true, 'appointment' => $row]);
        }

        if ($method === 'PUT' && $adminId !== null) {
            $status = $body['status'] ?? null;
            $notes  = $body['notes']  ?? null;
            $allowed = ['pending','confirmed','cancelled','no_show','completed'];
            if ($status && !in_array($status, $allowed)) {
                json_out(['ok' => false, 'error' => 'Invalid status'], 400);
            }
            $sets = []; $params = [];
            if ($status !== null) { $sets[] = 'status=?'; $params[] = $status; }
            if ($notes  !== null) { $sets[] = 'notes=?';  $params[] = $notes; }
            if (empty($sets)) json_out(['ok' => false, 'error' => 'Nothing to update'], 400);
            $params[] = (int)$adminId;
            db()->prepare('UPDATE bk_appointments SET ' . implode(',', $sets) . ' WHERE id=?')->execute($params);
            json_out(['ok' => true]);
        }

        if ($method === 'DELETE' && $adminId !== null) {
            db()->prepare('DELETE FROM bk_appointments WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok' => true]);
        }
    }

    // ── Memberships ───────────────────────────────────────────
    if ($adminRes === 'memberships') {
        if ($method === 'POST' && $adminId === 'bulk') {
            $ids = array_values(array_filter(array_map('intval', (array)($body['ids'] ?? []))));
            if (!$ids) json_out(['ok'=>false,'error'=>'ids required'], 400);
            $action = (string)($body['action'] ?? '');
            $in = implode(',', array_fill(0, count($ids), '?'));
            if ($action === 'set_status') {
                $status = (string)($body['status'] ?? '');
                if (!in_array($status, ['active','expired','cancelled'], true)) json_out(['ok'=>false,'error'=>'Invalid status'], 400);
                $params = array_merge([$status], $ids);
                db()->prepare("UPDATE bk_memberships SET status=? WHERE id IN ($in)")->execute($params);
                json_out(['ok'=>true,'updated'=>count($ids)]);
            }
            if ($action === 'delete') {
                if (current_admin_role() !== 'admin') json_out(['ok'=>false,'error'=>'Forbidden: only admin can bulk delete'], 403);
                db()->prepare("DELETE FROM bk_memberships WHERE id IN ($in)")->execute($ids);
                json_out(['ok'=>true,'deleted'=>count($ids)]);
            }
            json_out(['ok'=>false,'error'=>'Unknown bulk action'], 400);
        }
        if ($method === 'GET' && $adminId === null) {
            $where = ['1=1']; $params = [];
            if (!empty($_GET['status'])) {
                if ($_GET['status'] === 'active') { $where[] = 'm.status="active" AND m.expires_at > NOW()'; }
                elseif ($_GET['status'] === 'expired') { $where[] = '(m.status="expired" OR m.expires_at <= NOW())'; }
                else { $where[] = 'm.status=?'; $params[] = $_GET['status']; }
            }
            if (!empty($_GET['search'])) {
                $like = '%' . $_GET['search'] . '%';
                $where[] = '(c.email LIKE ? OR c.name LIKE ?)';
                $params[] = $like; $params[] = $like;
            }
            if (!empty($_GET['plan_id'])) { $where[] = 'm.plan_id=?'; $params[] = (int)$_GET['plan_id']; }
            if (!empty($_GET['date_from'])) { $where[] = 'DATE(m.starts_at)>=?'; $params[] = $_GET['date_from']; }
            if (!empty($_GET['date_to'])) { $where[] = 'DATE(m.starts_at)<=?'; $params[] = $_GET['date_to']; }
            $page = max(1, (int)($_GET['page'] ?? 1));
            $limit = 50; $offset = ($page - 1) * $limit;
            $whereStr = implode(' AND ', $where);

            $cntStmt = db()->prepare("SELECT COUNT(*) FROM bk_memberships m JOIN bk_customers c ON c.id=m.customer_id WHERE {$whereStr}");
            $cntStmt->execute($params);
            $total = (int) $cntStmt->fetchColumn();

            $stmt = db()->prepare(
                "SELECT m.*, c.name as customer_name, c.email as customer_email,
                        p.name as plan_name
                 FROM bk_memberships m
                 JOIN bk_customers c ON c.id = m.customer_id
                 JOIN bk_plans p ON p.id = m.plan_id
                 WHERE {$whereStr}
                 ORDER BY m.created_at DESC LIMIT {$limit} OFFSET {$offset}"
            );
            $stmt->execute($params);
            json_out(['ok' => true, 'memberships' => $stmt->fetchAll(), 'total' => $total, 'page' => $page, 'pages' => ceil($total/$limit)]);
        }

        if ($method === 'POST') {
            $custEmail = trim(strtolower($body['customer_email'] ?? ''));
            $planId    = (int)($body['plan_id'] ?? 0);
            $startsAt  = $body['starts_at'] ?? date('Y-m-d H:i:s');
            $notes     = $body['notes'] ?? '';

            if (!$custEmail || !$planId) json_out(['ok' => false, 'error' => 'customer_email and plan_id required'], 400);

            $planRow = db()->prepare('SELECT * FROM bk_plans WHERE id=?');
            $planRow->execute([$planId]);
            $plan = $planRow->fetch();
            if (!$plan) json_out(['ok' => false, 'error' => 'Plan not found'], 404);

            [$profileName, $profilePhone, $profileDob, $profileGender, $profileAddress, $profileCity, $profileCountry, $profileEmergName, $profileEmergPhone] =
                bk_validate_profile($body, false);
            $custRow = db()->prepare('SELECT id FROM bk_customers WHERE email=?');
            $custRow->execute([$custEmail]);
            $cust = $custRow->fetch();
            if (!$cust) {
                db()->prepare('INSERT INTO bk_customers (email, name, phone, dob, gender, address_line1, city, country, emergency_contact_name, emergency_contact_phone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
                    ->execute([
                        $custEmail,
                        $profileName ?: $custEmail,
                        $profilePhone ?: null,
                        $profileDob ?: null,
                        $profileGender ?: null,
                        $profileAddress ?: null,
                        $profileCity ?: null,
                        $profileCountry ?: null,
                        $profileEmergName ?: null,
                        $profileEmergPhone ?: null
                    ]);
                $customerId = (int) db()->lastInsertId();
            } else {
                $customerId = (int) $cust['id'];
                db()->prepare(
                    'UPDATE bk_customers SET
                        name=COALESCE(NULLIF(?,\'\'),name),
                        phone=COALESCE(NULLIF(?,\'\'),phone),
                        dob=COALESCE(NULLIF(?,\'\'),dob),
                        gender=COALESCE(NULLIF(?,\'\'),gender),
                        address_line1=COALESCE(NULLIF(?,\'\'),address_line1),
                        city=COALESCE(NULLIF(?,\'\'),city),
                        country=COALESCE(NULLIF(?,\'\'),country),
                        emergency_contact_name=COALESCE(NULLIF(?,\'\'),emergency_contact_name),
                        emergency_contact_phone=COALESCE(NULLIF(?,\'\'),emergency_contact_phone)
                     WHERE id=?'
                )->execute([
                    $profileName, $profilePhone, $profileDob, $profileGender, $profileAddress, $profileCity, $profileCountry, $profileEmergName, $profileEmergPhone, $customerId
                ]);
            }

            $validDays = (int)($plan['validity_days'] ?? 0);
            if ($validDays <= 0) $validDays = (int)($plan['duration_days'] ?? 30);
            $expiresAt = date('Y-m-d H:i:s', strtotime($startsAt . ' +' . $validDays . ' days'));
            $sessionLimit = $plan['session_limit'] !== null ? (int)$plan['session_limit'] : null;
            db()->prepare(
                'INSERT INTO bk_memberships (customer_id, plan_id, sessions_total, sessions_remaining, starts_at, expires_at, status, activated_by, notes)
                 VALUES (?, ?, ?, ?, ?, ?, "active", "manual", ?)'
            )->execute([$customerId, $planId, $sessionLimit, $sessionLimit, $startsAt, $expiresAt, $notes]);

            // Email
            $custNameRow = db()->prepare('SELECT name FROM bk_customers WHERE id=?');
            $custNameRow->execute([$customerId]);
            $custName = $custNameRow->fetchColumn() ?: $custEmail;
            $html = "<h2>Your Membership Has Been Activated</h2><p>Hi {$custName},</p><p>Your <strong>{$plan['name']}</strong> membership is now active until <strong>{$expiresAt}</strong>.</p><p><a href=\"" . APP_URL . "/booking/book.php\">Book an appointment →</a></p>";
            $text = "Membership activated: {$plan['name']} — expires {$expiresAt}";
            try { send_mail($custEmail, $custName, "Your Membership Has Been Activated", $html, $text); } catch(\Throwable $e) {}

            json_out(['ok' => true, 'id' => (int) db()->lastInsertId()]);
        }

        if ($method === 'PUT' && $adminId !== null) {
            $sets = []; $params = [];
            if (isset($body['status']))     { $sets[] = 'status=?';     $params[] = $body['status']; }
            if (isset($body['expires_at'])) { $sets[] = 'expires_at=?'; $params[] = $body['expires_at']; }
            if (isset($body['notes']))      { $sets[] = 'notes=?';      $params[] = $body['notes']; }
            if (isset($body['sessions_total'])) { $sets[] = 'sessions_total=?'; $params[] = (int)$body['sessions_total']; }
            if (isset($body['sessions_remaining'])) { $sets[] = 'sessions_remaining=?'; $params[] = (int)$body['sessions_remaining']; }
            if (empty($sets)) json_out(['ok' => false, 'error' => 'Nothing to update'], 400);
            $params[] = (int)$adminId;
            db()->prepare('UPDATE bk_memberships SET ' . implode(',', $sets) . ' WHERE id=?')->execute($params);
            json_out(['ok' => true]);
        }
    }

    // ── Plans CRUD ────────────────────────────────────────────
    if ($adminRes === 'plans') {
        if ($method === 'GET') {
            if ($adminId) {
                $stmt = db()->prepare('SELECT * FROM bk_plans WHERE id=?');
                $stmt->execute([(int)$adminId]);
                $plan = $stmt->fetch();
                if (!$plan) json_out(['ok' => false, 'error' => 'Not found'], 404);
                $svcs = db()->prepare('SELECT service_id FROM bk_plan_services WHERE plan_id=?');
                $svcs->execute([(int)$adminId]);
                $plan['service_ids'] = array_column($svcs->fetchAll(), 'service_id');
                json_out(['ok' => true, 'plan' => $plan]);
            } else {
                $plans = db()->query('SELECT * FROM bk_plans ORDER BY price ASC')->fetchAll();
                foreach ($plans as &$p) {
                    $s = db()->prepare('SELECT service_id FROM bk_plan_services WHERE plan_id=?');
                    $s->execute([$p['id']]);
                    $p['service_ids'] = array_column($s->fetchAll(), 'service_id');
                }
                json_out(['ok' => true, 'plans' => $plans]);
            }
        }

        if ($method === 'POST') {
            $name    = trim($body['name'] ?? '');
            $price   = (float)($body['price'] ?? 0);
            $days    = (int)($body['duration_days'] ?? 30);
            $sessionLimit = isset($body['session_limit']) && $body['session_limit'] !== '' ? (int)$body['session_limit'] : null;
            $validityDays = isset($body['validity_days']) && $body['validity_days'] !== '' ? (int)$body['validity_days'] : null;
            $desc    = $body['description'] ?? '';
            $color   = $body['color'] ?? '#e09500';
            $active  = isset($body['is_active']) ? (int)$body['is_active'] : 1;
            $svcIds  = $body['service_ids'] ?? [];
            if (!$name) json_out(['ok' => false, 'error' => 'Name required'], 400);

            db()->prepare(
                'INSERT INTO bk_plans (name, description, price, duration_days, session_limit, validity_days, color, is_active) VALUES (?,?,?,?,?,?,?,?)'
            )->execute([$name, $desc, $price, $days, $sessionLimit, $validityDays, $color, $active]);
            $planId = (int) db()->lastInsertId();

            foreach ((array)$svcIds as $sid) {
                try { db()->prepare('INSERT IGNORE INTO bk_plan_services (plan_id, service_id) VALUES (?,?)')->execute([$planId, (int)$sid]); } catch(\Throwable $e) {}
            }
            json_out(['ok' => true, 'id' => $planId]);
        }

        if ($method === 'PUT' && $adminId) {
            $sets = []; $params = [];
            foreach (['name','description','color'] as $f) {
                if (isset($body[$f])) { $sets[] = "{$f}=?"; $params[] = $body[$f]; }
            }
            if (isset($body['price'])) { $sets[] = 'price=?'; $params[] = (float)$body['price']; }
            if (isset($body['duration_days'])) { $sets[] = 'duration_days=?'; $params[] = (int)$body['duration_days']; }
            if (isset($body['is_active'])) { $sets[] = 'is_active=?'; $params[] = (int)$body['is_active']; }
            if (array_key_exists('session_limit', $body)) {
                $sets[] = 'session_limit=?';
                $params[] = ($body['session_limit'] === '' || $body['session_limit'] === null) ? null : (int)$body['session_limit'];
            }
            if (array_key_exists('validity_days', $body)) {
                $sets[] = 'validity_days=?';
                $params[] = ($body['validity_days'] === '' || $body['validity_days'] === null) ? null : (int)$body['validity_days'];
            }
            if ($sets) {
                $params[] = (int)$adminId;
                db()->prepare('UPDATE bk_plans SET ' . implode(',', $sets) . ' WHERE id=?')->execute($params);
            }
            if (isset($body['service_ids'])) {
                db()->prepare('DELETE FROM bk_plan_services WHERE plan_id=?')->execute([(int)$adminId]);
                foreach ((array)$body['service_ids'] as $sid) {
                    try { db()->prepare('INSERT IGNORE INTO bk_plan_services (plan_id, service_id) VALUES (?,?)')->execute([(int)$adminId, (int)$sid]); } catch(\Throwable $e) {}
                }
            }
            json_out(['ok' => true]);
        }

        if ($method === 'DELETE' && $adminId) {
            db()->prepare('DELETE FROM bk_plan_services WHERE plan_id=?')->execute([(int)$adminId]);
            db()->prepare('DELETE FROM bk_plans WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok' => true]);
        }
    }

    // ── Services CRUD ─────────────────────────────────────────
    if ($adminRes === 'services') {
        if ($method === 'GET') {
            if ($adminId) {
                $stmt = db()->prepare('SELECT * FROM bk_services WHERE id=?');
                $stmt->execute([(int)$adminId]);
                $row = $stmt->fetch();
                json_out($row ? ['ok' => true, 'service' => $row] : ['ok' => false, 'error' => 'Not found']);
            } else {
                $rows = db()->query('SELECT * FROM bk_services ORDER BY name')->fetchAll();
                json_out(['ok' => true, 'services' => $rows]);
            }
        }
        if ($method === 'POST') {
            $name   = trim($body['name'] ?? '');
            $dur    = (int)($body['duration_min'] ?? 60);
            $desc   = $body['description'] ?? '';
            $color  = $body['color'] ?? '#e09500';
            $restr  = isset($body['is_restricted']) ? (int)$body['is_restricted'] : 1;
            $active = isset($body['is_active']) ? (int)$body['is_active'] : 1;
            if (!$name) json_out(['ok' => false, 'error' => 'Name required'], 400);
            db()->prepare('INSERT INTO bk_services (name, description, duration_min, color, is_restricted, is_active) VALUES (?,?,?,?,?,?)')
                ->execute([$name, $desc, $dur, $color, $restr, $active]);
            json_out(['ok' => true, 'id' => (int) db()->lastInsertId()]);
        }
        if ($method === 'PUT' && $adminId) {
            $sets = []; $params = [];
            foreach (['name','description','color'] as $f) {
                if (isset($body[$f])) { $sets[] = "{$f}=?"; $params[] = $body[$f]; }
            }
            foreach (['duration_min','is_restricted','is_active'] as $f) {
                if (isset($body[$f])) { $sets[] = "{$f}=?"; $params[] = (int)$body[$f]; }
            }
            if ($sets) {
                $params[] = (int)$adminId;
                db()->prepare('UPDATE bk_services SET ' . implode(',', $sets) . ' WHERE id=?')->execute($params);
            }
            json_out(['ok' => true]);
        }
        if ($method === 'DELETE' && $adminId) {
            db()->prepare('DELETE FROM bk_services WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok' => true]);
        }
    }

    // ── Providers CRUD ────────────────────────────────────────
    if ($adminRes === 'providers') {
        if ($method === 'GET' && $adminId === null) {
            $rows = db()->query('SELECT * FROM bk_providers ORDER BY name')->fetchAll();
            json_out(['ok' => true, 'providers' => $rows]);
        }
        if ($method === 'GET' && $adminId !== null && $adminSub === null) {
            $stmt = db()->prepare('SELECT * FROM bk_providers WHERE id=?');
            $stmt->execute([(int)$adminId]);
            $prov = $stmt->fetch();
            if (!$prov) json_out(['ok' => false, 'error' => 'Not found'], 404);

            $svcs = db()->prepare('SELECT ps.*, s.name as service_name FROM bk_provider_services ps JOIN bk_services s ON s.id=ps.service_id WHERE ps.provider_id=?');
            $svcs->execute([(int)$adminId]);
            $prov['services'] = $svcs->fetchAll();

            $hrs = db()->prepare('SELECT * FROM bk_provider_hours WHERE provider_id=? ORDER BY day_of_week');
            $hrs->execute([(int)$adminId]);
            $prov['hours'] = $hrs->fetchAll();

            json_out(['ok' => true, 'provider' => $prov]);
        }
        if ($method === 'POST' && $adminId === null) {
            $name   = trim($body['name'] ?? '');
            $branch = $body['branch_name'] ?? '';
            $email  = $body['email'] ?? '';
            $phone  = $body['phone'] ?? '';
            $bio    = $body['bio'] ?? '';
            $tz     = $body['timezone'] ?? 'UTC';
            $active = isset($body['is_active']) ? (int)$body['is_active'] : 1;
            if (!$name) json_out(['ok' => false, 'error' => 'Name required'], 400);
            db()->prepare('INSERT INTO bk_providers (name, branch_name, email, phone, bio, timezone, is_active) VALUES (?,?,?,?,?,?,?)')
                ->execute([$name, $branch, $email, $phone, $bio, $tz, $active]);
            $newId = (int) db()->lastInsertId();
            if (isset($body['service_ids'])) {
                $cap = (int)($body['capacity'] ?? 1);
                foreach ((array)$body['service_ids'] as $sid) {
                    try {
                        db()->prepare('INSERT INTO bk_provider_services (provider_id, service_id, capacity_per_slot) VALUES (?,?,?)')
                            ->execute([$newId, (int)$sid, $cap]);
                    } catch(\Throwable $e) {}
                }
            }
            json_out(['ok' => true, 'id' => $newId]);
        }
        if ($method === 'PUT' && $adminId !== null && $adminSub === null) {
            $sets = []; $params = [];
            foreach (['name','branch_name','email','phone','bio','avatar_url','timezone'] as $f) {
                if (isset($body[$f])) { $sets[] = "{$f}=?"; $params[] = $body[$f]; }
            }
            if (isset($body['is_active'])) { $sets[] = 'is_active=?'; $params[] = (int)$body['is_active']; }
            if ($sets) {
                $params[] = (int)$adminId;
                db()->prepare('UPDATE bk_providers SET ' . implode(',', $sets) . ' WHERE id=?')->execute($params);
            }
            // Update services if provided
            if (isset($body['service_ids'])) {
                db()->prepare('DELETE FROM bk_provider_services WHERE provider_id=?')->execute([(int)$adminId]);
                foreach ((array)$body['service_ids'] as $sid) {
                    $cap = (int)($body['capacity'] ?? 1);
                    try { db()->prepare('INSERT INTO bk_provider_services (provider_id, service_id, capacity_per_slot) VALUES (?,?,?)')->execute([(int)$adminId, (int)$sid, $cap]); } catch(\Throwable $e) {}
                }
            }
            json_out(['ok' => true]);
        }
        if ($method === 'DELETE' && $adminId !== null) {
            db()->prepare('DELETE FROM bk_providers WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok' => true]);
        }

        // Provider hours: GET/POST/PUT/DELETE /api/booking/admin/providers/:id/hours
        if ($adminSub === 'hours') {
            $provId = (int)$adminId;
            if ($method === 'GET') {
                $stmt = db()->prepare('SELECT * FROM bk_provider_hours WHERE provider_id=? ORDER BY day_of_week');
                $stmt->execute([$provId]);
                json_out(['ok' => true, 'hours' => $stmt->fetchAll()]);
            }
            if ($method === 'POST') {
                // Upsert all 7 days at once or single day
                $hoursData = $body['hours'] ?? [$body]; // array of {day_of_week, start_time, end_time, is_active}
                foreach ($hoursData as $h) {
                    $dow   = (int)($h['day_of_week'] ?? 0);
                    $start = $h['start_time'] ?? '09:00:00';
                    $end   = $h['end_time'] ?? '17:00:00';
                    $act   = isset($h['is_active']) ? (int)$h['is_active'] : 1;
                    db()->prepare(
                        'INSERT INTO bk_provider_hours (provider_id, day_of_week, start_time, end_time, is_active)
                         VALUES (?,?,?,?,?)
                         ON DUPLICATE KEY UPDATE start_time=VALUES(start_time), end_time=VALUES(end_time), is_active=VALUES(is_active)'
                    )->execute([$provId, $dow, $start, $end, $act]);
                }
                json_out(['ok' => true]);
            }
            if ($method === 'PUT') {
                $dow = isset($body['day_of_week']) ? (int)$body['day_of_week'] : null;
                if ($dow === null || $dow < 0 || $dow > 6) {
                    json_out(['ok' => false, 'error' => 'Valid day_of_week required (0-6)'], 400);
                }
                $start = $body['start_time'] ?? '09:00:00';
                $end   = $body['end_time'] ?? '17:00:00';
                $act   = isset($body['is_active']) ? (int)$body['is_active'] : 1;
                db()->prepare(
                    'INSERT INTO bk_provider_hours (provider_id, day_of_week, start_time, end_time, is_active)
                     VALUES (?,?,?,?,?)
                     ON DUPLICATE KEY UPDATE start_time=VALUES(start_time), end_time=VALUES(end_time), is_active=VALUES(is_active)'
                )->execute([$provId, $dow, $start, $end, $act]);
                json_out(['ok' => true]);
            }
            if ($method === 'DELETE') {
                db()->prepare('DELETE FROM bk_provider_hours WHERE provider_id=?')->execute([$provId]);
                json_out(['ok' => true]);
            }
        }

        // Provider breaks: /api/booking/admin/providers/:id/breaks
        if ($adminSub === 'breaks') {
            $provId = (int)$adminId;
            if ($method === 'GET') {
                $date = $_GET['date'] ?? null;
                if ($date) {
                    $stmt = db()->prepare('SELECT * FROM bk_provider_breaks WHERE provider_id=? AND break_date=? ORDER BY start_time');
                    $stmt->execute([$provId, $date]);
                } else {
                    $stmt = db()->prepare('SELECT * FROM bk_provider_breaks WHERE provider_id=? AND break_date >= CURDATE() ORDER BY break_date, start_time');
                    $stmt->execute([$provId]);
                }
                json_out(['ok' => true, 'breaks' => $stmt->fetchAll()]);
            }
            if ($method === 'POST') {
                $breakDate = $body['break_date'] ?? date('Y-m-d');
                $start     = $body['start_time'] ?? '12:00:00';
                $end       = $body['end_time'] ?? '13:00:00';
                $label     = $body['label'] ?? 'Break';
                db()->prepare('INSERT INTO bk_provider_breaks (provider_id, break_date, start_time, end_time, label) VALUES (?,?,?,?,?)')
                    ->execute([$provId, $breakDate, $start, $end, $label]);
                json_out(['ok' => true, 'id' => (int) db()->lastInsertId()]);
            }
            if ($method === 'PUT') {
                $breakId = (int)($segs[4] ?? 0);
                if (!$breakId) {
                    json_out(['ok' => false, 'error' => 'Break id required in route'], 400);
                }
                $sets = [];
                $params = [];
                foreach (['break_date', 'start_time', 'end_time', 'label'] as $f) {
                    if (array_key_exists($f, $body)) {
                        $sets[] = "{$f}=?";
                        $params[] = $body[$f];
                    }
                }
                if (!$sets) {
                    json_out(['ok' => false, 'error' => 'Nothing to update'], 400);
                }
                $params[] = $breakId;
                $params[] = $provId;
                db()->prepare('UPDATE bk_provider_breaks SET ' . implode(',', $sets) . ' WHERE id=? AND provider_id=?')
                    ->execute($params);
                json_out(['ok' => true]);
            }
            if ($method === 'DELETE') {
                $breakId = (int)($segs[4] ?? 0);
                if ($breakId) {
                    db()->prepare('DELETE FROM bk_provider_breaks WHERE id=? AND provider_id=?')->execute([$breakId, $provId]);
                } else {
                    db()->prepare('DELETE FROM bk_provider_breaks WHERE provider_id=?')->execute([$provId]);
                }
                json_out(['ok' => true]);
            }
        }
    }

    // ── Promo Codes CRUD ──────────────────────────────────────
    if ($adminRes === 'promo-codes') {
        if ($method === 'GET') {
            if ($adminId) {
                $stmt = db()->prepare('SELECT * FROM bk_promo_codes WHERE id=?');
                $stmt->execute([(int)$adminId]);
                $row = $stmt->fetch();
                json_out($row ? ['ok' => true, 'promo' => $row] : ['ok' => false, 'error' => 'Not found'], $row ? 200 : 404);
            }
            $rows = db()->query('SELECT * FROM bk_promo_codes ORDER BY created_at DESC')->fetchAll();
            json_out(['ok' => true, 'promos' => $rows]);
        }
        if ($method === 'POST') {
            $code  = strtoupper(preg_replace('/[^A-Z0-9\-_]/', '', strtoupper(trim($body['code'] ?? ''))));
            $dtype = $body['discount_type'] ?? 'percent';
            $dval  = (float)($body['discount_value'] ?? 0);
            $desc  = $body['description'] ?? '';
            $max   = isset($body['max_uses']) && $body['max_uses'] !== '' ? (int)$body['max_uses'] : null;
            $from  = $body['valid_from'] ?: null;
            $until = $body['valid_until'] ?: null;
            $plans = isset($body['plan_ids']) && is_array($body['plan_ids']) ? json_encode($body['plan_ids']) : null;
            $act   = isset($body['is_active']) ? (int)$body['is_active'] : 1;
            if (!$code || !in_array($dtype, ['percent','fixed'])) json_out(['ok'=>false,'error'=>'Code and valid discount_type required'], 400);
            try {
                db()->prepare(
                    'INSERT INTO bk_promo_codes (code,description,discount_type,discount_value,max_uses,valid_from,valid_until,plan_ids,is_active)
                     VALUES (?,?,?,?,?,?,?,?,?)'
                )->execute([$code, $desc, $dtype, $dval, $max, $from, $until, $plans, $act]);
            } catch (\Throwable $e) {
                json_out(['ok'=>false,'error'=>'Code already exists'], 409);
            }
            json_out(['ok'=>true,'id'=>(int)db()->lastInsertId()]);
        }
        if ($method === 'PUT' && $adminId) {
            $sets=[]; $params=[];
            foreach (['description','discount_type','valid_from','valid_until'] as $f) {
                if (array_key_exists($f,$body)) { $sets[]="{$f}=?"; $params[]=$body[$f]; }
            }
            foreach (['discount_value','max_uses','is_active'] as $f) {
                if (array_key_exists($f,$body)) { $sets[]="{$f}=?"; $params[]=$body[$f]===''?null:(float)$body[$f]; }
            }
            if (array_key_exists('plan_ids',$body)) {
                $sets[]='plan_ids=?';
                $params[]=is_array($body['plan_ids'])&&!empty($body['plan_ids'])?json_encode($body['plan_ids']):null;
            }
            if (!$sets) json_out(['ok'=>false,'error'=>'Nothing to update'], 400);
            $params[]=(int)$adminId;
            db()->prepare('UPDATE bk_promo_codes SET '.implode(',',$sets).' WHERE id=?')->execute($params);
            json_out(['ok'=>true]);
        }
        if ($method === 'DELETE' && $adminId) {
            db()->prepare('DELETE FROM bk_promo_codes WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok'=>true]);
        }
    }

    // ── Admin: send reminders now (cron trigger endpoint) ────
    if ($method === 'POST' && $adminRes === 'send-reminders') {
        $sent24 = 0; $sent1 = 0; $errors = 0;

        // 24h reminders: appointments starting in 23-25h that haven't been reminded
        $stmt24 = db()->query(
            "SELECT a.*, c.name as customer_name, c.email as customer_email,
                    s.name as service_name, p.name as provider_name
             FROM bk_appointments a
             JOIN bk_customers c ON c.id = a.customer_id
             JOIN bk_services s ON s.id = a.service_id
             JOIN bk_providers p ON p.id = a.provider_id
             WHERE a.status IN ('pending','confirmed')
               AND a.start_datetime BETWEEN DATE_ADD(NOW(), INTERVAL 23 HOUR)
                                        AND DATE_ADD(NOW(), INTERVAL 25 HOUR)
               AND a.reminder_24h_sent = 0"
        );
        foreach ($stmt24->fetchAll() as $appt) {
            $apptDate = date('D, d M Y \a\t g:ia', strtotime($appt['start_datetime']));
            $html = "<h2>Reminder: Your appointment tomorrow</h2>
                <p>Hi {$appt['customer_name']},</p>
                <p>This is a reminder that you have an appointment tomorrow.</p>
                <table>
                    <tr><td><strong>Service:</strong></td><td>{$appt['service_name']}</td></tr>
                    <tr><td><strong>Provider:</strong></td><td>{$appt['provider_name']}</td></tr>
                    <tr><td><strong>Date &amp; Time:</strong></td><td>{$apptDate}</td></tr>
                    <tr><td><strong>Reference:</strong></td><td>{$appt['ref']}</td></tr>
                </table>
                <p>Need to cancel? Visit your <a href=\"" . APP_URL . "/customer/dashboard.php\">Member Portal</a>.</p>";
            $text = "Reminder: {$appt['service_name']} with {$appt['provider_name']} — {$apptDate} (Ref: {$appt['ref']})";
            try {
                send_mail($appt['customer_email'], $appt['customer_name'], "Reminder: Your appointment tomorrow — {$appt['service_name']}", $html, $text);
                db()->prepare('UPDATE bk_appointments SET reminder_24h_sent=1 WHERE id=?')->execute([$appt['id']]);
                $sent24++;
            } catch (\Throwable $e) {
                error_log('Reminder 24h error: ' . $e->getMessage()); $errors++;
            }
        }

        // 1h reminders: appointments starting in 55-65 minutes
        $stmt1h = db()->query(
            "SELECT a.*, c.name as customer_name, c.email as customer_email,
                    s.name as service_name, p.name as provider_name
             FROM bk_appointments a
             JOIN bk_customers c ON c.id = a.customer_id
             JOIN bk_services s ON s.id = a.service_id
             JOIN bk_providers p ON p.id = a.provider_id
             WHERE a.status IN ('pending','confirmed')
               AND a.start_datetime BETWEEN DATE_ADD(NOW(), INTERVAL 55 MINUTE)
                                        AND DATE_ADD(NOW(), INTERVAL 65 MINUTE)
               AND a.reminder_1h_sent = 0"
        );
        foreach ($stmt1h->fetchAll() as $appt) {
            $apptDate = date('D, d M Y \a\t g:ia', strtotime($appt['start_datetime']));
            $html = "<h2>Your appointment is in 1 hour</h2>
                <p>Hi {$appt['customer_name']},</p>
                <p>Just a heads-up — your appointment starts in about an hour.</p>
                <table>
                    <tr><td><strong>Service:</strong></td><td>{$appt['service_name']}</td></tr>
                    <tr><td><strong>Provider:</strong></td><td>{$appt['provider_name']}</td></tr>
                    <tr><td><strong>Time:</strong></td><td>{$apptDate}</td></tr>
                </table>";
            $text = "Starting in 1 hour: {$appt['service_name']} with {$appt['provider_name']} — {$apptDate}";
            try {
                send_mail($appt['customer_email'], $appt['customer_name'], "Starting soon: {$appt['service_name']} in 1 hour", $html, $text);
                db()->prepare('UPDATE bk_appointments SET reminder_1h_sent=1 WHERE id=?')->execute([$appt['id']]);
                $sent1++;
            } catch (\Throwable $e) {
                error_log('Reminder 1h error: ' . $e->getMessage()); $errors++;
            }
        }

        json_out(['ok' => true, 'sent_24h' => $sent24, 'sent_1h' => $sent1, 'errors' => $errors]);
    }

    // ── Admin: Ratings ────────────────────────────────────────
    if ($adminRes === 'ratings') {
        if ($method === 'GET') {
            $where = ['1=1']; $params = [];
            if (!empty($_GET['provider_id'])) { $where[] = 'r.provider_id=?'; $params[] = (int)$_GET['provider_id']; }
            $page = max(1,(int)($_GET['page'] ?? 1)); $limit = 50; $offset = ($page-1)*$limit;
            $ws = implode(' AND ',$where);
            $cStmt = db()->prepare("SELECT COUNT(*) FROM bk_ratings r WHERE {$ws}");
            $cStmt->execute($params);
            $total = (int)$cStmt->fetchColumn();
            $stmt = db()->prepare(
                "SELECT r.*, c.name as customer_name, c.email as customer_email,
                        p.name as provider_name, s.name as service_name
                 FROM bk_ratings r
                 JOIN bk_customers c ON c.id = r.customer_id
                 JOIN bk_providers p ON p.id = r.provider_id
                 JOIN bk_services s ON s.id = r.service_id
                 WHERE {$ws} ORDER BY r.created_at DESC LIMIT {$limit} OFFSET {$offset}"
            );
            $stmt->execute($params);
            json_out(['ok'=>true,'ratings'=>$stmt->fetchAll(),'total'=>$total,'page'=>$page,'pages'=>ceil($total/$limit)]);
        }
        if ($method === 'PUT' && $adminId) {
            $sets=[]; $params=[];
            if (isset($body['is_public'])) { $sets[]='is_public=?'; $params[]=(int)$body['is_public']; }
            if (!$sets) json_out(['ok'=>false,'error'=>'Nothing to update'],400);
            $params[]=(int)$adminId;
            db()->prepare('UPDATE bk_ratings SET '.implode(',',$sets).' WHERE id=?')->execute($params);
            json_out(['ok'=>true]);
        }
        if ($method === 'DELETE' && $adminId) {
            db()->prepare('DELETE FROM bk_ratings WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok'=>true]);
        }
    }

    // ── Admin: Membership freeze/unfreeze ─────────────────────
    if ($adminRes === 'memberships' && $adminSub === 'freeze' && $adminId) {
        $memRow = db()->prepare('SELECT * FROM bk_memberships WHERE id=?');
        $memRow->execute([(int)$adminId]);
        $mem = $memRow->fetch();
        if (!$mem) json_out(['ok'=>false,'error'=>'Not found'],404);
        if ($method === 'POST') {
            // Freeze membership
            if ($mem['frozen_at']) json_out(['ok'=>false,'error'=>'Already frozen'],400);
            db()->prepare('UPDATE bk_memberships SET frozen_at=NOW(), status="active" WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok'=>true,'frozen_at'=>date('Y-m-d H:i:s')]);
        }
        if ($method === 'DELETE') {
            // Unfreeze — extend expiry by frozen days
            if (!$mem['frozen_at']) json_out(['ok'=>false,'error'=>'Not frozen'],400);
            $frozenDays = (int)ceil((time() - strtotime($mem['frozen_at'])) / 86400);
            $newExpiry = date('Y-m-d H:i:s', strtotime($mem['expires_at']) + $frozenDays * 86400);
            db()->prepare('UPDATE bk_memberships SET frozen_at=NULL, frozen_days=frozen_days+?, expires_at=? WHERE id=?')
                ->execute([$frozenDays, $newExpiry, (int)$adminId]);
            json_out(['ok'=>true,'new_expires_at'=>$newExpiry,'days_restored'=>$frozenDays]);
        }
    }

    // ── Admin: Waitlist notify ─────────────────────────────────
    if ($method === 'POST' && $adminRes === 'waitlist' && $adminId && $adminSub === 'notify') {
        $wlRow = db()->prepare(
            'SELECT w.*, s.name as service_name, p.name as provider_name
             FROM bk_waitlist w
             JOIN bk_services s ON s.id = w.service_id
             JOIN bk_providers p ON p.id = w.provider_id
             WHERE w.id=?'
        );
        $wlRow->execute([(int)$adminId]);
        $wl = $wlRow->fetch();
        if (!$wl) json_out(['ok'=>false,'error'=>'Waitlist entry not found'],404);

        $slotDate = date('D, d M Y \a\t g:ia', strtotime($wl['slot_datetime']));
        $bookUrl  = APP_URL . '/book.php';
        $html = "<h2>A spot just opened up!</h2>
            <p>Hi {$wl['customer_name']},</p>
            <p>Good news — a slot you were waiting for is now available.</p>
            <table>
                <tr><td><strong>Service:</strong></td><td>{$wl['service_name']}</td></tr>
                <tr><td><strong>Provider:</strong></td><td>{$wl['provider_name']}</td></tr>
                <tr><td><strong>Slot:</strong></td><td>{$slotDate}</td></tr>
            </table>
            <p><a href=\"{$bookUrl}\">Book now →</a> (slots fill up fast!)</p>";
        $text = "Slot available: {$wl['service_name']} with {$wl['provider_name']} — {$slotDate}. Book at: {$bookUrl}";
        try {
            send_mail($wl['customer_email'], $wl['customer_name'], "Spot available: {$wl['service_name']} with {$wl['provider_name']}", $html, $text);
            json_out(['ok'=>true,'emailed'=>$wl['customer_email']]);
        } catch (\Throwable $e) {
            json_out(['ok'=>false,'error'=>$e->getMessage()],500);
        }
    }

    // ── Admin: Session ledger timeline + exports ───────────────
    if ($adminRes === 'session-ledger' && $method === 'GET') {
        $where = ['1=1']; $params = [];
        if (!empty($_GET['customer_id'])) { $where[] = 'c.id=?'; $params[] = (int)$_GET['customer_id']; }
        if (!empty($_GET['membership_id'])) { $where[] = 'ci.membership_id=?'; $params[] = (int)$_GET['membership_id']; }
        if (!empty($_GET['date_from'])) { $where[] = 'DATE(ci.checked_in_at)>=?'; $params[] = $_GET['date_from']; }
        if (!empty($_GET['date_to'])) { $where[] = 'DATE(ci.checked_in_at)<=?'; $params[] = $_GET['date_to']; }
        if (!empty($_GET['search'])) {
            $like = '%' . trim((string)$_GET['search']) . '%';
            $where[] = '(c.email LIKE ? OR c.name LIKE ? OR a.ref LIKE ?)';
            array_push($params, $like, $like, $like);
        }
        $page = max(1, (int)($_GET['page'] ?? 1));
        $limit = min(200, max(1, (int)($_GET['limit'] ?? 50)));
        $offset = ($page - 1) * $limit;
        $ws = implode(' AND ', $where);
        $countStmt = db()->prepare(
            "SELECT COUNT(*)
             FROM bk_checkins ci
             JOIN bk_customers c ON c.id=ci.customer_id
             LEFT JOIN bk_appointments a ON a.id=ci.appointment_id
             WHERE {$ws}"
        );
        $countStmt->execute($params);
        $total = (int)$countStmt->fetchColumn();
        $stmt = db()->prepare(
            "SELECT ci.id, ci.membership_id, ci.customer_id, ci.appointment_id, ci.service_id, ci.checked_in_at,
                    c.name as customer_name, c.email as customer_email, a.ref as appointment_ref,
                    m.sessions_total, m.sessions_remaining
             FROM bk_checkins ci
             JOIN bk_customers c ON c.id=ci.customer_id
             LEFT JOIN bk_appointments a ON a.id=ci.appointment_id
             LEFT JOIN bk_memberships m ON m.id=ci.membership_id
             WHERE {$ws}
             ORDER BY ci.checked_in_at DESC
             LIMIT {$limit} OFFSET {$offset}"
        );
        $stmt->execute($params);
        json_out(['ok'=>true,'rows'=>$stmt->fetchAll(),'total'=>$total,'page'=>$page,'pages'=>ceil($total/$limit)]);
    }
    if ($adminRes === 'exports' && $method === 'GET') {
        $type = strtolower((string)($adminId ?? ''));
        $filename = 'export-' . $type . '-' . date('Ymd-His') . '.csv';
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        $out = fopen('php://output', 'w');
        if ($type === 'memberships') {
            fputcsv($out, ['id','customer_name','customer_email','plan_name','status','sessions_total','sessions_remaining','starts_at','expires_at','activated_by','stripe_payment_id','stripe_session_id']);
            $rows = db()->query('SELECT m.id,c.name customer_name,c.email customer_email,p.name plan_name,m.status,m.sessions_total,m.sessions_remaining,m.starts_at,m.expires_at,m.activated_by,m.stripe_payment_id,m.stripe_session_id FROM bk_memberships m JOIN bk_customers c ON c.id=m.customer_id JOIN bk_plans p ON p.id=m.plan_id ORDER BY m.created_at DESC')->fetchAll();
            foreach ($rows as $r) fputcsv($out, array_values($r));
            fclose($out); exit;
        }
        if ($type === 'checkins') {
            fputcsv($out, ['id','checked_in_at','membership_id','customer_id','customer_name','customer_email','appointment_ref','service_id']);
            $rows = db()->query('SELECT ci.id,ci.checked_in_at,ci.membership_id,ci.customer_id,c.name customer_name,c.email customer_email,a.ref appointment_ref,ci.service_id FROM bk_checkins ci JOIN bk_customers c ON c.id=ci.customer_id LEFT JOIN bk_appointments a ON a.id=ci.appointment_id ORDER BY ci.checked_in_at DESC')->fetchAll();
            foreach ($rows as $r) fputcsv($out, array_values($r));
            fclose($out); exit;
        }
        if ($type === 'payments') {
            fputcsv($out, ['membership_id','customer_name','customer_email','plan_name','activated_by','stripe_payment_id','stripe_session_id','created_at','expires_at']);
            $rows = db()->query('SELECT m.id membership_id,c.name customer_name,c.email customer_email,p.name plan_name,m.activated_by,m.stripe_payment_id,m.stripe_session_id,m.created_at,m.expires_at FROM bk_memberships m JOIN bk_customers c ON c.id=m.customer_id JOIN bk_plans p ON p.id=m.plan_id WHERE m.stripe_payment_id IS NOT NULL OR m.stripe_session_id IS NOT NULL ORDER BY m.created_at DESC')->fetchAll();
            foreach ($rows as $r) fputcsv($out, array_values($r));
            fclose($out); exit;
        }
        json_out(['ok'=>false,'error'=>'Unknown export type'], 404);
    }


    if ($adminRes === 'customers') {
        if ($method === 'GET') {
            $search = $_GET['search'] ?? '';
            if ($search) {
                $like = '%' . $search . '%';
                $stmt = db()->prepare('SELECT * FROM bk_customers WHERE email LIKE ? OR name LIKE ? OR phone LIKE ? ORDER BY name LIMIT 50');
                $stmt->execute([$like, $like, $like]);
            } else {
                $stmt = db()->query('SELECT * FROM bk_customers ORDER BY created_at DESC LIMIT 100');
            }
            json_out(['ok' => true, 'customers' => $stmt->fetchAll()]);
        }
        if ($method === 'POST') {
            $email = trim(strtolower((string)($body['email'] ?? '')));
            [$name, $phone, $dob, $gender, $addressLine1, $city, $country, $emergencyName, $emergencyPhone] = bk_validate_profile($body, false);
            if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
                json_out(['ok' => false, 'error' => 'Valid email required'], 400);
            }
            try {
                db()->prepare('INSERT INTO bk_customers (email, name, phone, dob, gender, address_line1, city, country, emergency_contact_name, emergency_contact_phone) VALUES (?,?,?,?,?,?,?,?,?,?)')
                    ->execute([
                        $email, $name, $phone ?: null, $dob ?: null, $gender ?: null,
                        $addressLine1 ?: null, $city ?: null, $country ?: null, $emergencyName ?: null, $emergencyPhone ?: null
                    ]);
            } catch (\Throwable $e) {
                json_out(['ok' => false, 'error' => 'Customer already exists'], 409);
            }
            json_out(['ok' => true, 'id' => (int)db()->lastInsertId()]);
        }
        if ($method === 'PUT' && $adminId) {
            $sets = [];
            $params = [];
            [$name, $phone, $dob, $gender, $addressLine1, $city, $country, $emergencyName, $emergencyPhone] = bk_validate_profile($body, false);
            if (array_key_exists('name', $body)) { $sets[] = 'name=?'; $params[] = $name; }
            if (array_key_exists('phone', $body)) { $sets[] = 'phone=?'; $params[] = $phone; }
            if (array_key_exists('dob', $body)) { $sets[] = 'dob=?'; $params[] = $dob; }
            if (array_key_exists('gender', $body)) { $sets[] = 'gender=?'; $params[] = $gender; }
            if (array_key_exists('address_line1', $body)) { $sets[] = 'address_line1=?'; $params[] = $addressLine1; }
            if (array_key_exists('city', $body)) { $sets[] = 'city=?'; $params[] = $city; }
            if (array_key_exists('country', $body)) { $sets[] = 'country=?'; $params[] = $country; }
            if (array_key_exists('emergency_contact_name', $body)) { $sets[] = 'emergency_contact_name=?'; $params[] = $emergencyName; }
            if (array_key_exists('emergency_contact_phone', $body)) { $sets[] = 'emergency_contact_phone=?'; $params[] = $emergencyPhone; }
            if (array_key_exists('email', $body)) {
                $email = trim(strtolower((string)$body['email']));
                if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) json_out(['ok' => false, 'error' => 'Valid email required'], 400);
                $sets[] = 'email=?';
                $params[] = $email;
            }
            if (!$sets) json_out(['ok' => false, 'error' => 'Nothing to update'], 400);
            $params[] = (int)$adminId;
            db()->prepare('UPDATE bk_customers SET ' . implode(',', $sets) . ' WHERE id=?')->execute($params);
            json_out(['ok' => true]);
        }
        if ($method === 'DELETE' && $adminId) {
            db()->prepare('DELETE FROM bk_customers WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok' => true]);
        }
    }

    // ── Waitlist ──────────────────────────────────────────────
    if ($adminRes === 'waitlist') {
        if ($method === 'GET') {
            $where = ['1=1']; $params = [];
            if (!empty($_GET['provider_id'])) { $where[] = 'w.provider_id=?'; $params[] = (int)$_GET['provider_id']; }
            if (!empty($_GET['slot_datetime'])) { $where[] = 'w.slot_datetime=?'; $params[] = $_GET['slot_datetime']; }
            $whereStr = implode(' AND ', $where);
            $stmt = db()->prepare(
                "SELECT w.*, s.name as service_name, p.name as provider_name
                 FROM bk_waitlist w
                 JOIN bk_services s ON s.id = w.service_id
                 JOIN bk_providers p ON p.id = w.provider_id
                 WHERE {$whereStr} ORDER BY w.created_at DESC"
            );
            $stmt->execute($params);
            json_out(['ok' => true, 'waitlist' => $stmt->fetchAll()]);
        }
        if ($method === 'DELETE' && $adminId) {
            db()->prepare('DELETE FROM bk_waitlist WHERE id=?')->execute([(int)$adminId]);
            json_out(['ok' => true]);
        }
    }
}

// ── 404 fallback ─────────────────────────────────────────────
json_out(['ok' => false, 'error' => 'Unknown route: ' . $method . ' /' . implode('/', $segs)], 404);

} catch (\Throwable $e) {
    error_log('Booking API error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
    json_out(['ok' => false, 'error' => 'Server error: ' . $e->getMessage()], 500);
}
