<?php
declare(strict_types=1);

require_once __DIR__ . '/db.php';

/**
 * Availability engine: staff working hours ∩ service duration ∩ existing bookings.
 */

function availability_day_key(DateTimeInterface $date): string
{
    return strtolower($date->format('D')); // mon, tue, ...
}

/**
 * Date-specific schedule exception for one barber (full day off or custom hours).
 *
 * @return array{is_day_off:bool,open:?string,close:?string,reason:?string}|null
 */
function availability_staff_exception(int $staffId, string $ymd): ?array
{
    static $cache = [];
    $key = $staffId . '|' . $ymd;
    if (array_key_exists($key, $cache)) {
        return $cache[$key];
    }
    try {
        $stmt = db()->prepare(
            'SELECT is_day_off, open_time, close_time, reason
             FROM staff_time_off WHERE staff_id = ? AND off_date = ? LIMIT 1'
        );
        $stmt->execute([$staffId, $ymd]);
        $row = $stmt->fetch();
        if (!$row) {
            $cache[$key] = null;
            return null;
        }
        $open = $row['open_time'] !== null ? substr((string) $row['open_time'], 0, 5) : null;
        $close = $row['close_time'] !== null ? substr((string) $row['close_time'], 0, 5) : null;
        $cache[$key] = [
            'is_day_off' => (int) ($row['is_day_off'] ?? 1) === 1,
            'open' => $open,
            'close' => $close,
            'reason' => $row['reason'] !== null ? (string) $row['reason'] : null,
        ];
    } catch (Throwable $e) {
        $cache[$key] = null;
    }
    return $cache[$key];
}

function availability_staff_is_off(int $staffId, string $ymd): bool
{
    $ex = availability_staff_exception($staffId, $ymd);
    return $ex !== null && !empty($ex['is_day_off']);
}

/**
 * Effective open/close for a barber on a date, or null if unavailable that day.
 *
 * @return array{open:string,close:string}|null
 */
function availability_staff_day_window(array $staff, string $ymd): ?array
{
    $ex = availability_staff_exception((int) $staff['id'], $ymd);
    if ($ex !== null) {
        if (!empty($ex['is_day_off'])) {
            return null;
        }
        if ($ex['open'] && $ex['close']) {
            return ['open' => $ex['open'], 'close' => $ex['close']];
        }
        // Custom hours row missing times — treat as unavailable
        return null;
    }

    $hours = json_decode((string) ($staff['working_hours'] ?? '{}'), true);
    if (!is_array($hours)) {
        return null;
    }
    $dayKey = availability_day_key(new DateTime($ymd . ' 12:00:00'));
    if (!array_key_exists($dayKey, $hours) || $hours[$dayKey] === null) {
        return null;
    }
    $open = (string) ($hours[$dayKey]['open'] ?? '');
    $close = (string) ($hours[$dayKey]['close'] ?? '');
    if ($open === '' || $close === '') {
        return null;
    }
    return ['open' => $open, 'close' => $close];
}

/**
 * @return list<array{id:int,name:string,working_hours:?string}>
 */
function availability_eligible_staff(int $tenantId, int $serviceId, ?int $staffId = null): array
{
    if ($staffId) {
        $stmt = db()->prepare(
            'SELECT sb.id, sb.name, sb.working_hours
             FROM staff_barbers sb
             WHERE sb.tenant_id = ? AND sb.id = ?'
        );
        $stmt->execute([$tenantId, $staffId]);
        $row = $stmt->fetch();
        if (!$row) {
            return [];
        }
        // If service assignments exist for this staff, enforce them
        $chk = db()->prepare('SELECT 1 FROM staff_services WHERE staff_id = ? LIMIT 1');
        $chk->execute([$staffId]);
        if ($chk->fetch()) {
            $ok = db()->prepare('SELECT 1 FROM staff_services WHERE staff_id = ? AND service_id = ?');
            $ok->execute([$staffId, $serviceId]);
            if (!$ok->fetch()) {
                return [];
            }
        }
        return [$row];
    }

    // Prefer staff linked to this service
    $stmt = db()->prepare(
        'SELECT sb.id, sb.name, sb.working_hours
         FROM staff_barbers sb
         INNER JOIN staff_services ss ON ss.staff_id = sb.id AND ss.service_id = ?
         WHERE sb.tenant_id = ?
         ORDER BY sb.name'
    );
    $stmt->execute([$serviceId, $tenantId]);
    $rows = $stmt->fetchAll();
    if ($rows) {
        return $rows;
    }

    // Fallback: all shop staff (no assignments configured)
    $all = db()->prepare(
        'SELECT id, name, working_hours FROM staff_barbers WHERE tenant_id = ? ORDER BY name'
    );
    $all->execute([$tenantId]);
    return $all->fetchAll();
}

