<?php

namespace AmeliaBooking\Domain\Services\User;

use AmeliaBooking\Domain\Collection\Collection;
use AmeliaBooking\Domain\Common\Exceptions\InvalidArgumentException;
use AmeliaBooking\Domain\Entity\Bookable\Service\Service;
use AmeliaBooking\Domain\Entity\Booking\Appointment\Appointment;
use AmeliaBooking\Domain\Entity\Location\Location;
use AmeliaBooking\Domain\Entity\Schedule\DayOff;
use AmeliaBooking\Domain\Entity\Schedule\Period;
use AmeliaBooking\Domain\Entity\Schedule\PeriodLocation;
use AmeliaBooking\Domain\Entity\Schedule\PeriodService;
use AmeliaBooking\Domain\Entity\Schedule\SpecialDay;
use AmeliaBooking\Domain\Entity\Schedule\SpecialDayPeriod;
use AmeliaBooking\Domain\Entity\Schedule\SpecialDayPeriodLocation;
use AmeliaBooking\Domain\Entity\Schedule\WeekDay;
use AmeliaBooking\Domain\Entity\User\Provider;
use AmeliaBooking\Domain\Factory\Bookable\Service\ServiceFactory;
use AmeliaBooking\Domain\Factory\Booking\Appointment\AppointmentFactory;
use AmeliaBooking\Domain\Factory\Schedule\PeriodFactory;
use AmeliaBooking\Domain\Factory\Schedule\PeriodLocationFactory;
use AmeliaBooking\Domain\Factory\Schedule\WeekDayFactory;
use AmeliaBooking\Domain\Services\DateTime\DateTimeService;
use AmeliaBooking\Domain\Services\Interval\IntervalService;
use AmeliaBooking\Domain\ValueObjects\DateTime\DateTimeValue;
use AmeliaBooking\Domain\ValueObjects\Duration;
use AmeliaBooking\Domain\ValueObjects\String\Status;
use DateInterval;
use DatePeriod;
use DateTime;
use DateTimeZone;
use Exception;
use Slim\Exception\ContainerValueNotFoundException;
use AmeliaBooking\Infrastructure\Licence;

/**
 * Class ProviderService
 *
 * @package AmeliaBooking\Domain\Services\User
 */
class ProviderService
{
    /** @var IntervalService */
    private $intervalService;

    /**
     * ProviderService constructor.
     *
     * @param IntervalService $intervalService
     */
    public function __construct(
        $intervalService
    ) {
        $this->intervalService = $intervalService;
    }

    /**
     * @param WeekDay|SpecialDay $day
     *
     * @return void
     * @throws InvalidArgumentException
     */
    private function makePeriodsAvailable($day, $removeBreaks)
    {
        $locations = [];

        if ($removeBreaks) {
            $day->setTimeOutList(new Collection());
        }

        /** @var Period $period */
        foreach ($day->getPeriodList()->getItems() as $period) {
            if ($period->getLocationId() && empty($locations[$period->getLocationId()->getValue()])) {
                $locations[$period->getLocationId()->getValue()] = PeriodLocationFactory::create(
                    ['locationId' => $period->getLocationId()->getValue()]
                );
            }

            /** @var PeriodLocation $periodLocation */
            foreach ($period->getPeriodLocationList()->getItems() as $periodLocation) {
                if (empty($locations[$periodLocation->getLocationId()->getValue()])) {
                    $locations[$periodLocation->getLocationId()->getValue()] = PeriodLocationFactory::create(
                        ['locationId' => $periodLocation->getLocationId()->getValue()]
                    );
                }
            }
        }

        $day->setPeriodList(new Collection());

        $day->getPeriodList()->addItem(
            PeriodFactory::create(
                [
                    'startTime'          => '00:00:00',
                    'endTime'            => '24:00:00',
                    'locationId'         => sizeof($locations) === 1 ? array_keys($locations)[0] : null,
                    'periodServiceList'  => [],
                    'periodLocationList' => sizeof($locations) > 1 ? array_values($locations) : [],
                ]
            )
        );
    }

