<?php
/**
 * Booking — public API.
 *
 * Static methods so other plugins can integrate without instantiating
 * the bootstrap class:
 *
 *   BookingAPI::getActiveServices()
 *   BookingAPI::getService(int|string $idOrSlug)
 *   BookingAPI::getProvidersForService(int $serviceId)
 *   BookingAPI::getProviderHours(int $providerId)
 *   BookingAPI::computeAvailableSlots(int $serviceId, int $providerId, string $date)
 *   BookingAPI::createAppointment(array $args)  // returns ['ok'=>..., 'id'=>..., 'ref'=>...]
 *   BookingAPI::generateRef()
 *
 * Slot engine notes:
 *   - Days are interpreted in the *site's* timezone (date_default_timezone_get()).
 *     This first cut treats providers' tz column as informational; we always
 *     compute slots in server-local time. Multi-tz support is a follow-up.
 *   - Slot interval = service.duration_min + service.buffer_min.
 *   - A slot is "available" iff:
 *       a) it sits entirely within at least one of the provider's
 *          weekly hours rows for that day_of_week, AND
 *       b) no confirmed appointment for the same provider overlaps it.
 *   - Race protection on createAppointment(): the final overlap check
 *     runs inside a transaction with SELECT ... FOR UPDATE so two
 *     concurrent requests can't both win.
 */

class BookingAPI {

    /** True once per request after we've confirmed the schema exists. */
    private static bool $schemaChecked = false;