/**
 * @return list<array{starts_at:string,ends_at:string,staff_id:?int,status:string}>
 */
function availability_bookings_for_day(int $tenantId, string $ymd, ?int $staffId = null, ?int $excludeBookingId = null): array
{
    $sql = 'SELECT starts_at, ends_at, staff_id, status
            FROM bookings
            WHERE tenant_id = ?
              AND starts_at >= ?
              AND starts_at < DATE_ADD(?, INTERVAL 1 DAY)
              AND status != \'cancelled\'
              AND NOT (status = \'awaiting_payment\' AND created_at < DATE_SUB(NOW(), INTERVAL 45 MINUTE))';
    $params = [$tenantId, $ymd . ' 00:00:00', $ymd];
    if ($excludeBookingId) {
        $sql .= ' AND id != ?';
        $params[] = $excludeBookingId;
    }
    if ($staffId) {
        $sql .= ' AND (staff_id = ? OR staff_id IS NULL)';
        $params[] = $staffId;
    }
    $stmt = db()->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchAll();
}

function availability_ranges_overlap(DateTimeInterface $aStart, DateTimeInterface $aEnd, DateTimeInterface $bStart, DateTimeInterface $bEnd): bool
{
    return $aStart < $bEnd && $bStart < $aEnd;
}

/**
 * @param list<array{starts_at:string,ends_at:string,staff_id:?int,status:string}> $bookings
 */
function availability_slot_free(DateTime $start, DateTime $end, array $bookings, ?int $staffId): bool
{
    foreach ($bookings as $b) {
        $bStaff = $b['staff_id'] !== null ? (int) $b['staff_id'] : null;
        // Unassigned booking blocks all chairs; assigned blocks that staff (and null-staff queries already filtered)
        if ($staffId !== null && $bStaff !== null && $bStaff !== $staffId) {
            continue;
        }
        $bStart = new DateTime((string) $b['starts_at']);
        $bEnd = new DateTime((string) $b['ends_at']);
        if (availability_ranges_overlap($start, $end, $bStart, $bEnd)) {
            return false;
        }
    }
    return true;
}

/**
 * Generate open slots for one staff on one day.
 *
 * @return list<array{time:string,staff_id:int,staff_name:string,starts_at:string,ends_at:string}>
 */
function availability_slots_for_staff(
    array $staff,
    string $ymd,
    int $durationMinutes,
    array $dayBookings,
    int $stepMinutes = 15,
    ?DateTime $notBefore = null
): array {
    $window = availability_staff_day_window($staff, $ymd);
    if ($window === null) {
        return [];
    }
    $open = $window['open'];
    $close = $window['close'];

    $cursor = DateTime::createFromFormat('Y-m-d H:i', $ymd . ' ' . $open);
    $dayEnd = DateTime::createFromFormat('Y-m-d H:i', $ymd . ' ' . $close);
    if (!$cursor || !$dayEnd) {
        return [];
    }

    $now = $notBefore ?? new DateTime('now');
    $slots = [];
    $staffId = (int) $staff['id'];
    $staffName = (string) $staff['name'];

    while (true) {
        $slotEnd = clone $cursor;
        $slotEnd->modify('+' . $durationMinutes . ' minutes');
        if ($slotEnd > $dayEnd) {
            break;
        }
        if ($cursor >= $now && availability_slot_free($cursor, $slotEnd, $dayBookings, $staffId)) {
            $slots[] = [
                'time' => $cursor->format('H:i'),
                'staff_id' => $staffId,
                'staff_name' => $staffName,
                'starts_at' => $cursor->format('Y-m-d H:i:s'),
                'ends_at' => $slotEnd->format('Y-m-d H:i:s'),
            ];
        }
        $cursor->modify('+' . $stepMinutes . ' minutes');
    }

    return $slots;
}

/**
 * Public slot list for a tenant/service/date.
 * When staffId is null ("any"), returns one row per time using the first available barber.
 *
 * @return array{ok:bool,date:string,duration_minutes:int,slots:list<array>,error?:string}
 */
function availability_get_slots(int $tenantId, int $serviceId, string $ymd, ?int $staffId = null, bool $collapseAny = true, ?int $excludeBookingId = null): array
{
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $ymd)) {
        return ['ok' => false, 'date' => $ymd, 'duration_minutes' => 0, 'slots' => [], 'error' => 'Invalid date'];
    }

    $svc = db()->prepare('SELECT id, duration_minutes, name FROM services WHERE id = ? AND tenant_id = ?');
    $svc->execute([$serviceId, $tenantId]);
    $service = $svc->fetch();
    if (!$service) {
        return ['ok' => false, 'date' => $ymd, 'duration_minutes' => 0, 'slots' => [], 'error' => 'Invalid service'];
    }

    $duration = (int) $service['duration_minutes'];
    $staffRows = availability_eligible_staff($tenantId, $serviceId, $staffId);
    if (!$staffRows) {
        return [
            'ok' => true,
            'date' => $ymd,
            'duration_minutes' => $duration,
            'service_name' => $service['name'],
            'slots' => [],
            'message' => 'No eligible barbers for this service on this day.',
        ];
    }

    $bookings = availability_bookings_for_day($tenantId, $ymd, null, $excludeBookingId);
    $notBefore = new DateTime('now');
    // Allow same-day only for future minutes
    $dayStart = new DateTime($ymd . ' 00:00:00');
    if ($dayStart->format('Y-m-d') > $notBefore->format('Y-m-d')) {
        $notBefore = $dayStart;
    }

    $all = [];
    foreach ($staffRows as $staff) {
        $all = array_merge(
            $all,
            availability_slots_for_staff($staff, $ymd, $duration, $bookings, 15, $notBefore)
        );
    }

    usort($all, static fn($a, $b) => [$a['starts_at'], $a['staff_name']] <=> [$b['starts_at'], $b['staff_name']]);

    if ($collapseAny && $staffId === null) {
        $collapsed = [];
        $seen = [];
        foreach ($all as $slot) {
            $t = $slot['time'];
            if (isset($seen[$t])) {
                continue;
            }
            $seen[$t] = true;
            $collapsed[] = $slot;
        }
        $all = $collapsed;
    }

    return [
        'ok' => true,
        'date' => $ymd,
        'duration_minutes' => $duration,
        'service_name' => $service['name'],
        'slots' => $all,
    ];
}