    /**
     * @param Collection $providers
     *
     * @return void
     * @throws InvalidArgumentException
     */
    public function setProvidersAlwaysAvailable($providers)
    {
        /** @var Provider $provider */
        foreach ($providers->getItems() as $k => $provider) {
            $providerWeekDayIndexes = [];

            /** @var WeekDay $weekDay */
            foreach ($provider->getWeekDayList()->getItems() as $weekDay) {
                $providerWeekDayIndexes[] = $weekDay->getDayIndex()->getValue();
            }

            /** @var WeekDay $weekDay */
            foreach ($provider->getWeekDayList()->getItems() as $index => $weekDay) {
                $weekDay->getStartTime()->getValue()->setTime(0, 0);
                $weekDay->getEndTime()->getValue()->modify('+1 day');
                $weekDay->getEndTime()->getValue()->setTime(0, 0);

                $this->makePeriodsAvailable($weekDay, true);
            }

            for ($i = 1; $i <= 7; $i++) {
                if (!in_array($i, $providerWeekDayIndexes)) {
                    $provider->getWeekDayList()->addItem(
                        WeekDayFactory::create(
                            [
                                'dayIndex'    => $i,
                                'startTime'   => '00:00:00',
                                'endTime'     => '00:00:00',
                                'timeOutList' => new Collection(),
                                'periodList'  => new Collection(
                                    [
                                        'startTime'         => '00:00:00',
                                        'endTime'           => '24:00:00',
                                        'periodServiceList' => [],
                                    ]
                                )
                            ]
                        )
                    );
                }
            }

            /** @var SpecialDay $specialDay */
            foreach ($provider->getSpecialDayList()->getItems() as $specialDay) {
                $this->makePeriodsAvailable($specialDay, false);
            }

            $provider->setDayOffList(new Collection());

            /** @var Collection $sortedWeekDays */
            $sortedWeekDays = new Collection();

            $allWeekDaysIndexes = [];

            /** @var WeekDay $weekDay */
            foreach ($provider->getWeekDayList()->getItems() as $key => $weekDay) {
                $allWeekDaysIndexes[$weekDay->getDayIndex()->getValue()] = $key;
            }

            $allWeekDaysKeysKeys = array_keys($allWeekDaysIndexes);

            sort($allWeekDaysKeysKeys);

            foreach ($allWeekDaysKeysKeys as $weekDayIndex) {
                $sortedWeekDays->addItem($provider->getWeekDayList()->getItem($allWeekDaysIndexes[$weekDayIndex]));
            }

            $provider->setWeekDayList($sortedWeekDays);
        }
    }

