<?php

declare(strict_types=1);

namespace App\Core;

use App\Core\Database as DB;

/**
 * Guest-house nightly availability engine. A stay occupies whole nights from
 * check-in (inclusive) to check-out (exclusive); a night is unavailable if it
 * falls inside another live stay, an owner blackout, or in the past. Used by
 * both the public /api/guesthouse/availability endpoint and the server-side
 * booking guard in PaymentController, so the two never disagree.
 */
final class Stay
{
    /** Bookings in these states no longer hold their nights. */
    private const FREED_STATES = ['cancelled', 'failed'];

    /**
     * Unavailable nights within [$from, $to) — booked, blacked-out, or past.
     *
     * @return list<string> sorted, de-duplicated "YYYY-MM-DD"
     */
    public static function unavailableDates(string $from, string $to): array
    {
        Schema::heal();
        if (!self::isDate($from) || !self::isDate($to) || $from >= $to) {
            return [];
        }
        $today = date('Y-m-d');
        $occupied = self::occupiedSet();
        $out = [];
        foreach (self::eachNight($from, $to) as $d) {
            if ($d < $today || isset($occupied[$d])) {
                $out[] = $d;
            }
        }
        return $out;
    }

    /** True only if every night in [$checkIn, $checkOut) is genuinely bookable now. */
    public static function isRangeOpen(string $checkIn, string $checkOut, int $minNights = 1): bool
    {
        Schema::heal();
        if (!self::isDate($checkIn) || !self::isDate($checkOut) || $checkIn < date('Y-m-d')) {
            return false;
        }
        $nights = self::nights($checkIn, $checkOut);
        if ($nights < 1 || $nights < max(1, $minNights)) {
            return false;
        }
        $occupied = self::occupiedSet();
        foreach (self::eachNight($checkIn, $checkOut) as $d) {
            if (isset($occupied[$d])) {
                return false;
            }
        }
        return true;
    }

    /** Number of nights between two dates (0 if invalid or non-positive). */
    public static function nights(string $checkIn, string $checkOut): int
    {
        if (!self::isDate($checkIn) || !self::isDate($checkOut)) {
            return 0;
        }
        $a = strtotime($checkIn);
        $b = strtotime($checkOut);
        if ($a === false || $b === false || $b <= $a) {
            return 0;
        }
        return (int) round(($b - $a) / 86400);
    }

    /** @return array<string,true> every night blocked by a live booking or a blackout */
    private static function occupiedSet(): array
    {
        $set = [];
        foreach (DB::select('SELECT date FROM stay_blackouts') as $r) {
            $set[(string) $r['date']] = true;
        }
        $placeholders = implode(',', array_fill(0, count(self::FREED_STATES), '?'));
        foreach (DB::select(
            "SELECT check_in, check_out FROM bookings
             WHERE item_type = 'stay' AND status NOT IN ($placeholders)
               AND check_in <> '' AND check_out <> ''",
            self::FREED_STATES,
        ) as $b) {
            foreach (self::eachNight((string) $b['check_in'], (string) $b['check_out']) as $d) {
                $set[$d] = true;
            }
        }
        return $set;
    }

    /**
     * Nights from $from (inclusive) to $to (exclusive).
     *
     * @return list<string>
     */
    private static function eachNight(string $from, string $to): array
    {
        if (!self::isDate($from) || !self::isDate($to)) {
            return [];
        }
        $t = strtotime($from);
        $end = strtotime($to);
        if ($t === false || $end === false) {
            return [];
        }
        $out = [];
        $guard = 0;
        while ($t < $end && $guard++ < 800) { // guard caps runaway ranges (~2 years)
            $out[] = date('Y-m-d', $t);
            $t = strtotime('+1 day', $t);
        }
        return $out;
    }

    private static function isDate(string $d): bool
    {
        return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d) === 1
            && checkdate((int) substr($d, 5, 2), (int) substr($d, 8, 2), (int) substr($d, 0, 4));
    }
}
