<?php

namespace App\Domain\Shared\Services;

use App\Domain\Attendance\Enums\AttendanceStatus;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Attendance\Services\AttendanceMarkingService;
use App\Domain\Identity\Services\GuardianResolver;
use App\Domain\Participant\Models\Participant;
use App\Domain\Participant\Services\ParticipantService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use App\Domain\Shared\Models\ServiceRequest;
use App\Domain\Training\Models\TrainingSession;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Member requests, and what approving one actually does.
 *
 * `grep -rln ServiceRequest` found the model, an event, a listener and a
 * provider — and no admin screen anywhere. Approving a freeze never called
 * `ParticipantService::freeze()`; the row's status changed and nothing else
 * happened. A member was told their subscription was frozen and it was not.
 *
 * So approval here is defined by its **effect**, and the effect runs in the
 * same transaction as the status change. If the effect fails, the approval
 * fails with it — a request that says "approved" and did nothing is worse than
 * one that says "pending".
 *
 * Excuses are requests too (E7). An excuse is never a direct attendance write:
 * the deleted API's `absences/report` wrote `status='excused'` straight into
 * `attendance_records` with no marker and no check that the session belonged to
 * the participant, so a player could excuse himself and corrupt every
 * attendance figure the product reports.
 */
class ServiceRequestService
{
    public function __construct(
        private readonly GuardianResolver $guardians,
        private readonly ParticipantService $participants,
        private readonly AttendanceMarkingService $attendance,
    ) {}

    /**
     * A member opens a request.
     */
    public function open(array $data, User $requester): ServiceRequest
    {
        $type = (string) ($data['type'] ?? 'other');

        if (! in_array($type, ServiceRequest::TYPES, true)) {
            throw new DomainException('نوع الطلب غير معروف');
        }

        $participantId = (int) ($data['participant_id'] ?? 0);

        // The requester must actually speak for this member.
        if (! in_array($participantId, $this->guardians->participantIdsFor($requester), true)) {
            throw new DomainException('لا تملك صلاحية تقديم طلب عن هذا العضو');
        }

        return DB::transaction(function () use ($data, $requester, $type, $participantId) {
            $sessionId = null;
            $recordId = null;

            if ($type === 'excuse') {
                [$sessionId, $recordId] = $this->resolveExcusableSession(
                    $participantId,
                    (int) ($data['training_session_id'] ?? 0),
                );

                $duplicate = ServiceRequest::withoutGlobalScopes()
                    ->where('participant_id', $participantId)
                    ->where('training_session_id', $sessionId)
                    ->where('type', 'excuse')
                    ->where('status', 'pending')
                    ->exists();

                if ($duplicate) {
                    throw new DomainException('يوجد عذر قيد المراجعة لهذه الحصة بالفعل');
                }
            }

            return ServiceRequest::create([
                'academy_id' => $requester->academy_id,
                'user_id' => $requester->id,
                'participant_id' => $participantId,
                'training_session_id' => $sessionId,
                'attendance_record_id' => $recordId,
                'type' => $type,
                'status' => 'pending',
                'reason' => $data['reason'] ?? null,
                'attachment_path' => $data['attachment_path'] ?? null,
                'attachment_mime' => $data['attachment_mime'] ?? null,
                'attachment_size' => $data['attachment_size'] ?? null,
                'metadata' => $data['metadata'] ?? [],
            ]);
        });
    }

    /**
     * Approve, and apply the effect.
     *
     * The status change is a conditional UPDATE so two administrators cannot
     * both approve, and the effect runs inside the same transaction so an
     * approval that could not take effect does not stand.
     */
    public function approve(ServiceRequest $request, User $actor, ?string $notes = null): ServiceRequest
    {
        return DB::transaction(function () use ($request, $actor, $notes) {
            $claimed = ServiceRequest::withoutGlobalScopes()
                ->where('id', $request->id)
                ->where('status', 'pending')
                ->update([
                    'status' => 'approved',
                    'handled_by' => $actor->id,
                    'handled_at' => now(),
                    'admin_notes' => $notes,
                    'updated_at' => now(),
                ]);

            if ($claimed === 0) {
                throw new InvalidStatusTransitionException('عالج زميل آخر هذا الطلب بالفعل');
            }

            $fresh = ServiceRequest::withoutGlobalScopes()->findOrFail($request->id);

            $this->applyEffect($fresh, $actor);

            return $fresh->refresh();
        });
    }

    public function reject(ServiceRequest $request, User $actor, string $notes): ServiceRequest
    {
        $claimed = ServiceRequest::withoutGlobalScopes()
            ->where('id', $request->id)
            ->where('status', 'pending')
            ->update([
                'status' => 'rejected',
                'handled_by' => $actor->id,
                'handled_at' => now(),
                'admin_notes' => $notes,
                'updated_at' => now(),
            ]);

        if ($claimed === 0) {
            throw new InvalidStatusTransitionException('عالج زميل آخر هذا الطلب بالفعل');
        }

        return $request->refresh();
    }

