<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Core\Util;

use BookiFlex\Core\Provider\LocalTimeZone;
use DateTimeImmutable;
use DateTimeZone;

defined( 'ABSPATH' ) || exit;

final class DateTimeUtils
{
    public static function toUtc(DateTimeImmutable $date): DateTimeImmutable
    {
        return $date->setTimezone(new DateTimeZone('UTC'));
    }

    public static function toUtcAtMidday(DateTimeImmutable $date): DateTimeImmutable
    {
        return self::toUtc($date)->setTime(12, 0);
    }

    public static function nowUtc(): DateTimeImmutable
    {
        return new DateTimeImmutable('now', new DateTimeZone('UTC'));
    }

    public static function nowAtLocalTimeZone(): DateTimeImmutable
    {
        return new DateTimeImmutable('now', LocalTimeZone::get());
    }

    /**
     * @param array $dateRanges <p>of arrays ['dateFrom' => YYYY-MM-DD, 'dateTo' => YYYY-MM-DD]</p>
     * @return Array<DateTimeImmutable|null, DateTimeImmutable|null> <p>[DateTimeImmutable, DateTimeImmutable]</p>
     */
    public static function findMinMaxInRanges(array $dateRanges): array
    {
        $min = null;
        $max = null;

        foreach ($dateRanges as $range) {
            if (!empty($range['dateFrom'])) {
                $date = (DateTimeImmutable::createFromFormat('Y-m-d', $range['dateFrom'], LocalTimeZone::get()))
                    ->setTime(0, 0);

                if ((!isset($min)) || (($date < $min))) {
                    $min = $date;
                }
            }

            if (!empty($range['dateTo'])) {
                $date = (DateTimeImmutable::createFromFormat('Y-m-d', $range['dateTo'], LocalTimeZone::get()))
                    ->setTime(23, 59, 59);

                if ((!isset($max)) || (($date > $max))) {
                    $max = $date;
                }
            }
        }

        // If last period is null then max will be Infinity (null)
        $size       = count($dateRanges);
        $lastDateTo = $dateRanges[$size - 1]['dateTo'];
        if ($size > 1 && empty($lastDateTo)) {
            $max = null;
        }

        return [$min, $max];
    }

    /**
     * This method returns a boolean value indicating whether the dates are continuous.
     *
     * @param string[] $dates array of dates in Y-m-d format
     *
     * @return bool
     */
    public static function isPeriodSequenced(array $dates): bool
    {
        try {
            $dates = array_map(function (string $date) {
                return new DateTimeImmutable($date, new DateTimeZone('UTC'));
            }, $dates);
        } catch (\Exception $e) {
            unset($e);
            return false;
        }

        $number = count($dates);
        if ($number === 1) {
            return true;
        }

        usort($dates, function (\DateTimeInterface $d1, \DateTimeInterface $d2) {
            if ($d1 == $d2) {
                return 0;
            }
            return ($d1 < $d2) ? -1 : 1;
        });

        $i = $number - 1;
        while ($i > 0) {
            if ($dates[$i]->diff($dates[$i - 1])->days !== 1) {
                return false;
            }
            $i--;
        }

        return true;
    }

    /**
     * Return length of stay between two dates in the number of days
     *
     * @param string|\DateTimeInterface $dateFrom
     * @param string|\DateTimeInterface $dateTo
     * @return int number of days
     */
    public static function lengthOfStay(string|\DateTimeInterface $dateFrom, string|\DateTimeInterface $dateTo): int
    {
        try {
            $dateFrom = self::normalizeToDate($dateFrom)->setTime(12, 0);
            $dateTo   = self::normalizeToDate($dateTo)->setTime(12, 0);
        } catch (\Exception $e) {
            unset($e);
            return 0;
        }

        return (int) $dateFrom->diff($dateTo)->format('%a');
    }

    /**
     * @throws \Exception
     */
    public static function normalizeToDate($date): DateTimeImmutable
    {
        if (is_string($date)) {
            $dateTime = DateTimeImmutable::createFromFormat('Y-m-d', $date, LocalTimeZone::get());
            if (!$dateTime) {
                throw new \InvalidArgumentException("Invalid date string format. Expected Y-m-d.");
            }
            return $dateTime;
        }

        if ($date instanceof \DateTimeInterface) {
            return (new DateTimeImmutable($date->format('Y-m-d'), LocalTimeZone::get()));
        }

        throw new \InvalidArgumentException("Expected string (Y-m-d) or DateTimeInterface.");
    }

    /**
     * @param string $date date at Y-m-d format
     * @param string $time time at H:i format
     * @return DateTimeImmutable at local time zone
     */
    public static function createDateTimeImmutable(string $date, string $time): DateTimeImmutable
    {
        [$hours, $minutes] = explode(':', $time);
        return (DateTimeImmutable::createFromFormat('Y-m-d', $date, LocalTimeZone::get()))
            ->setTime((int) $hours, (int) $minutes);
    }

    /**
     * Format a number of hours as a human-readable duration string.
     *
     * Values below 24 h are shown as hours only. From 24 h onwards the value
     * is expressed in days (and remaining hours when not a whole number of days).
     *
     * Examples: 6 → "6 hours", 24 → "1 day", 36 → "1 day and 12 hours", 48 → "2 days"
     */
    public static function formatHoursDuration(int $hours): string
    {
        if ($hours < 24) {
            /* translators: %d: number of hours */
            return sprintf(_n('%d hour', '%d hours', $hours, 'bookiflex'), $hours);
        }

        $days = intdiv($hours, 24);
        $rem  = $hours % 24;

        /* translators: %d: number of days */
        $daysStr = sprintf(_n('%d day', '%d days', $days, 'bookiflex'), $days);

        if ($rem === 0) {
            return $daysStr;
        }

        /* translators: %d: number of hours */
        $hoursStr = sprintf(_n('%d hour', '%d hours', $rem, 'bookiflex'), $rem);

        return sprintf(
            /* translators: %1$s: number of days, %2$s: number of hours */
            __('%1$s and %2$s', 'bookiflex'),
            $daysStr,
            $hoursStr
        );
    }

    /**
     * Return number of days left before arrival date
     *
     * @param DateTimeImmutable $arrivalDate
     * @return int number of days
     */
    public static function getLeftDaysBeforeArrival(DateTimeImmutable $arrivalDate): int
    {
        $arrivalDayStart = (new DateTimeImmutable($arrivalDate->format('Y-m-d'), LocalTimeZone::get()));
        $now             = self::nowAtLocalTimeZone();

        // If arrival day has already come or is in past
        if ($now >= $arrivalDayStart) {
            return 0;
        }

        // Count full days between now and arrival day start
        return (int) $now->diff($arrivalDayStart)->format('%a');
    }
}