/**
 * Find the earliest open slot within $daysAhead days.
 *
 * @return array{ok:bool,slot?:array,error?:string}
 */
function availability_first_available(int $tenantId, int $serviceId, ?int $staffId = null, int $daysAhead = 14): array
{
    $day = new DateTime('today');
    for ($i = 0; $i < $daysAhead; $i++) {
        $ymd = $day->format('Y-m-d');
        $result = availability_get_slots($tenantId, $serviceId, $ymd, $staffId, true);
        if (!empty($result['slots'])) {
            $slot = $result['slots'][0];
            return [
                'ok' => true,
                'slot' => $slot,
                'date' => $ymd,
                'duration_minutes' => $result['duration_minutes'],
                'service_name' => $result['service_name'] ?? null,
            ];
        }
        $day->modify('+1 day');
    }
    return ['ok' => false, 'error' => 'No open slots in the next ' . $daysAhead . ' days.'];
}

/**
 * Re-validate a proposed booking start against live availability.
 */
function availability_is_bookable(int $tenantId, int $serviceId, string $startsAt, ?int $staffId, ?int $excludeBookingId = null): bool
{
    $starts = DateTime::createFromFormat('Y-m-d H:i:s', $startsAt)
        ?: DateTime::createFromFormat('Y-m-d H:i', $startsAt);
    if (!$starts) {
        return false;
    }
    $ymd = $starts->format('Y-m-d');
    $time = $starts->format('H:i');
    $result = availability_get_slots($tenantId, $serviceId, $ymd, $staffId, false, $excludeBookingId);
    foreach ($result['slots'] as $slot) {
        if ($slot['time'] === $time && ($staffId === null || (int) $slot['staff_id'] === $staffId)) {
            return true;
        }
        if ($staffId === null && $slot['time'] === $time) {
            return true;
        }
    }
    return false;
}

/**
 * Resolve which staff_id to assign when client chose "any" for a given slot time.
 */
function availability_resolve_staff_for_slot(int $tenantId, int $serviceId, string $ymd, string $time, ?int $preferredStaffId, ?int $excludeBookingId = null): ?int
{
    if ($preferredStaffId) {
        $result = availability_get_slots($tenantId, $serviceId, $ymd, $preferredStaffId, false, $excludeBookingId);
        foreach ($result['slots'] as $slot) {
            if ($slot['time'] === $time && (int) $slot['staff_id'] === $preferredStaffId) {
                return $preferredStaffId;
            }
        }
        return null;
    }
    $result = availability_get_slots($tenantId, $serviceId, $ymd, null, true, $excludeBookingId);
    foreach ($result['slots'] as $slot) {
        if ($slot['time'] === $time) {
            return (int) $slot['staff_id'];
        }
    }
    return null;
}