    /**
     * What approval means, per type.
     *
     * `other` is the only type with no effect, and that is the point of having
     * it: a free-text request a human handles off-system.
     */
    private function applyEffect(ServiceRequest $request, User $actor): void
    {
        $participant = $request->participant_id
            ? Participant::withoutGlobalScopes()->find($request->participant_id)
            : null;

        try {
            match ($request->type) {
                'freeze' => $participant
                    ? $this->participants->freeze($participant, $request->reason ?: 'طلب من ولي الأمر', $actor)
                    : null,

                'unfreeze' => $participant
                    ? $this->participants->activate($participant, $request->reason ?: 'طلب من ولي الأمر', $actor)
                    : null,

                'cancellation' => $participant
                    ? $this->participants->withdraw($participant, $request->reason ?: 'طلب من ولي الأمر', $actor)
                    : null,

                'transfer' => $this->applyTransfer($request, $participant, $actor),

                'excuse' => $this->applyExcuse($request, $actor),

                // 'renewal' and 'other' are handled by a person; there is no
                // automatic effect to apply and pretending otherwise would be
                // the same mistake this class exists to fix.
                default => null,
            };

            ServiceRequest::withoutGlobalScopes()->where('id', $request->id)->update([
                'effect_applied_at' => now(),
                'effect_error' => null,
                'updated_at' => now(),
            ]);
        } catch (\Throwable $e) {
            Log::error('Service request effect failed', [
                'request_id' => $request->id,
                'type' => $request->type,
                'error' => $e->getMessage(),
            ]);

            // Rethrown, so the transaction rolls back and the request stays
            // pending. An approval whose effect failed must not be recorded as
            // an approval.
            throw new DomainException(
                'تعذّر تنفيذ الطلب: ' . $e->getMessage()
            );
        }
    }

    private function applyTransfer(ServiceRequest $request, ?Participant $participant, User $actor): void
    {
        $branchId = (int) ($request->metadata['branch_id'] ?? 0);

        if (! $participant || ! $branchId) {
            throw new DomainException('طلب النقل لا يحدد الفرع المطلوب');
        }

        app(\App\Domain\Participant\Services\TransferService::class)
            ->transferToBranch($participant, $branchId, $request->reason ?: 'طلب من ولي الأمر', $actor);
    }

    /**
     * Mark the absence excused — through the one attendance write path, with
     * the approving staff member as the marker.
     */
    private function applyExcuse(ServiceRequest $request, User $actor): void
    {
        $record = $request->attendance_record_id
            ? AttendanceRecord::withoutGlobalScopes()->lockForUpdate()->find($request->attendance_record_id)
            : null;

        if (! $record) {
            throw new DomainException('لا يوجد سجل حضور مرتبط بهذا العذر');
        }

        // A coach who already marked the member present observed something; an
        // excuse approved afterwards must not overwrite that observation.
        $status = $record->status instanceof \BackedEnum ? $record->status->value : (string) $record->status;

        if (in_array($status, ['present', 'late', 'partial'], true)) {
            throw new DomainException('سُجّل حضور العضو في هذه الحصة بالفعل');
        }

        $this->attendance->markStatus(
            $record,
            AttendanceStatus::Excused,
            $actor,
            $request->reason ?: 'عذر معتمد من الإدارة',
        );
    }

    /**
     * The session an excuse may be filed against, and its attendance row.
     *
     * Checked here rather than trusted from the request: the deleted endpoint
     * never checked that the session belonged to the participant's groups at
     * all, which is what let a member excuse himself from someone else's class.
     *
     * @return array{0: int, 1: int|null}
     */
    private function resolveExcusableSession(int $participantId, int $sessionId): array
    {
        if (! $sessionId) {
            throw new DomainException('يجب تحديد الحصة');
        }

        $participant = Participant::withoutGlobalScopes()->findOrFail($participantId);
        $groupIds = $participant->activeEnrollments()->pluck('training_group_id')->all();

        $session = TrainingSession::withoutGlobalScopes()
            ->where('id', $sessionId)
            ->where('academy_id', $participant->academy_id)
            ->whereIn('training_group_id', $groupIds)
            ->first();

        if (! $session) {
            throw new DomainException('هذه الحصة ليست ضمن مجموعات العضو');
        }

        // An excuse is for an absence that has not happened yet, or has just
        // happened. Excusing something from last month is a records change and
        // belongs with staff, not in a member's hands.
        if ($session->session_date?->lt(now()->subDays(7))) {
            throw new DomainException('انتهت مهلة تقديم العذر لهذه الحصة');
        }

        $record = AttendanceRecord::withoutGlobalScopes()
            ->where('training_session_id', $session->id)
            ->where('subject_type', Participant::class)
            ->where('subject_id', $participantId)
            ->first();

        return [(int) $session->id, $record?->id];
    }
}
