<?php

namespace App\Rules;

use App\Models\ClientProviderBlock;
use App\Models\Provider;
use App\Models\ProviderBlockedDay;
use App\Models\ProviderClientBlock;
use App\Models\ProviderWorkingDay;
use App\Models\Schedule;
use App\Models\ScheduleProposal;
use Carbon\Carbon;
use Illuminate\Support\Collection;

class ScheduleBusinessRules
{
    // Status que devem ser ignorados na validação de limite por semana

    private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];

    public static function validateProviderVisibleToCustomers($providerId): bool
    {
        $provider = Provider::query()
            ->visibleToCustomers()
            ->where('providers.id', $providerId)
            ->exists();

        if (! $provider) {
            throw new \Exception(__('messages.provider_unavailable_for_schedule'));
        }

        return true;
    }

    /**
     * Valida se o prestador pode ter mais um agendamento com o cliente na semana
     * Limite: 2 agendamentos por semana (domingo a sábado)
     *
     * @param  int      $clientId
     * @param  int      $providerId
     * @param  string   $date  (Y-m-d)
     * @param  int|null $excludeScheduleId
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateWeeklyScheduleLimit($clientId, $providerId, $date, $excludeScheduleId = null)
    {
        $date = Carbon::parse($date);

        $weekStart = $date->copy()->startOfWeek(Carbon::SUNDAY);
        $weekEnd   = $date->copy()->endOfWeek(Carbon::SATURDAY);

        $weeklySchedulesCount = Schedule::where('client_id', $clientId)
            ->where('provider_id', $providerId)
            ->whereNotIn('status', self::EXCLUDED_STATUSES)
            ->whereBetween('date', [$weekStart->format('Y-m-d'), $weekEnd->format('Y-m-d')])
            ->when($excludeScheduleId, function ($query) use ($excludeScheduleId) {
                $query->where('id', '!=', $excludeScheduleId);
            })
            ->count();

        if ($weeklySchedulesCount >= 2) {
            throw new \Exception(__('validation.custom.schedule.weekly_limit_exceeded'));
        }

        return true;
    }

    /**
     * Valida se o prestador tem horário de trabalho cadastrado para o dia da semana e período
     *
     * @param  int    $provider_id
     * @param  int    $day_of_week (0 - domingo, 6 - sábado)
     * @param  string $period      ('morning' ou 'afternoon')
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateWorkingDay($provider_id, $day_of_week, $period)
    {
        $workingDay = ProviderWorkingDay::where('provider_id', $provider_id)
            ->where('day', $day_of_week)
            ->where('period', $period)
            ->first();

        if (! $workingDay) {
            throw new \Exception(__('validation.custom.schedule.provider_not_working'));
        }

        return true;
    }

    /**
     * Valida se o prestador tem bloqueio cadastrado para o dia e horário
     *
     * @param  int    $provider_id
     * @param  string $date_ymd   (Y-m-d)
     * @param  string $start_time (H:i:s)
     * @param  string $end_time   (H:i:s)
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateBlockedDay($provider_id, $date_ymd, $start_time, $end_time)
    {
        $blockedDay = ProviderBlockedDay::where('provider_id', $provider_id)
            ->where('date', $date_ymd)
            ->where(function ($query) use ($start_time, $end_time) {
                $query->where('period', 'full')
                    ->orWhere(function ($q) use ($start_time, $end_time) {
                        $q->where('period', 'partial')
                            ->where(function ($q2) use ($start_time, $end_time) {
                                $q2->whereBetween('init_hour', [$start_time, $end_time])
                                    ->orWhereBetween('end_hour', [$start_time, $end_time])
                                    ->orWhere(function ($q3) use ($start_time, $end_time) {
                                        $q3->where('init_hour', '<=', $start_time)
                                            ->where('end_hour', '>=', $end_time);
                                    });
                            });
                    });
            })
            ->first();

        if ($blockedDay) {
            throw new \Exception(__('validation.custom.schedule.provider_blocked'));
        }

        return true;
    }

    // apenas para custom_schedules

    public static function validatePricePeriod($provider_id, $min_price, $max_price, $period_type)
    {
        if ($min_price < 0 || $max_price < 0) {
            throw new \Exception(__('validation.custom.schedule.invalid_price'));
        }

        if ($min_price > $max_price) {
            throw new \Exception(__('validation.custom.schedule.invalid_price_range'));
        }

        $provider = Provider::find($provider_id);

        $min_price_proportional = 0;
        $max_price_proportional = 0;
        $provider_price_period  = 0;

        switch ($period_type) {
            case '2': // 2 horas
                $provider_price_period  = $provider->daily_price_2h;
                $min_price_proportional = $min_price * 0.30;
                $max_price_proportional = $max_price * 0.30;
                break;
            case '4': // 4 horas
                $provider_price_period  = $provider->daily_price_4h;
                $min_price_proportional = $min_price * 0.55;
                $max_price_proportional = $max_price * 0.55;
                break;
            case '6': // 6 horas
                $provider_price_period  = $provider->daily_price_6h;
                $min_price_proportional = $min_price * 0.85;
                $max_price_proportional = $max_price * 0.85;

                break;
            case '8': // 8 horas
                $provider_price_period  = $provider->daily_price_8h;
                $min_price_proportional = $min_price;
                $max_price_proportional = $max_price;

                break;
            default:
                throw new \Exception(__('validation.custom.schedule.invalid_period_type'));
        }

        if ($provider_price_period < $min_price_proportional || $provider_price_period > $max_price_proportional) {
            throw new \Exception(__('validation.custom.schedule.price_not_in_range'));
        }

        return true;
    }

    /**
     * Valida se o prestador tem outro agendamento no mesmo dia e horário
     *
     * @param  int      $provider_id
     * @param  string   $date_ymd   (Y-m-d)
     * @param  string   $start_time (H:i:s)
     * @param  string   $end_time   (H:i:s)
     * @param  int|null $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateConflictingSchedule($provider_id, $date_ymd, $start_time, $end_time, $exclude_schedule_id = null)
    {
        $conflictingSchedule = Schedule::where('provider_id', $provider_id)
            ->where('date', $date_ymd)
            ->whereIn('status', ['pending', 'accepted', 'paid', 'started'])
            ->where(function ($query) use ($start_time, $end_time) {
                $query->whereBetween('start_time', [$start_time, $end_time])
                    ->orWhereBetween('end_time', [$start_time, $end_time])
                    ->orWhere(function ($q) use ($start_time, $end_time) {
                        $q->where('start_time', '<=', $start_time)
                            ->where('end_time', '>=', $end_time);
                    });
            })
            ->when($exclude_schedule_id, function ($query) use ($exclude_schedule_id) {
                $query->where('id', '!=', $exclude_schedule_id);
            })
            ->first();

        if ($conflictingSchedule) {
            throw new \Exception(__('validation.custom.schedule.provider_conflicting_schedule'));
        }

        return true;
    }

    /**
     * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário
     *
     * @param  int       $provider_id
     * @param  string    $date_ymd   (Y-m-d)
     * @param  string    $start_time (H:i:s)
     * @param  string    $end_time   (H:i:s)
     * @param  int|null  $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateConflictingSameProposal($provider_id, $schedule_id)
    {
        $conflictingSameProposal = ScheduleProposal::where('schedule_proposals.provider_id', $provider_id)
            ->where('schedule_proposals.schedule_id', $schedule_id)
            ->leftJoin('schedules', 'schedule_proposals.schedule_id', '=', 'schedules.id')
            ->whereNotIn('schedules.status', self::EXCLUDED_STATUSES)
            ->first();

        if ($conflictingSameProposal) {
            throw new \Exception(__('validation.custom.schedule.provider_conflicting_same_proposal'));
        }

        return true;
    }

    /**
     * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário, ignorando o horário
     *
     * @param  int      $provider_id
     * @param  string   $date_ymd   (Y-m-d)
     * @param  string   $start_time (H:i:s)
     * @param  string   $end_time   (H:i:s)
     * @param  int|null $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateConflictingProposalSameDate($provider_id, $date_ymd, $start_time, $end_time, $exclude_schedule_id = null)
    {
        $conflictingProposalSameDate = ScheduleProposal::where('schedule_proposals.provider_id', $provider_id)
            ->leftJoin('schedules', 'schedule_proposals.schedule_id', '=', 'schedules.id')
            ->where('schedules.date', $date_ymd)
            ->where(function ($query) use ($start_time, $end_time) {
                $query->whereBetween('schedules.start_time', [$start_time, $end_time])
                    ->orWhereBetween('schedules.end_time', [$start_time, $end_time])
                    ->orWhere(function ($q) use ($start_time, $end_time) {
                        $q->where('schedules.start_time', '<=', $start_time)
                            ->where('schedules.end_time', '>=', $end_time);
                    });
            })
            ->whereNotIn('status', self::EXCLUDED_STATUSES)
            ->when($exclude_schedule_id, function ($query) use ($exclude_schedule_id) {
                $query->where('schedules.id', '!=', $exclude_schedule_id);
            })
            ->first();

        if ($conflictingProposalSameDate) {
            throw new \Exception(__('validation.custom.schedule.provider_conflicting_proposal_same_date'));
        }

        return true;
    }

    /**
     * Valida se o cliente tem bloqueio cadastrado para o prestador
     *
     * @param  int  $client_id
     * @param  int  $provider_id
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateClientNotBlockedByProvider($client_id, $provider_id)
    {
        $provider_client_block = ProviderClientBlock::where('provider_id', $provider_id)
            ->where('client_id', $client_id)
            ->first();

        if ($provider_client_block) {
            throw new \Exception(__('validation.custom.schedule.client_blocked_by_provider'));
        }

        return true;
    }

    /**
     * Valida se o prestador tem bloqueio cadastrado para o cliente
     *
     * @param  int  $client_id
     * @param  int  $provider_id
     * @return bool
     *
     * @throws \Exception
     */
    public static function validateProviderNotBlockedByClient($client_id, $provider_id)
    {
        $client_provider_block = ClientProviderBlock::where('provider_id', $provider_id)
            ->where('client_id', $client_id)
            ->first();

        if ($client_provider_block) {
            throw new \Exception(__('validation.custom.schedule.provider_blocked_by_client'));
        }

        return true;
    }

    // -------------------------------------------------------------------------
    // Métodos de consulta em batch — usados para filtragem em listagens.
    // Não lançam exceção: retornam coleções de IDs para uso em whereIn/whereNotIn.
    // -------------------------------------------------------------------------

    /**
     * Retorna os IDs de prestadores que bloquearam o cliente OU foram bloqueados por ele.
     * Centraliza ambas as direções de bloqueio em um único método para uso em listagens.
     */
    public static function getBlockedProviderIdsForClient(int $client_id): Collection
    {
        // Prestadores que bloquearam este cliente (ProviderClientBlock)

        $blockedByProvider = ProviderClientBlock::where('client_id', $client_id)
            ->pluck('provider_id');

        // Prestadores que este cliente bloqueou (ClientProviderBlock)

        $blockedByClient = ClientProviderBlock::where('client_id', $client_id)
            ->pluck('provider_id');

        return $blockedByProvider->merge($blockedByClient)->unique()->values();
    }

    /**
     * Retorna os IDs de prestadores que possuem pelo menos um dia de trabalho cadastrado.
     * Garante que apenas prestadores ativos na plataforma apareçam em listagens.
     */
    public static function getProviderIdsWithWorkingDays(): Collection
    {
        return ProviderWorkingDay::select('provider_id')
            ->distinct()
            ->pluck('provider_id');
    }

    /**
     * Retorna os IDs de prestadores disponíveis em uma data específica,
     * dentro de um conjunto pré-filtrado de IDs.
     *
     * Regras (em batch, sem iteração PHP):
     *  1. Prestador tem pelo menos um ProviderWorkingDay para o day_of_week da data.
     *  2. Prestador NÃO tem ProviderBlockedDay com period = 'all' nessa data.
     *     (period = 'morning' ou 'afternoon' = bloqueio parcial → ainda disponível)
     *
     * @param  string     $date_ymd  Y-m-d
     * @param  Collection $providerIds  conjunto de IDs a filtrar
     */
    public static function getAvailableProviderIdsForDate(string $date_ymd, Collection $providerIds): Collection
    {
        $dayOfWeek = Carbon::parse($date_ymd)->dayOfWeek;

        $withWorkingDay = ProviderWorkingDay::whereIn('provider_id', $providerIds)
            ->where('day', $dayOfWeek)
            ->pluck('provider_id')
            ->unique();

        $fullyBlockedIds = ProviderBlockedDay::whereIn('provider_id', $withWorkingDay)
            ->where('date', $date_ymd)
            ->where('period', 'all')
            ->pluck('provider_id')
            ->unique();

        return $withWorkingDay->diff($fullyBlockedIds)->values();
    }
}
