<?php
// ─────────────────────────────────────────────────────────────
//  lib/BookingEngine.php — Slot availability & access engine
// ─────────────────────────────────────────────────────────────
if (!defined('DB_HOST')) require_once dirname(__DIR__) . '/lib.php';

class BookingEngine {

    /**
     * Get available time slots for a provider+service on a given date.
     * Returns array of slot objects.
     */
    public static function getSlots(
        int $providerId,
        int $serviceId,
        string $date  // 'Y-m-d'
    ): array {
        // 1. Get provider working hours for this day_of_week
        $dow = (int) date('w', strtotime($date)); // 0=Sun
        $hours = db()->prepare(
            'SELECT start_time, end_time FROM bk_provider_hours
             WHERE provider_id=? AND day_of_week=? AND is_active=1'
        );
        $hours->execute([$providerId, $dow]);
        $h = $hours->fetch();
        if (!$h) return []; // provider not working this day

        // 2. Get service duration + provider capacity
        $ps = db()->prepare(
            'SELECT ps.capacity_per_slot, s.duration_min
             FROM bk_provider_services ps
             JOIN bk_services s ON s.id = ps.service_id
             WHERE ps.provider_id=? AND ps.service_id=?'
        );
        $ps->execute([$providerId, $serviceId]);
        $config = $ps->fetch();
        if (!$config) return [];

        $duration = (int) $config['duration_min'];
        $capacity = (int) $config['capacity_per_slot'];

        // 3. Generate all possible slot start times
        $start = strtotime($date . ' ' . $h['start_time']);
        $end   = strtotime($date . ' ' . $h['end_time']);
        $now   = time();
        $slots = [];
        $cursor = $start;
        while ($cursor + $duration * 60 <= $end) {
            $slots[] = $cursor;
            $cursor += $duration * 60;
        }

        // 4. Get breaks for this date
        $breaks = db()->prepare(
            'SELECT start_time, end_time FROM bk_provider_breaks
             WHERE provider_id=? AND break_date=?'
        );
        $breaks->execute([$providerId, $date]);
        $breakList = $breaks->fetchAll();

        // 5. Get existing appointment counts per slot
        $appts = db()->prepare(
            'SELECT start_datetime, COUNT(*) as cnt
             FROM bk_appointments
             WHERE provider_id=? AND service_id=?
               AND DATE(start_datetime)=?
               AND status NOT IN ("cancelled","no_show")
             GROUP BY start_datetime'
        );
        $appts->execute([$providerId, $serviceId, $date]);
        $bookedMap = [];
        foreach ($appts->fetchAll() as $a) {
            $bookedMap[strtotime($a['start_datetime'])] = (int)$a['cnt'];
        }

        // 6. Build result — filter past, filter breaks
        $result = [];
        foreach ($slots as $ts) {
            if ($ts <= $now) continue; // past

            // Check break overlap
            $slotTime = date('H:i:s', $ts);
            $slotEnd  = date('H:i:s', $ts + $duration * 60);
            $inBreak = false;
            foreach ($breakList as $b) {
                if ($slotTime < $b['end_time'] && $slotEnd > $b['start_time']) {
                    $inBreak = true; break;
                }
            }
            if ($inBreak) continue;

            $booked    = $bookedMap[$ts] ?? 0;
            $available = $booked < $capacity;

            $result[] = [
                'time'      => date('H:i', $ts),
                'datetime'  => date('Y-m-d H:i:s', $ts),
                'timestamp' => $ts,
                'available' => $available,
                'booked'    => $booked,
                'capacity'  => $capacity,
                'full'      => !$available,
            ];
        }

        return $result;
    }

    /**
     * Check if a customer email has an active membership that unlocks a service.
     */
    public static function checkAccess(string $email, int $serviceId): array {
        // Get customer
        $cust = db()->prepare('SELECT * FROM bk_customers WHERE email=?');
        $cust->execute([$email]);
        $customer = $cust->fetch();
        if (!$customer) return ['allowed' => false, 'reason' => 'no_customer'];

        // Get active memberships
        $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) return ['allowed' => false, 'reason' => 'no_membership'];

        // Check service is in plan
        $svc = db()->prepare(
            'SELECT 1 FROM bk_plan_services
             WHERE plan_id=? AND service_id=?'
        );
        $svc->execute([$membership['plan_id'], $serviceId]);
        if (!$svc->fetch()) {
            return ['allowed' => false, 'reason' => 'service_not_in_plan'];
        }