    /**
     *
     * @param Period|SpecialDayPeriod $period
     * @param Location                $providerLocation
     * @param Collection              $locations
     * @param bool                    $hasVisibleLocations
     *
     * @return Collection
     *
     * @throws ContainerValueNotFoundException
     * @throws InvalidArgumentException
     */
    public function getProviderPeriodLocations($period, $providerLocation, $locations, $hasVisibleLocations)
    {
        /** @var Collection $availablePeriodLocations */
        $availablePeriodLocations = new Collection();

        if ($period->getPeriodLocationList()->length()) {
            /** @var PeriodLocation|SpecialDayPeriodLocation $periodLocation */
            foreach ($period->getPeriodLocationList()->getItems() as $periodLocation) {
                if (
                    $providerLocation &&
                    $periodLocation->getLocationId()->getValue() === $providerLocation->getId()->getValue() &&
                    ($hasVisibleLocations ? $providerLocation->getStatus()->getValue() === Status::VISIBLE : true)
                ) {
                    $availablePeriodLocations->addItem($providerLocation, $providerLocation->getId()->getValue());
                }
            }

            /** @var PeriodLocation|SpecialDayPeriodLocation $periodLocation */
            foreach ($period->getPeriodLocationList()->getItems() as $periodLocation) {
                /** @var Location $availableLocation */
                $availableLocation = $locations->keyExists($periodLocation->getLocationId()->getValue()) ?
                    $locations->getItem($periodLocation->getLocationId()->getValue()) : null;

                if (
                    $availableLocation &&
                    (
                        $providerLocation ?
                        $periodLocation->getLocationId()->getValue() !== $providerLocation->getId()->getValue() : true
                    ) &&
                    ($hasVisibleLocations ? $availableLocation->getStatus()->getValue() === Status::VISIBLE : true)
                ) {
                    $availablePeriodLocations->addItem($availableLocation, $availableLocation->getId()->getValue());
                }
            }
        } elseif ($period->getLocationId() && $period->getLocationId()->getValue()) {
            /** @var Location $availableLocation */
            $availableLocation = $locations->keyExists($period->getLocationId()->getValue()) ?
                $locations->getItem($period->getLocationId()->getValue()) : null;

            if (
                $availableLocation &&
                ($hasVisibleLocations ? $availableLocation->getStatus()->getValue() === Status::VISIBLE : true)
            ) {
                $availablePeriodLocations->addItem($availableLocation, $availableLocation->getId()->getValue());
            }
        } elseif (
            $providerLocation &&
            ($hasVisibleLocations ? $providerLocation->getStatus()->getValue() === Status::VISIBLE : true)
        ) {
            $availablePeriodLocations->addItem($providerLocation, $providerLocation->getId()->getValue());
        }

        return $availablePeriodLocations;
    }

    /**
     * @param Provider   $provider
     * @param Collection $services
     * @param bool       $allowHiddenServices
     *
     * @return void
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function setProviderServices($provider, $services, $allowHiddenServices)
    {
        /** @var Collection $providerServiceList */
        $providerServiceList = new Collection();

        /** @var Service $providerService */
        foreach ($provider->getServiceList()->getItems() as $providerService) {
            if ($services->keyExists($providerService->getId()->getValue())) {
                /** @var Service $service */
                $service = $services->getItem($providerService->getId()->getValue());

                if ($allowHiddenServices ? true : $service->getStatus()->getValue() === Status::VISIBLE) {
                    $providerServiceList->addItem(
                        ServiceFactory::create(
                            array_merge(
                                $service->toArray(),
                                [
                                    'price'         => $providerService->getPrice()->getValue(),
                                    'minCapacity'   => $providerService->getMinCapacity()->getValue(),
                                    'maxCapacity'   => $providerService->getMaxCapacity()->getValue(),
                                    'customPricing' => $providerService->getCustomPricing() ?
                                        $providerService->getCustomPricing()->getValue() : null,
                                ]
                            )
                        ),
                        $service->getId()->getValue()
                    );
                }
            }
        }

