<?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\DateRange;

use BookiFlex\Core\Exception\InvalidArgumentException;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;
use DateTimeInterface;
use Iterator;

defined( 'ABSPATH' ) || exit;

/**
 * Create date range from dateFrom to dateTo.
 * Supports two strategies: day (use last day) and night.
 */
class DateRange implements Iterator
{
    public const DAY_STRATEGY   = 'day';
    public const NIGHT_STRATEGY = 'night';

    private DateTimeImmutable $today;

    /** @var DateTimeImmutable $current  */
    private DateTimeInterface $current;

    private DatePeriod $period;

    private string $strategy;

    /**
     * @param DateTimeImmutable|string $dateFrom
     * @param DateTimeImmutable|string $dateTo
     * @param string $strategy
     * @throws InvalidArgumentException
     */
    public function __construct(DateTimeImmutable|string $dateFrom, DateTimeImmutable|string $dateTo, string $strategy = self::NIGHT_STRATEGY)
    {
        $this->strategy($strategy);
        $this->today = new DateTimeImmutable('today');

        $this->init($dateFrom, $dateTo);
    }

    /**
     * @param DateTimeImmutable|string $from
     * @param DateTimeImmutable|string $to
     * @throws InvalidArgumentException
     */
    protected function init(DateTimeImmutable|string $from, DateTimeImmutable|string $to): void
    {
        if (!$from instanceof DateTimeInterface) {
            $dateFrom = DateTimeImmutable::createFromFormat('Y-m-d', $from);

            if (!$dateFrom) {
                throw new InvalidArgumentException(
                    sprintf(
                        'Date from %s is wrong.',
                        $from
                    ),
                    400,
                    null,
                    ['date' => $from]
                );
            }
        } else {
            $dateFrom = DateTimeImmutable::createFromFormat('Y-m-d', $from->format('Y-m-d'));
        }

        if (!$to instanceof DateTimeInterface) {
            $dateTo = DateTimeImmutable::createFromFormat('Y-m-d', $to);

            if (!$dateTo) {
                throw new InvalidArgumentException(
                    sprintf(
                        'Date to %s is wrong.',
                        $to
                    ),
                    400,
                    null,
                    ['date' => $to]
                );
            }
        } else {
            $dateTo = DateTimeImmutable::createFromFormat('Y-m-d', $to->format('Y-m-d'));
        }

        $dateFrom = $dateFrom->setTime(0, 0, 0);
        $dateTo   = $dateTo->setTime(23, 59, 59);

        $this->assertDateFromLessOrEqualThanDateTo($dateFrom, $dateTo);

        $dateTo = ($this->strategy === self::NIGHT_STRATEGY)
            ? $dateTo->modify('-1 day')
            : $dateTo;

        $this->period = new DatePeriod(
            $dateFrom,
            new DateInterval('P1D'),
            $dateTo,
            DatePeriod::EXCLUDE_START_DATE
        );

        $this->current = $this->period->getStartDate();

        if (!$this->period->include_start_date) {
            $this->next();
        }
    }

    public function rewind(): void
    {
        $this->current = DateTimeImmutable::createFromInterface($this->period->getStartDate());
    }

    #[\ReturnTypeWillChange]
    public function current()
    {
        return clone $this->current;
    }

    #[\ReturnTypeWillChange]
    public function key()
    {
        return $this->current->diff($this->period->start)->d;
    }

    public function next(): void
    {
        $this->current = $this->current->add($this->period->getDateInterval());
    }

    public function valid(): bool
    {
        return $this->current < $this->period->end;
    }

    /**
     * @param string|null $format
     * @return DateTimeInterface|string
     */
    public function getDateFrom(?string $format = null): DateTimeInterface|string
    {
        if (isset($format)) {
            return $this->period->start->format($format);
        }

        return $this->period->start;
    }

    /**
     * @param string|null $format
     * @return DateTimeInterface|string|null
     */
    public function getDateTo(?string $format = null): DateTimeInterface|string|null
    {
        if (isset($format)) {
            return $this->period->end->format($format);
        }

        return $this->period->end;
    }

    /**
     * @param string|null $format
     * @return DateTimeImmutable|string
     */
    public function getToday(?string $format = null): DateTimeImmutable|string
    {
        if (isset($format)) {
            return $this->today->format($format);
        }

        return $this->today;
    }

    public function getLengthOfStay(): int
    {
        return $this->period->start->diff($this->period->end)->d + 1;
    }

    //TODO: possible error. Need test
    public function getDaysLeftBeforeDateFrom(): int
    {
        if ($this->strategy === self::DAY_STRATEGY && ($this->period->start > $this->today)) {
            return $this->today->diff($this->period->start)->days + 1;
        }

        return $this->today->diff($this->period->start)->days;
    }

    protected function strategy(string $strategy): void
    {
        if (in_array($strategy, [self::DAY_STRATEGY, self::NIGHT_STRATEGY], true)) {
            $this->strategy = $strategy;
        } elseif (!isset($this->strategy)) {
            $this->strategy = self::NIGHT_STRATEGY;
        }
    }

    /**
     * @param DateTimeInterface $dateFrom
     * @param DateTimeInterface $dateTo
     * @throws InvalidArgumentException
     */
    protected function assertDateFromLessOrEqualThanDateTo(DateTimeInterface $dateFrom, DateTimeInterface $dateTo): void
    {
        if ($dateFrom > $dateTo) {
            throw new InvalidArgumentException(
                sprintf(
                    'Date to %1$s must be equal or more than Date from %2$s.',
                    $dateTo->format('Y-m-d'),
                    $dateFrom->format('Y-m-d')
                ),
                400,
                null,
                [
                    'dateFrom' => $dateFrom->format('Y-m-d'),
                    'dateTo'   => $dateTo->format('Y-m-d')
                ]
            );
        }
    }
}
