<?php

namespace App\Services;

use App\Exceptions\ScheduleStatusTransitionException;
use App\Enums\ServicePackageStatusEnum;
use App\Enums\UserTypeEnum;
use App\Enums\NotificationTypeEnum;
use App\Jobs\StartScheduleJob;
use App\Models\Provider;
use App\Models\Schedule;
use App\Models\ServicePackage;
use App\Rules\ScheduleBusinessRules;
use App\Services\NotificationService;
use App\Services\PushNotificationService;
use App\Notifications\Push\Prestador\Agendamento\NewPushRequest;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class ScheduleService
{
    private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];

    public function getAll()
    {
        return Schedule::with(['client.user', 'provider.user', 'address'])
            ->where('schedule_type', 'default')
            ->orderBy('date', 'desc')
            ->orderBy('start_time', 'desc')
            ->get();
    }

    public function getById($id)
    {
        return Schedule::with(['client.user', 'provider.user', 'address'])->findOrFail($id);
    }

    public function create(array $data): Schedule
    {
        return data_get($this->createSingleOrMultiple([], [$data]), 0);
    }

    public function createSingleOrMultiple(array $baseData, array $schedules)
    {
        try {
            DB::beginTransaction();

            $createdSchedules = [];

            foreach ($schedules as $schedule) {
                $datasMerged = array_merge($baseData, $schedule);

                if (data_get($datasMerged, 'schedule_type', 'default') === 'default') {
                    $provider = Provider::findOrFail(data_get($datasMerged, 'provider_id'));

                    $datasMerged['total_amount'] = $this->calculateAmount(
                        $provider,
                        (string) data_get($datasMerged, 'period_type'),
                    );
                }

                $this->validateProviderAvailability($datasMerged, null);

                $scheduleData = array_merge($datasMerged, [
                    'code' => str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT),
                ]);

                $newSchedule = Schedule::create($scheduleData);

                // NOTIFICAÇÃO PRESTADOR

                if ($newSchedule->provider_id) {
                    $notificationService = app(NotificationService::class);

                    $notificationService->create([
                        'title'       => __('notifications.new_schedule_request_title'),
                        'description' => __('notifications.new_schedule_request_description'),
                        'origin'      => 'schedule',
                        'origin_id'   => $newSchedule->id,
                        'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_NEW_SOLICITATION->value,
                        'user_id'     => $newSchedule->provider->user_id,
                    ]);
                    // Push Notification
                    $pushNotificationService = app(PushNotificationService::class);

                    $pushNotificationService->sendToUser(
                        $newSchedule->provider->user,
                        new NewPushRequest($newSchedule->client->user->name)
                    );
                }


                $createdSchedules[] = $newSchedule;
            }

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();

            throw $e;
        }

        return $createdSchedules;
    }

    public function update($id, array $data)
    {
        unset($data['status']);

        $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id);

        if (data_get($data, 'provider_id') !== null || data_get($data, 'period_type') !== null) {
            $providerId = data_get($data, 'provider_id', $schedule->provider_id);
            $periodType = data_get($data, 'period_type', $schedule->period_type);

            $provider = Provider::findOrFail($providerId);

            $data['total_amount'] = $this->calculateAmount($provider, $periodType);
        }

        if (data_get($data, 'date') !== null || data_get($data, 'start_time') !== null || data_get($data, 'provider_id') !== null) {
            $validationData = array_merge($schedule->toArray(), $data);

            $this->validateProviderAvailability($validationData, $id);
        }

        $schedule->update($data);

        return $schedule->fresh(['client.user', 'provider.user', 'address']);
    }

    public function delete($id)
    {
        $schedule = Schedule::findOrFail($id);

        $schedule->delete();

        return $schedule;
    }

    //

    public function updateStatus($id, string $status, bool $fromPackage = false)
    {
        try {
            DB::beginTransaction();

            $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id);

            if (! $fromPackage && in_array($status, ['accepted', 'rejected']) && Auth::user()->type === UserTypeEnum::PROVIDER) {
                $belongsToServicePackage = DB::table('service_package_items')
                    ->where('schedule_id', $schedule->id)
                    ->exists();

                if ($belongsToServicePackage) {
                    throw new \DomainException(__('messages.schedule_belongs_to_package_use_package_endpoint'));
                }
            }

            $allowedTransitions = [
                'pending'   => ['accepted', 'rejected', 'cancelled'],
                'accepted'  => ['paid', 'cancelled'],
                'paid'      => ['cancelled', 'started'],
                'started'   => ['finished'],
                'rejected'  => [],
                'cancelled' => [],
                'finished'  => [],
            ];

            $currentStatus = $schedule->status;

            if (data_get($allowedTransitions, $currentStatus) === null) {
                throw new ScheduleStatusTransitionException;
            }

            if (! in_array($status, data_get($allowedTransitions, $currentStatus))) {
                throw new ScheduleStatusTransitionException;
            }

            $schedule->update(['status' => $status]);
            $schedule->refresh();

            $currentStatus = $schedule->status;

            switch ($status) {
                case 'pending':
                    break;

                case 'accepted':
                    $notificationService = app(NotificationService::class);

                    switch (Auth::user()->type) {
                        case UserTypeEnum::PROVIDER:
                            $notificationService->create([
                                'title'       => __('notifications.schedule_accepted_title'),
                                'description' => __('notifications.provider_accepted_schedule_description', ['provider' => $schedule->provider->user->name]),
                                'origin'      => 'schedule',
                                'origin_id'   => $schedule->id,
                                'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_ACCEPTED->value,
                                'user_id'     => $schedule->client->user_id,
                            ]);

                            break;

                        case UserTypeEnum::CLIENT:
                            if ($schedule->provider_id) {
                                $notificationService->create([
                                    'title'       => __('notifications.proposal_accepted_title'),
                                    'description' => __('notifications.proposal_accepted_description'),
                                    'origin'      => 'schedule',
                                    'origin_id'   => $schedule->id,
                                    'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_PROPOSAL_ACCEPTED->value,
                                    'user_id'     => $schedule->provider->user_id,
                                ]);
                            }

                            break;

                        default:
                            break;
                    }

                    break;

                case 'cancelled':
                    $notificationService = app(NotificationService::class);

                    switch (Auth::user()->type) {
                        case UserTypeEnum::CLIENT:
                            $notificationService->create([
                                'title'       => __('notifications.schedule_cancelled_title'),
                                'description' => __('notifications.client_cancelled_schedule_description'),
                                'origin'      => 'schedule',
                                'origin_id'   => $schedule->id,
                                'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value,
                                'user_id'     => $schedule->provider->user_id,
                            ]);

                            break;

                        case UserTypeEnum::PROVIDER:
                            $notificationService->create([
                                'title'       => __('notifications.schedule_cancelled_title'),
                                'description' => __('notifications.provider_cancelled_schedule_description', ['provider' => $schedule->provider->user->name]),
                                'origin'      => 'schedule',
                                'origin_id'   => $schedule->id,
                                'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_CANCELLED->value,
                                'user_id'     => $schedule->client->user_id,
                            ]);

                            break;

                        default:
                            break;
                    }

                    break;

                case 'started':
                    $notificationService = app(NotificationService::class);

                    // CLIENTE

                    $notificationService->create([
                        'title'       => __('notifications.provider_on_the_way_title'),
                        'description' => __('notifications.provider_on_the_way_description', ['code' => $schedule->code]),
                        'origin'      => 'schedule',
                        'origin_id'   => $schedule->id,
                        'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_COMING->value,
                        'user_id'     => $schedule->client->user_id,
                    ]);

                    // PRESTADOR

                    $notificationService->create([
                        'title'       => __('notifications.service_start_title'),
                        'description' => __('notifications.service_start_description'),
                        'origin'      => 'schedule',
                        'origin_id'   => $schedule->id,
                        'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
                        'user_id'     => $schedule->provider->user_id,
                    ]);

                    break;

                case 'finished':
                    $notificationService = app(NotificationService::class);

                    // CLIENTE

                    $notificationService->create([
                        'title'       => __('notifications.service_finished_title'),
                        'description' => __('notifications.service_finished_description'),
                        'origin'      => 'schedule',
                        'origin_id'   => $schedule->id,
                        'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_FINISHED->value,
                        'user_id'     => $schedule->client->user_id,
                    ]);

                    break;

                case 'paid':
                    $notificationService = app(NotificationService::class);

                    switch (Auth::user()->type) {
                        case UserTypeEnum::CLIENT:
                            if ($schedule->provider_id) {
                                $notificationService->create([
                                    'title'       => __('notifications.payment_confirmed_title'),
                                    'description' => __('notifications.payment_confirmed_description'),
                                    'origin'      => 'schedule',
                                    'origin_id'   => $schedule->id,
                                    'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
                                    'user_id'     => $schedule->provider->user_id,
                                ]);
                            }

                            break;

                        default:
                            break;
                    }

                    $date_cleaned = Carbon::parse($schedule->date)
                        ->format('Y-m-d');

                    $date_time_dispatch = Carbon::parse(
                        $date_cleaned . ' ' . $schedule->start_time
                    )->subHour();

                    StartScheduleJob::dispatch($schedule->id)
                        ->delay($date_time_dispatch);

                    break;

                case 'rejected':
                    $notificationService = app(NotificationService::class);

                    $notificationService->create([
                        'title'       => __('notifications.schedule_refused_title'),
                        'description' => __('notifications.schedule_refused_description'),
                        'origin'      => 'schedule',
                        'origin_id'   => $schedule->id,
                        'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_REFUSED->value,
                        'user_id'     => $schedule->client->user_id,
                    ]);

                    break;
            }

            DB::commit();

            return $schedule->fresh(['client.user', 'provider.user', 'address']);
        } catch (ScheduleStatusTransitionException $e) {
            DB::rollBack();

            throw $e;
        } catch (\Exception $e) {
            DB::rollBack();

            Log::error('Erro ao atualizar status do agendamento: ' . $e->getMessage());

            throw $e;
        }
    }

    //

    public function getClientProviderBlocks(int $clientId, int $providerId): array
    {
        $today = Carbon::today()->format('Y-m-d');

        $schedules = Schedule::where('client_id', $clientId)
            ->where('provider_id', $providerId)
            ->whereNotIn('status', self::EXCLUDED_STATUSES)
            ->whereDate('date', '>=', $today)
            ->orderBy('date')
            ->orderBy('start_time')
            ->get(['id', 'date', 'start_time', 'end_time', 'status']);

        $existingSchedules = $schedules->map(function ($schedule) {
            return [
                'id'         => $schedule->id,
                'date'       => Carbon::parse($schedule->date)->format('Y-m-d'),
                'start_time' => $schedule->start_time,
                'end_time'   => $schedule->end_time,
                'status'     => $schedule->status,
            ];
        })->values();

        $fullyBlockedWeeks = $schedules
            ->groupBy(function ($schedule) {
                return Carbon::parse($schedule->date)
                    ->startOfWeek(Carbon::SUNDAY)
                    ->format('Y-m-d');
            })
            ->filter(function ($weekSchedules) {
                return $weekSchedules->count() >= 2;
            })
            ->keys()
            ->values();

        return [
            'existing_schedules'  => $existingSchedules,
            'fully_blocked_weeks' => $fullyBlockedWeeks,
        ];
    }

    public function getFinished()
    {
        return Schedule::with(['client.user', 'provider.user'])
            ->where('status', 'finished')
            ->orderBy('date', 'desc')
            ->orderBy('start_time', 'desc')
            ->get();
    }

    public function getSchedulesDefaultGroupedByClient()
    {
        $schedules = Schedule::with(['client.user', 'provider.user', 'address', 'reviews.reviewsImprovements.improvementType'])
            ->orderBy('id', 'desc')
            ->where('schedule_type', 'default')
            ->select(
                'schedules.*'
            )
            ->get();

        $grouped = $schedules->groupBy('client_id')->map(function ($clientSchedules) {
            $firstSchedule = $clientSchedules->first();

            return [
                'client_id'   => $firstSchedule->client_id,
                'client_name' => $firstSchedule->client->user->name ?? 'N/A',

                'schedules' => $clientSchedules->map(function ($schedule) {
                    return [
                        'id'            => $schedule->id,
                        'date'          => $schedule->date ? Carbon::parse($schedule->date)->format('d/m/Y') : null,
                        'start_time'    => $schedule->start_time,
                        'end_time'      => $schedule->end_time,
                        'period_type'   => $schedule->period_type,
                        'status'        => $schedule->status,
                        'total_amount'  => $schedule->total_amount,
                        'code'          => $schedule->code,
                        'code_verified' => $schedule->code_verified,
                        'client_id'     => $schedule->client_id,
                        'provider_id'   => $schedule->provider_id,
                        'provider_name' => $schedule->provider->user->name ?? 'N/A',

                        'address' => $schedule->address ? [
                            'id'         => $schedule->address->id,
                            'address'    => $schedule->address->address,
                            'complement' => $schedule->address->complement,
                            'zip_code'   => $schedule->address->zip_code,
                            'city'       => $schedule->address->city->name        ?? '',
                            'state'      => $schedule->address->city->state->name ?? '',
                        ] : null,

                        'client_name' => $schedule->client->user->name ?? 'N/A',

                        'reviews' => $schedule->reviews->map(function ($review) {
                            return [
                                'id'           => $review->id,
                                'stars'        => $review->stars,
                                'comment'      => $review->comment,
                                'origin'       => $review->origin,
                                'origin_id'    => $review->origin_id,
                                'created_at'   => Carbon::parse($review->created_at)->format('Y-m-d H:i'),
                                'updated_at'   => Carbon::parse($review->updated_at)->format('Y-m-d H:i'),

                                'improvements' => $review->reviewsImprovements->map(function ($ri) {
                                    return [
                                        'id'                    => $ri->id,
                                        'improvement_type_id'   => $ri->improvement_type_id,
                                        'improvement_type_name' => $ri->improvementType ? $ri->improvementType->description : null,
                                    ];
                                })->values(),
                            ];
                        }),
                    ];
                })->values(),
            ];
        })->sortBy('id')->values();

        return $grouped;
    }

    //

    public function cancelWithReason(int $id, string $cancelText)
    {
        try {
            DB::beginTransaction();

            $schedule = Schedule::findOrFail($id);

            $allowedStatuses = ['accepted', 'paid', 'pending'];

            if (! in_array($schedule->status, $allowedStatuses)) {
                throw new ScheduleStatusTransitionException;
            }

            $cancelled_by = Auth::user()->type;

            $schedule->update([
                'status'       => 'cancelled',
                'cancel_text'  => $cancelText,
                'cancelled_by' => $cancelled_by,
            ]);

            $this->cascadeCancelServicePackages($schedule, $cancelText, $cancelled_by);

            DB::commit();

            return $schedule->fresh(['client.user', 'provider.user', 'address']);
        } catch (ScheduleStatusTransitionException $e) {
            DB::rollBack();

            throw $e;
        } catch (\Exception $e) {
            DB::rollBack();

            Log::error('Erro ao cancelar agendamento: ' . $e->getMessage());

            throw $e;
        }
    }

    private function cascadeCancelServicePackages(Schedule $schedule, string $cancelText, $cancelledBy): void
    {
        $packageIds = DB::table('service_package_items')
            ->where('schedule_id', $schedule->id)
            ->pluck('service_package_id');

        if ($packageIds->isEmpty()) {
            return;
        }

        $packages = ServicePackage::query()
            ->with('items.schedule')
            ->whereIn('id', $packageIds)
            ->get();

        foreach ($packages as $package) {
            $siblingSchedules = $package->items->pluck('schedule')->filter();

            $siblingSchedules
                ->filter(fn (Schedule $sibling) => $sibling->id !== $schedule->id
                    && in_array($sibling->status, ['pending', 'accepted', 'paid'], true))
                ->each(fn (Schedule $sibling) => $sibling->update([
                    'status'       => 'cancelled',
                    'cancel_text'  => $cancelText,
                    'cancelled_by' => $cancelledBy,
                ]));

            $hasRealizedSchedule = $siblingSchedules->contains(
                fn (Schedule $sibling) => in_array($sibling->status, ['started', 'finished'], true),
            );

            if ($package->status === ServicePackageStatusEnum::OPEN
                || ($package->status === ServicePackageStatusEnum::PAID && ! $hasRealizedSchedule)) {
                $package->update(['status' => ServicePackageStatusEnum::CANCELLED->value]);
            }
        }
    }

    //

    private function calculateAmount(Provider $provider, string $periodType): float
    {
        $hourlyRates = [
            '2' => $provider->daily_price_2h ?? 0,
            '4' => $provider->daily_price_4h ?? 0,
            '6' => $provider->daily_price_6h ?? 0,
            '8' => $provider->daily_price_8h ?? 0,
        ];

        return data_get($hourlyRates, $periodType, 0);
    }

    private function validateProviderAvailability(array $data, $excludeScheduleId = null)
    {
        $provider_id = data_get($data, 'provider_id');
        $client_id   = data_get($data, 'client_id');

        $date = Carbon::parse(data_get($data, 'date'));

        $dayOfWeek = $date->dayOfWeek;
        $startTime = data_get($data, 'start_time');
        $endTime   = data_get($data, 'end_time');
        $date_ymd  = $date->format('Y-m-d');
        $period    = $startTime < '13:00:00' ? 'morning' : 'afternoon';

        ScheduleBusinessRules::validateProviderVisibleToCustomers($provider_id);

        // bloqueio 2 schedules por semana para o mesmo client e provider

        ScheduleBusinessRules::validateWeeklyScheduleLimit(
            $client_id,
            $provider_id,
            data_get($data, 'date'),
            $excludeScheduleId
        );

        // bloqueio provider trabalha no dia/periodo

        ScheduleBusinessRules::validateWorkingDay(
            $provider_id,
            $dayOfWeek,
            $period
        );

        // bloqueio provider tem blockedday para dia/hora

        ScheduleBusinessRules::validateBlockedDay(
            $provider_id,
            $date->format('Y-m-d'),
            $startTime,
            $endTime
        );

        // bloqueio provider tem outro agendamento para dia/hora

        ScheduleBusinessRules::validateConflictingSchedule(
            $provider_id,
            $date->format('Y-m-d'),
            $startTime,
            $endTime,
            $excludeScheduleId
        );

        // bloqueio provider tem outra proposta na mesma data

        ScheduleBusinessRules::validateConflictingProposalSameDate(
            $provider_id,
            $date_ymd,
            $startTime,
            $endTime,
            null
        );

        // bloqueio caso o client tenha bloqueado o provider

        ScheduleBusinessRules::validateClientNotBlockedByProvider(
            $client_id,
            $provider_id
        );

        // bloqueio caso o provider tenha bloqueado o client

        ScheduleBusinessRules::validateProviderNotBlockedByClient(
            $client_id,
            $provider_id
        );

        return true;
    }
}