    /**
     * Lazily create the booking_* tables if they don't already exist.
     * Defensive against half-activated installs (install.sql didn't
     * run or partially failed).
     */
    public static function ensureSchema(): void {
        if (self::$schemaChecked) return;
        self::$schemaChecked = true;
        try {
            $pdo = Database::get();
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `booking_services` (
                    `id`             INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`      INT UNSIGNED NOT NULL DEFAULT 1,
                    `name`           VARCHAR(160) NOT NULL,
                    `slug`           VARCHAR(80) NOT NULL,
                    `description`    TEXT NULL,
                    `duration_min`   INT UNSIGNED NOT NULL DEFAULT 30,
                    `buffer_min`     INT UNSIGNED NOT NULL DEFAULT 0,
                    `price_cents`    INT UNSIGNED NOT NULL DEFAULT 0,
                    `currency`       VARCHAR(8) NOT NULL DEFAULT 'USD',
                    `color`          VARCHAR(16) NOT NULL DEFAULT '#2563EB',
                    `is_active`      TINYINT(1) NOT NULL DEFAULT 1,
                    `created_at`     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    `updated_at`     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    UNIQUE KEY `tenant_slug` (`tenant_id`, `slug`),
                    KEY `tenant_active` (`tenant_id`, `is_active`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `booking_providers` (
                    `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`  INT UNSIGNED NOT NULL DEFAULT 1,
                    `name`       VARCHAR(160) NOT NULL,
                    `email`      VARCHAR(200) NULL,
                    `timezone`   VARCHAR(60) NOT NULL DEFAULT 'UTC',
                    `bio`        TEXT NULL,
                    `is_active`  TINYINT(1) NOT NULL DEFAULT 1,
                    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    KEY `tenant_active` (`tenant_id`, `is_active`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `booking_provider_services` (
                    `provider_id` INT UNSIGNED NOT NULL,
                    `service_id`  INT UNSIGNED NOT NULL,
                    PRIMARY KEY (`provider_id`, `service_id`),
                    KEY `service_idx` (`service_id`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `booking_provider_hours` (
                    `id`          INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`   INT UNSIGNED NOT NULL DEFAULT 1,
                    `provider_id` INT UNSIGNED NOT NULL,
                    `day_of_week` TINYINT NOT NULL,
                    `start_time`  TIME NOT NULL,
                    `end_time`    TIME NOT NULL,
                    PRIMARY KEY (`id`),
                    KEY `provider_day` (`provider_id`, `day_of_week`),
                    KEY `tenant_provider` (`tenant_id`, `provider_id`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `booking_appointments` (
                    `id`             INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`      INT UNSIGNED NOT NULL DEFAULT 1,
                    `ref`            VARCHAR(32) NOT NULL,
                    `service_id`     INT UNSIGNED NOT NULL,
                    `provider_id`    INT UNSIGNED NOT NULL,
                    `customer_id`    INT UNSIGNED NULL,
                    `customer_name`  VARCHAR(160) NOT NULL,
                    `customer_email` VARCHAR(200) NOT NULL,
                    `customer_phone` VARCHAR(40) NULL,
                    `notes`          TEXT NULL,
                    `starts_at`      DATETIME NOT NULL,
                    `ends_at`        DATETIME NOT NULL,
                    `status`         ENUM('confirmed','cancelled','no_show','completed') NOT NULL DEFAULT 'confirmed',
                    `reminder_24h_sent` TINYINT(1) NOT NULL DEFAULT 0,
                    `reminder_1h_sent`  TINYINT(1) NOT NULL DEFAULT 0,
                    `created_at`     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    `updated_at`     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    UNIQUE KEY `tenant_ref` (`tenant_id`, `ref`),
                    KEY `tenant_status_starts` (`tenant_id`, `status`, `starts_at`),
                    KEY `provider_slot` (`provider_id`, `starts_at`, `ends_at`),
                    KEY `customer_starts` (`customer_id`, `starts_at`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
        } catch (\Throwable $e) {
            if (function_exists('slate_log')) {
                slate_log('BookingAPI::ensureSchema failed: ' . $e->getMessage(), 'error');
            }
        }
    }

    public static function getActiveServices(): array {
        self::ensureSchema();
        return Database::rows(
            "SELECT * FROM booking_services
              WHERE tenant_id = ? AND is_active = 1
           ORDER BY name",
            [current_tenant_id()]
        );
    }

    public static function getService($idOrSlug): ?array {
        $tid = current_tenant_id();
        if (is_numeric($idOrSlug)) {
            return Database::row(
                "SELECT * FROM booking_services WHERE id = ? AND tenant_id = ?",
                [(int)$idOrSlug, $tid]
            );
        }
        return Database::row(
            "SELECT * FROM booking_services WHERE slug = ? AND tenant_id = ?",
            [(string)$idOrSlug, $tid]
        );
    }

    public static function getProvidersForService(int $serviceId): array {
        return Database::rows(
            "SELECT p.* FROM booking_providers p
               JOIN booking_provider_services ps ON ps.provider_id = p.id
              WHERE ps.service_id = ? AND p.tenant_id = ? AND p.is_active = 1
           ORDER BY p.name",
            [$serviceId, current_tenant_id()]
        );
    }

    public static function getAllProviders(): array {
        return Database::rows(
            "SELECT * FROM booking_providers
              WHERE tenant_id = ? AND is_active = 1
           ORDER BY name",
            [current_tenant_id()]
        );
    }

    public static function getProviderHours(int $providerId): array {
        return Database::rows(
            "SELECT * FROM booking_provider_hours
              WHERE provider_id = ? AND tenant_id = ?
           ORDER BY day_of_week, start_time",
            [$providerId, current_tenant_id()]
        );
    }

    /**
     * Compute available start times for a given (service, provider, date).
     * Returns ['HH:MM', 'HH:MM', ...] in 24h time, server-local.
     *
     * $date is 'YYYY-MM-DD'.
     */
    public static function computeAvailableSlots(int $serviceId, int $providerId, string $date): array {
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) return [];

        $tid     = current_tenant_id();
        $service = self::getService($serviceId);
        if (!$service || empty($service['is_active'])) return [];

        $duration = (int)$service['duration_min'];
        $buffer   = (int)$service['buffer_min'];
        $step     = max(15, $duration);   // step in minutes; min 15
        if ($duration <= 0) return [];

        // Day-of-week 0=Sun..6=Sat
        $dow = (int) date('w', strtotime($date));

        // Provider hours blocks for that day
        $blocks = Database::rows(
            "SELECT start_time, end_time FROM booking_provider_hours
              WHERE provider_id = ? AND tenant_id = ? AND day_of_week = ?
           ORDER BY start_time",
            [$providerId, $tid, $dow]
        );
        if (!$blocks) return [];

        // Existing appointments for that provider on that day
        $busy = Database::rows(
            "SELECT starts_at, ends_at FROM booking_appointments
              WHERE provider_id = ? AND tenant_id = ?
                AND status = 'confirmed'
                AND DATE(starts_at) = ?",
            [$providerId, $tid, $date]
        );

        $now  = time();
        $slots = [];

        foreach ($blocks as $b) {
            $start = strtotime($date . ' ' . $b['start_time']);
            $end   = strtotime($date . ' ' . $b['end_time']);
            if ($start === false || $end === false || $end <= $start) continue;

            $cursor = $start;
            while ($cursor + $duration * 60 <= $end) {
                $slotStart = $cursor;
                $slotEnd   = $cursor + $duration * 60;

                // Skip slots that have already started.
                if ($slotStart <= $now) {
                    $cursor += $step * 60;
                    continue;
                }

                $overlap = false;
                foreach ($busy as $bz) {
                    $bzS = strtotime($bz['starts_at']);
                    $bzE = strtotime($bz['ends_at']);
                    if ($bzS < $slotEnd && $bzE > $slotStart) {
                        $overlap = true;
                        break;
                    }
                }
                if (!$overlap) {
                    $slots[] = date('H:i', $slotStart);
                }

                $cursor += ($step + $buffer) * 60;
            }
        }
        return array_values(array_unique($slots));
    }

    /**
     * Create an appointment with race-condition protection.
     *
     * Required keys in $args:
     *   service_id, provider_id, starts_at ('YYYY-MM-DD HH:MM' or 'YYYY-MM-DDTHH:MM'),
     *   customer_name, customer_email
     * Optional: customer_phone, customer_id, notes
     *
     * Returns ['ok'=>true, 'id'=>int, 'ref'=>string]
     *      or ['ok'=>false, 'error'=>string].
     */
    public static function createAppointment(array $args): array {
        $serviceId  = (int)($args['service_id']  ?? 0);
        $providerId = (int)($args['provider_id'] ?? 0);
        $startsAt   = trim((string)($args['starts_at'] ?? ''));
        $name       = trim((string)($args['customer_name']  ?? ''));
        $email      = trim((string)($args['customer_email'] ?? ''));
        $phone      = trim((string)($args['customer_phone'] ?? ''));
        $notes      = trim((string)($args['notes'] ?? ''));
        $customerId = isset($args['customer_id']) ? (int)$args['customer_id'] : null;

        if ($serviceId <= 0 || $providerId <= 0) return ['ok' => false, 'error' => 'Service and provider are required.'];
        if ($name === '')                         return ['ok' => false, 'error' => 'Your name is required.'];
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return ['ok' => false, 'error' => 'A valid email is required.'];

        // Normalise starts_at — accept 'YYYY-MM-DDTHH:MM' from <input type=datetime-local>
        $startsAt = str_replace('T', ' ', $startsAt);
        if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/', $startsAt)) {
            return ['ok' => false, 'error' => 'Invalid time.'];
        }
        $startTs = strtotime($startsAt);
        if (!$startTs || $startTs <= time()) {
            return ['ok' => false, 'error' => 'Pick a future time slot.'];
        }

        $service = self::getService($serviceId);
        if (!$service || empty($service['is_active'])) {
            return ['ok' => false, 'error' => 'That service isn\'t available.'];
        }

        // Confirm the provider performs the service
        $assoc = Database::row(
            "SELECT 1 FROM booking_provider_services WHERE provider_id = ? AND service_id = ?",
            [$providerId, $serviceId]
        );
        if (!$assoc) return ['ok' => false, 'error' => 'That provider doesn\'t offer this service.'];

        $endTs   = $startTs + ((int)$service['duration_min']) * 60;
        $endsAt  = date('Y-m-d H:i:s', $endTs);
        $startsAtSql = date('Y-m-d H:i:s', $startTs);
        $tid     = current_tenant_id();

        // Validate the slot is inside the provider's working hours for that day
        $dow = (int) date('w', $startTs);
        $insideHours = Database::row(
            "SELECT 1 FROM booking_provider_hours
              WHERE provider_id = ? AND tenant_id = ? AND day_of_week = ?
                AND start_time <= ? AND end_time >= ?
              LIMIT 1",
            [$providerId, $tid, $dow, date('H:i:s', $startTs), date('H:i:s', $endTs)]
        );
        if (!$insideHours) return ['ok' => false, 'error' => 'That time is outside the provider\'s working hours.'];

        // Race-safe overlap check + insert
        $pdo = Database::get();
        $pdo->beginTransaction();
        try {
            $clash = Database::row(
                "SELECT id FROM booking_appointments
                  WHERE provider_id = ? AND tenant_id = ? AND status = 'confirmed'
                    AND starts_at < ? AND ends_at > ?
                  LIMIT 1 FOR UPDATE",
                [$providerId, $tid, $endsAt, $startsAtSql]
            );
            if ($clash) {
                $pdo->rollBack();
                return ['ok' => false, 'error' => 'Sorry, that slot was just taken. Pick another time.'];
            }

            $ref = self::generateRef();
            $id  = Database::insert('booking_appointments', [
                'tenant_id'      => $tid,
                'ref'            => $ref,
                'service_id'     => $serviceId,
                'provider_id'    => $providerId,
                'customer_id'    => $customerId,
                'customer_name'  => mb_substr($name,  0, 160),
                'customer_email' => mb_substr($email, 0, 200),
                'customer_phone' => $phone !== '' ? mb_substr($phone, 0, 40) : null,
                'notes'          => $notes !== '' ? $notes : null,
                'starts_at'      => $startsAtSql,
                'ends_at'        => $endsAt,
                'status'         => 'confirmed',
            ]);

            $pdo->commit();
        } catch (\Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            return ['ok' => false, 'error' => 'Booking failed: ' . $e->getMessage()];
        }

        AuditLog::record('booking.created', (string)$id, [
            'ref'         => $ref,
            'service_id'  => $serviceId,
            'provider_id' => $providerId,
        ]);

        Hook::doAction('booking_created', $id, $serviceId, $providerId);

        // Fire-and-forget confirmation email (errors logged, not fatal)
        try {
            self::sendConfirmation([
                'ref'            => $ref,
                'customer_name'  => $name,
                'customer_email' => $email,
                'service_name'   => $service['name'],
                'provider_name'  => (string)Database::value("SELECT name FROM booking_providers WHERE id=?", [$providerId]),
                'starts_at'      => $startsAtSql,
            ]);
        } catch (\Throwable $e) {
            slate_log('Booking: confirmation email failed: ' . $e->getMessage(), 'warning');
        }

        return ['ok' => true, 'id' => $id, 'ref' => $ref];
    }

    public static function cancelAppointment(int $id, string $reason = ''): bool {
        $tid = current_tenant_id();
        $row = Database::row("SELECT * FROM booking_appointments WHERE id = ? AND tenant_id = ?", [$id, $tid]);
        if (!$row) return false;
        Database::update('booking_appointments', ['status' => 'cancelled'], 'id = ?', [$id]);
        AuditLog::record('booking.cancelled', (string)$id, ['reason' => $reason]);
        Hook::doAction('booking_cancelled', $id);
        return true;
    }

    public static function generateRef(): string {
        return 'BK-' . strtoupper(bin2hex(random_bytes(4)));
    }

    /**
     * Email helpers
     */

    public static function sendConfirmation(array $args): bool {
        $siteName = Database::setting('site_name') ?: 'Slate';
        $when     = date('l, j F Y, H:i', strtotime($args['starts_at']));
        $body     = '<p>Hi ' . e($args['customer_name']) . ',</p>'
                  . '<p>Your booking is confirmed.</p>'
                  . '<table style="border-collapse:collapse;font-family:sans-serif;font-size:14px;">'
                  . '<tr><td style="padding:6px 12px;color:#555;">Service</td><td style="padding:6px 12px;">' . e($args['service_name']) . '</td></tr>'
                  . '<tr><td style="padding:6px 12px;color:#555;">With</td><td style="padding:6px 12px;">'    . e($args['provider_name']) . '</td></tr>'
                  . '<tr><td style="padding:6px 12px;color:#555;">When</td><td style="padding:6px 12px;">'    . e($when) . '</td></tr>'
                  . '<tr><td style="padding:6px 12px;color:#555;">Reference</td><td style="padding:6px 12px;"><code>' . e($args['ref']) . '</code></td></tr>'
                  . '</table>'
                  . '<p>If you need to cancel or reschedule, reply to this email.</p>'
                  . '<p style="color:#888;font-size:12px;margin-top:24px;">— ' . e($siteName) . '</p>';
        return (bool) Mailer::send($args['customer_email'], 'Your booking is confirmed (' . $args['ref'] . ')', $body, $args['customer_name']);
    }

    public static function sendReminder(array $appt, string $when): void {
        $body = '<p>Hi ' . e($appt['customer_name']) . ',</p>'
              . '<p>This is a reminder that your appointment for <strong>' . e($appt['service_name']) . '</strong> '
              . 'with ' . e($appt['provider_name']) . ' is in ' . e($when) . '.</p>'
              . '<p>Starts: <strong>' . e(date('l, j F Y, H:i', strtotime($appt['starts_at']))) . '</strong></p>'
              . '<p>Reference: <code>' . e($appt['ref']) . '</code></p>';
        Mailer::send($appt['customer_email'], 'Reminder: ' . $appt['service_name'] . ' in ' . $when, $body, $appt['customer_name']);
    }

    public static function slugify(string $name, ?int $excludeId = null): string {
        $base = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $name), '-'));
        if ($base === '') $base = 'service';
        if (mb_strlen($base) > 60) $base = substr($base, 0, 60);
        $slug = $base;
        $i = 2;
        $tid = current_tenant_id();
        while (true) {
            $row = Database::row(
                "SELECT id FROM booking_services WHERE tenant_id = ? AND slug = ?" . ($excludeId ? " AND id <> ?" : ""),
                $excludeId ? [$tid, $slug, $excludeId] : [$tid, $slug]
            );
            if (!$row) return $slug;
            $slug = $base . '-' . $i++;
            if ($i > 999) return $base . '-' . bin2hex(random_bytes(3));
        }
    }
}