        return [
            'allowed'    => true,
            'customer'   => $customer,
            'membership' => $membership,
        ];
    }

    /**
     * Get all services unlocked by a customer's active membership.
     */
    public static function getUnlockedServices(string $email): array {
        $cust = db()->prepare('SELECT id FROM bk_customers WHERE email=?');
        $cust->execute([$email]);
        $customer = $cust->fetch();
        if (!$customer) return [];

        $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) return [];

        $svcs = db()->prepare(
            'SELECT s.id, s.name, s.duration_min, s.color
             FROM bk_plan_services ps
             JOIN bk_services s ON s.id = ps.service_id
             WHERE ps.plan_id=? AND s.is_active=1
             ORDER BY s.name'
        );
        $svcs->execute([$membership['plan_id']]);
        $res = $svcs->fetchAll();
        return $res;
    }

    /**
     * Check if a specific slot is still available (for race-condition protection).
     * Must be called inside a transaction.
     */
    public static function isSlotAvailable(
        int $providerId,
        int $serviceId,
        string $datetime
    ): bool {
        $ps = db()->prepare(
            'SELECT ps.capacity_per_slot FROM bk_provider_services ps
             WHERE ps.provider_id=? AND ps.service_id=?'
        );
        $ps->execute([$providerId, $serviceId]);
        $config = $ps->fetch();
        if (!$config) return false;

        $capacity = (int) $config['capacity_per_slot'];

        $cnt = db()->prepare(
            'SELECT COUNT(*) FROM bk_appointments
             WHERE provider_id=? AND service_id=? AND start_datetime=?
               AND status NOT IN ("cancelled","no_show")'
        );
        $cnt->execute([$providerId, $serviceId, $datetime]);
        $booked = (int) $cnt->fetchColumn();

        return $booked < $capacity;
    }

    /**
     * Generate a booking reference.
     */
    public static function genRef(): string {
        return 'BK-' . strtoupper(base_convert((string)time(), 10, 36))
             . '-' . strtoupper(substr(bin2hex(random_bytes(3)), 0, 6));
    }

    // ─────────────────────────────────────────────────────────
    //  PHASE 1 — Session-limited membership methods
    // ─────────────────────────────────────────────────────────

    /**
     * Check whether a membership has sessions remaining.
     * Returns ['ok' => true] or ['ok' => false, 'error' => '...'].
     *
     * Skips the check entirely when module_membership is disabled,
     * so walk-in bookings always pass through.
     */
    public static function checkSessionLimit(int $membershipId): array {
        if (!defined('DB_HOST')) require_once dirname(__DIR__) . '/lib.php';
        if (class_exists('ModuleToggle') && !ModuleToggle::isEnabled('module_membership')) {
            return ['ok' => true];
        }

        $stmt = db()->prepare(
            'SELECT sessions_remaining FROM bk_memberships
             WHERE id = ? AND status = "active" LIMIT 1'
        );
        $stmt->execute([$membershipId]);
        $remaining = $stmt->fetchColumn();

        if ($remaining === false) {
            return ['ok' => false, 'error' => 'Membership not found or inactive'];
        }
        // NULL = unlimited plan, no gate needed
        if ($remaining !== null && (int) $remaining <= 0) {
            return [
                'ok'    => false,
                'error' => 'No sessions remaining. Please renew your membership.',
            ];
        }
        return ['ok' => true];
    }

    /**
     * Atomically deduct one session from a membership.
     * MUST be called inside an active transaction.
     *
     * Returns true on success, false if the session was already at zero
     * (race condition — caller should rollback and return an error).
     *
     * Does nothing (returns true) when sessions_remaining IS NULL (unlimited).
     */
    public static function deductSession(int $membershipId): bool {
        $stmt = db()->prepare(
            'UPDATE bk_memberships
             SET sessions_remaining = sessions_remaining - 1
             WHERE id = ?
               AND sessions_remaining > 0
               AND sessions_remaining IS NOT NULL'
        );
        $stmt->execute([$membershipId]);

        // rowCount() == 0 means either it was NULL (unlimited, OK)
        // or it was already 0 (race condition, not OK). Distinguish:
        if ($stmt->rowCount() === 0) {
            $check = db()->prepare(
                'SELECT sessions_remaining FROM bk_memberships WHERE id = ?'
            );
            $check->execute([$membershipId]);
            $val = $check->fetchColumn();
            // NULL = unlimited, deduction not needed — that's fine
            if ($val === null) return true;
            // 0 or less = race condition
            return false;
        }
        return true;
    }
}