        $provider->setServiceList($providerServiceList);
    }

    /**
     * Add appointments to provider's appointments list
     *
     * @param Collection $providers
     * @param Collection $appointments
     * @param bool       $isGloballyBusySlot
     *
     * @throws InvalidArgumentException
     */
    public function addAppointmentsToAppointmentList($providers, $appointments, $isGloballyBusySlot)
    {
        /** @var Appointment $appointment */
        foreach ($appointments->getItems() as $appointment) {
            if ($isGloballyBusySlot) {
                /** @var Provider $provider */
                foreach ($providers->getItems() as $provider) {
                    /** @var Appointment $fakeAppointment */
                    $fakeAppointment = AppointmentFactory::create(
                        array_merge(
                            $appointment->toArray(),
                            ['providerId' => $provider->getId()->getValue()]
                        )
                    );

                    if (!$fakeAppointment->getService()->getTimeBefore()) {
                        $fakeAppointment->getService()->setTimeBefore(new Duration(0));
                    }

                    if (!$fakeAppointment->getService()->getTimeAfter()) {
                        $fakeAppointment->getService()->setTimeAfter(new Duration(0));
                    }

                    $provider->getAppointmentList()->addItem($fakeAppointment);
                }
            } elseif ($providers->keyExists($appointment->getProviderId()->getValue())) {
                /** @var Provider $provider */
                $provider = $providers->getItem($appointment->getProviderId()->getValue());

                $provider->getAppointmentList()->addItem($appointment);
            }
        }
    }

    /**
     * @param Provider $provider
     * @param array    $globalDaysOff
     * @param DateTime $startDateTime
     * @param DateTime $endDateTime
     *
     * @return void
     * @throws InvalidArgumentException
     */
    public function modifyProviderTimeZone($provider, $globalDaysOff, $startDateTime, $endDateTime)
    {
        /** @var Appointment $appointment */
        foreach ($provider->getAppointmentList()->getItems() as $appointment) {
            $appointment->getBookingStart()->getValue()->setTimezone(
                new DateTimeZone($provider->getTimeZone()->getValue())
            );

            $appointment->getBookingEnd()->getValue()->setTimezone(
                new DateTimeZone($provider->getTimeZone()->getValue())
            );
        }

        /** @var SpecialDay $specialDay */
        foreach ($provider->getSpecialDayList()->getItems() as $specialDay) {
            $specialDay->setStartDate(
                new DateTimeValue(
                    DateTimeService::getDateTimeObjectInTimeZone(
                        $specialDay->getStartDate()->getValue()->format('Y-m-d') . ' 00:00:00',
                        $provider->getTimeZone()->getValue()
                    )
                )
            );

            $specialDay->setEndDate(
                new DateTimeValue(
                    DateTimeService::getDateTimeObjectInTimeZone(
                        $specialDay->getEndDate()->getValue()->format('Y-m-d') . ' 00:00:00',
                        $provider->getTimeZone()->getValue()
                    )
                )
            );
        }

        /** @var DayOff $dayOff */
        foreach ($provider->getDayOffList()->getItems() as $dayOff) {
            $dayOff->setStartDate(
                new DateTimeValue(
                    DateTimeService::getDateTimeObjectInTimeZone(
                        $dayOff->getStartDate()->getValue()->format('Y-m-d') . ' 00:00:00',
                        $provider->getTimeZone()->getValue()
                    )
                )
            );

            $dayOff->setEndDate(
                new DateTimeValue(
                    DateTimeService::getDateTimeObjectInTimeZone(
                        $dayOff->getEndDate()->getValue()->format('Y-m-d') . ' 00:00:00',
                        $provider->getTimeZone()->getValue()
                    )
                )
            );
        }

        $yearsDiff = $startDateTime->diff($endDateTime)->format('%y');

        $yearsDiff = $yearsDiff === '0' ? '1' : $yearsDiff;

        $startYear = $startDateTime->format('Y');

        /** @var Collection $fakeAppointments */
        $fakeAppointments = new Collection();

        foreach ($globalDaysOff as $globalDayOff) {
            $dayOffParts = explode('-', $globalDayOff);

            if (sizeof($dayOffParts) === 2) {
                for ($i = 0; $i <= $yearsDiff; $i++) {
                    $dateOffStart = DateTimeService::getCustomDateTimeObject(
                        $startYear . '-' . $globalDayOff . ' 00:00'
                    )->modify("+$i years");

                    $dateOffEnd = DateTimeService::getCustomDateTimeObject(
                        $startYear . '-' . $globalDayOff . ' 00:00'
                    )->modify("+$i years")->modify('+1 days');

                    /** @var Appointment $fakeAppointment */
                    $fakeAppointment = AppointmentFactory::create(
                        [
                            'bookingStart'       => $dateOffStart->format('Y-m-d H:i'),
                            'bookingEnd'         => $dateOffEnd->format('Y-m-d H:i'),
                            'notifyParticipants' => false,
                            'serviceId'          => 0,
                            'providerId'         => $provider->getId()->getValue(),
                        ]
                    );

                    $fakeAppointment->getBookingStart()->getValue()->setTimezone(
                        new DateTimeZone($provider->getTimeZone()->getValue())
                    );

                    $fakeAppointment->getBookingEnd()->getValue()->setTimezone(
                        new DateTimeZone($provider->getTimeZone()->getValue())
                    );

                    $fakeAppointments->addItem($fakeAppointment);
                }
            } elseif (sizeof($dayOffParts) === 3) {
                /** @var Appointment $fakeAppointment */
                $fakeAppointment = AppointmentFactory::create(
                    [
                        'bookingStart'       => $globalDayOff . ' 00:00',
                        'bookingEnd'         => DateTimeService::getCustomDateTimeObject(
                            $globalDayOff . ' 00:00'
                        )->modify('+1 days')->format('Y-m-d H:i'),
                        'notifyParticipants' => false,
                        'serviceId'          => 0,
                        'providerId'         => $provider->getId()->getValue(),
                    ]
                );

                $fakeAppointment->getBookingStart()->getValue()->setTimezone(
                    new DateTimeZone($provider->getTimeZone()->getValue())
                );

                $fakeAppointment->getBookingEnd()->getValue()->setTimezone(
                    new DateTimeZone($provider->getTimeZone()->getValue())
                );

                $fakeAppointments->addItem($fakeAppointment);
            }
        }

        /** @var Appointment $fakeAppointment */
        foreach ($fakeAppointments->getItems() as $fakeAppointment) {
            $provider->getAppointmentList()->addItem($fakeAppointment);
        }
    }

    /**
     * @param Service $service
     * @param string  $timeZone
     *
     * @return array
     */
    public function getCustomPricing($service, $timeZone)
    {
        if ($service->getCustomPricing()) {
            $customPricing = json_decode($service->getCustomPricing()->getValue(), true);

            if (
                $customPricing &&
                $customPricing['enabled'] === 'period' &&
                Licence\Licence::getLicence() !== 'Lite' &&
                Licence\Licence::getLicence() !== 'Starter'
            ) {
                foreach ($customPricing['periods']['default'] as &$item) {
                    $ranges = [];

                    foreach ($item['ranges'] as $range) {
                        $ranges[] = [
                            'from'  => $this->intervalService->getSeconds($range['from'] . ':00'),
                            'to'    => $this->intervalService->getSeconds($range['to'] . ':00'),
                            'price' => $range['price'],
                        ];
                    }

                    $item['ranges'] = $ranges;
                }

                foreach ($customPricing['periods']['custom'] as &$item) {
                    $days = new DatePeriod(
                        DateTimeService::getCustomDateTimeObjectInTimeZone(
                            $item['dates']['start'] . ' 00:00:00',
                            $timeZone
                        ),
                        new DateInterval('P1D'),
                        DateTimeService::getCustomDateTimeObjectInTimeZone(
                            $item['dates']['end'] . ' 23:59:59',
                            $timeZone
                        )
                    );

                    $dates = [];

                    /** @var DateTime $day */
                    foreach ($days as $day) {
                        $dates[$day->format('Y-m-d')] = true;
                    }

                    $ranges = [];

                    foreach ($item['ranges'] as $range) {
                        $ranges[] = [
                            'from'  => $this->intervalService->getSeconds($range['from'] . ':00'),
                            'to'    => $this->intervalService->getSeconds($range['to'] . ':00'),
                            'price' => $range['price'],
                        ];
                    }

                    $item['dates'] = $dates;

                    $item['ranges'] = $ranges;
                }

                return $customPricing;
            }
        }

        return [];
    }

    /**
     * @param array  $customPricing
     * @param string $dateString
     * @param string $timeInSeconds
     * @param string $timeZone
     *
     * @return float|null
     */
    public function getDateTimePrice($customPricing, $dateString, $timeInSeconds, $timeZone)
    {
        foreach ($customPricing['periods']['custom'] as $item) {
            if (array_key_exists($dateString, $item['dates'])) {
                foreach ($item['ranges'] as $range) {
                    if ($timeInSeconds >= $range['from'] && $timeInSeconds < $range['to']) {
                        return $range['price'];
                    }
                }
            }
        }

        $appStart = DateTimeService::getCustomDateTimeObjectInTimeZone($dateString, $timeZone);

        $dayIndex = (int)$appStart->format('N') - 1;

        foreach ($customPricing['periods']['default'] as $item) {
            if (in_array($dayIndex, $item['days'])) {
                foreach ($item['ranges'] as $range) {
                    if ($timeInSeconds >= $range['from'] && $timeInSeconds < $range['to']) {
                        return $range['price'];
                    }
                }
            }
        }

        return null;
    }

    /**
     * @param Collection $providers
     * @param array      $criteria
     *
     * @return void
     * @throws InvalidArgumentException
     */
    public function filterProvidersAndScheduleByCriteria($providers, $criteria)
    {
        if (!empty($criteria['providers'])) {
            foreach ($providers->getItems() as $providerId => $provider) {
                if (!in_array($provider->getId()->getValue(), $criteria['providers'])) {
                    $providers->deleteItem($providerId);
                }
            }
        }

        if (!empty($criteria['services']) || !empty($criteria['locations'])) {
            /** @var Provider $provider */
            foreach ($providers->getItems() as $provider) {
                $this->filterScheduleByCriteria($provider->getWeekDayList(), $criteria);

                $this->filterScheduleByCriteria($provider->getSpecialDayList(), $criteria);
            }
        }
    }

    /**
     * @param Collection $dayList
     * @param array      $criteria
     *
     * @return void
     * @throws InvalidArgumentException
     */
    private function filterScheduleByCriteria($dayList, $criteria)
    {
        /** @var WeekDay|SpecialDay $day */
        foreach ($dayList->getItems() as $weekDayIndex => $day) {
            $hasPeriods = $day->getPeriodList()->length() > 0;

            /** @var Period $period */
            foreach ($day->getPeriodList()->getItems() as $periodIndex => $period) {
                if ($period->getLocationId() && $period->getPeriodLocationList()->length() > 0) {
                    $period->setLocationId(null);
                }

                $hasPeriodServices = $period->getPeriodServiceList()->length() > 0;

                $hasPeriodLocations = $period->getPeriodLocationList()->length() > 0;

                /** @var PeriodService $periodService */
                foreach ($period->getPeriodServiceList()->getItems() as $periodServiceIndex => $periodService) {
                    if (
                        !empty($criteria['services']) &&
                        !in_array($periodService->getServiceId()->getValue(), $criteria['services'])
                    ) {
                        $period->getPeriodServiceList()->deleteItem($periodServiceIndex);
                    }
                }

                /** @var PeriodLocation $periodLocation */
                foreach ($period->getPeriodLocationList()->getItems() as $periodLocationIndex => $periodLocation) {
                    if (
                        !empty($criteria['locations']) &&
                        !in_array($periodLocation->getLocationId()->getValue(), $criteria['locations'])
                    ) {
                        $period->getPeriodLocationList()->deleteItem($periodLocationIndex);
                    }
                }

                if (
                    ($hasPeriodServices && $period->getPeriodServiceList()->length() === 0) ||
                    ($hasPeriodLocations && $period->getPeriodLocationList()->length() === 0) ||
                    (
                        !empty($criteria['locations']) &&
                        $period->getLocationId() &&
                        !in_array($period->getLocationId()->getValue(), $criteria['locations'])
                    )
                ) {
                    $day->getPeriodList()->deleteItem($periodIndex);
                }
            }

            if ($hasPeriods && $day->getPeriodList()->length() === 0) {
                $dayList->deleteItem($weekDayIndex);
            }
        }
    }
}
