<?php

namespace Modules\MembershipApplication\Services;

use App\Services\DocumentService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Modules\Auth\Entities\User;
use Modules\Auth\Services\EmailVerificationOtpService;
use Modules\MembershipApplication\Entities\MembershipApplication;
use Modules\MembershipApplication\Enums\ApplicationStatus;
use Modules\MembershipApplication\Enums\BoardDecision;
use Modules\MembershipApplication\Enums\BoardResult;
use Modules\MembershipApplication\Enums\ManagementDecision;
use Modules\MembershipApplication\Enums\ReferenceType;
use Modules\MembershipApplication\Enums\ReviewStage;
use Modules\MembershipApplication\Emails\MembershipApplicationFailedNotification;
use Modules\MembershipApplication\Emails\MembershipApplicationPassedNotification;
use Modules\MembershipApplication\Emails\MembershipApplicationSubmittedNotification;
use Modules\MembershipApplication\Repositories\Contracts\MembershipApplicationRepositoryInterface;
use Modules\Role\Entities\Role;

class MembershipApplicationService
{
    public function __construct(
        protected MembershipApplicationRepositoryInterface $repository,
        protected DocumentService $documentService,
    ) {}

    /**
     * @param  array{
     *     applicant: array<string, mixed>,
     *     heirs: list<array<string, mixed>>,
     *     references?: array{proposer_ic_number?: string|null, supporter_ic_number?: string|null}
     * }  $data
     * @param  array<string, UploadedFile>  $documents
     */
    public function submit(array $data, array $documents): MembershipApplication
    {
        $application = DB::transaction(function () use ($data, $documents) {
            // membership_applications table
            $application = $this->repository->create([
                'application_number' => $this->generateApplicationNumber(),
                'status' => ApplicationStatus::Submitted,
                'board_result' => '',
                'submitted_at' => now()->toDateTimeString(),
                'completed_at' => '',
            ]);

            // membership_application_applicants table
            $application->applicant()->create($data['applicant']);

            // membership_application_heirs table
            foreach ($data['heirs'] as $heir) {
                $application->heirs()->create($heir);
            }

            // membership_application_references table
            $this->syncReferences(
                $application,
                $data['references'] ?? [],
                (string) ($data['applicant']['ic_number'] ?? ''),
            );

            // documents table
            foreach ($documents as $type => $file) {
                $this->documentService->validateFile($file);
                $this->documentService->uploadDocument($application, $file, $type);
            }

            return $this->repository->findByIdWithRelations($application->id);
        });

        $this->sendSubmissionConfirmation($application);

        return $application;
    }

    public function getPaginatedList(int $perPage, string $search, ?string $status, string $sortBy, string $sortOrder): LengthAwarePaginator 
    {
        return $this->repository->getAllPaginated($perPage, $search, $status, $sortBy, $sortOrder);
    }

    public function getByIdWithRelations(string $id): ?MembershipApplication
    {
        return $this->repository->findByIdWithRelations($id);
    }

    public function update(MembershipApplication $application, array $data): MembershipApplication
    {
        if ($application->status === ApplicationStatus::Completed) {
            throw ValidationException::withMessages([
                'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
            ]);
        }

        return DB::transaction(function () use ($application, $data) {
            $application->loadMissing('applicant');

            if (isset($data['applicant'])) {
                $application->applicant->update($data['applicant']);
            }

            if (isset($data['heirs'])) {
                $application->heirs()->delete();
                foreach ($data['heirs'] as $heir) {
                    $application->heirs()->create($heir);
                }
            }

            if (array_key_exists('references', $data)) {
                $this->syncReferences(
                    $application,
                    $data['references'] ?? [],
                    (string) ($data['applicant']['ic_number'] ?? $application->applicant->ic_number),
                );
            }

            return $this->repository->findByIdWithRelations($application->id);
        });
    }

    public function uploadDocument(MembershipApplication $application, string $type, UploadedFile $file): MembershipApplication
    {
        if ($application->status === ApplicationStatus::Completed) {
            throw ValidationException::withMessages([
                'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
            ]);
        }

        $this->documentService->validateFile($file);

        $application->documentsOfType($type)->get()->each(
            fn ($document) => $this->documentService->deleteDocument($document->id)
        );

        $this->documentService->uploadDocument($application, $file, $type);

        return $this->repository->findByIdWithRelations($application->id);
    }

    public function managementReview(MembershipApplication $application, ManagementDecision $decision, ?string $remarks, User $reviewer): MembershipApplication {
        if ($application->status !== ApplicationStatus::Submitted) {
            throw ValidationException::withMessages([
                'status' => ['Permohonan ini tidak boleh disemak pada peringkat pentadbiran.'],
            ]);
        }

        return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) {
            $application->reviews()->create([
                'stage' => ReviewStage::Management,
                'decision' => $decision->value,
                'remarks' => $remarks,
                'reviewer_id' => $reviewer->id,
                'reviewed_at' => now(),
            ]);

            $application->update([
                'status' => $decision === ManagementDecision::Approved
                    ? ApplicationStatus::PendingBoard
                    : ApplicationStatus::ManagementRejected,
            ]);

            return $this->repository->findByIdWithRelations($application->id);
        });
    }

    public function boardReview(MembershipApplication $application, BoardDecision $decision, ?string $remarks, User $reviewer): MembershipApplication 
    {
        if ($application->status !== ApplicationStatus::PendingBoard) {
            throw ValidationException::withMessages([
                'status' => ['Permohonan ini tidak boleh disemak pada peringkat lembaga.'],
            ]);
        }

        return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) {
            $application->reviews()->create([
                'stage' => ReviewStage::Board,
                'decision' => $decision->value,
                'remarks' => $remarks,
                'reviewer_id' => $reviewer->id,
                'reviewed_at' => now(),
            ]);

            $application->update([
                'board_result' => $decision === BoardDecision::Pass
                    ? BoardResult::Pass->value
                    : BoardResult::Fail->value,
                'status' => ApplicationStatus::PendingNotification,
            ]);

            return $this->repository->findByIdWithRelations($application->id);
        });
    }

    /**
     * @param  array{proposer_id?: string|null, supporter_id?: string|null}  $references
     */
    public function assignReferences(MembershipApplication $application, array $references, User $assignedBy): MembershipApplication 
    {
        return DB::transaction(function () use ($application, $references, $assignedBy) {
            $referenceMap = [
                'proposer_id' => ReferenceType::Proposer,
                'supporter_id' => ReferenceType::Supporter,
            ];

            foreach ($referenceMap as $key => $type) {
                if (! array_key_exists($key, $references)) {
                    continue;
                }

                $application->references()->updateOrCreate(
                    ['reference_type' => $type->value],
                    [
                        'user_id' => $references[$key],
                        'assigned_by' => $references[$key] ? $assignedBy->id : null,
                        'assigned_at' => $references[$key] ? now() : null,
                    ]
                );
            }

            return $this->repository->findByIdWithRelations($application->id);
        });
    }

    public function complete(MembershipApplication $application, User $processedBy): MembershipApplication
    {
        if ($application->status !== ApplicationStatus::PendingNotification) {
            throw ValidationException::withMessages([
                'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'],
            ]);
        }

        if (! in_array($application->board_result, [BoardResult::Pass->value, BoardResult::Fail->value], true)) {
            throw ValidationException::withMessages([
                'board_result' => ['Keputusan lembaga belum ditetapkan.'],
            ]);
        }

        return DB::transaction(function () use ($application) {
            if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
                $user = $this->createMemberFromApplication($application);
                $application->update(['user_id' => $user->id]);
            }

            $this->sendResultNotification($application);

            $application->update([
                'status' => ApplicationStatus::Completed,
                'completed_at' => now()->toDateTimeString(),
            ]);

            return $this->repository->findByIdWithRelations($application->id);
        });
    }

    /**
     * @param  list<string>  $applicationIds
     * @return array{
     *     succeeded: list<MembershipApplication>,
     *     failed: list<array{id: string, application_number: string|null, message: string}>
     * }
     */
    public function completeBatch(array $applicationIds, User $processedBy): array
    {
        $succeeded = [];
        $failed = [];

        foreach ($applicationIds as $id) {
            $application = $this->repository->findById($id);

            if (! $application) {
                $failed[] = [
                    'id' => $id,
                    'application_number' => null,
                    'message' => 'Permohonan tidak dijumpai.',
                ];

                continue;
            }

            try {
                $succeeded[] = $this->complete($application, $processedBy);
            } catch (ValidationException $e) {
                $failed[] = [
                    'id' => $id,
                    'application_number' => $application->application_number,
                    'message' => collect($e->errors())->flatten()->first() ?? 'Permohonan tidak boleh diselesaikan.',
                ];
            }
        }

        return [
            'succeeded' => $succeeded,
            'failed' => $failed,
        ];
    }

    protected function createMemberFromApplication(MembershipApplication $application): User
    {
        $application->loadMissing(['applicant', 'heirs']);
        $applicant = $application->applicant;

        if (User::where('email', $applicant->email)->exists()) {
            throw ValidationException::withMessages([
                'email' => ['E-mel pemohon sudah didaftarkan dalam sistem.'],
            ]);
        }

        $user = User::create([
            'name' => $applicant->name,
            'email' => $applicant->email,
            'password' => Hash::make(Str::password(16)),
            'ic_number' => $applicant->ic_number,
            'phone_number' => $applicant->phone_number,
            'position' => $applicant->current_position,
            'status' => 'active',
            'gender' => $applicant->gender,
            'marriage_status' => $applicant->marriage_status,
            'member_number' => $this->generateMemberNumber(),
            'member_type' => 'Anggota',
            'join_date' => now()->toDateString(),
            'birth_date' => $applicant->birth_date,
            'birth_place' => $applicant->birth_place,
        ]);

        $role = Role::where('name', 'Anggota')->first();
        if ($role) {
            $user->assignRole($role);
        }

        $user->addresses()->create([
            'address_type' => 'home',
            'address_line_1' => $applicant->address,
            'postcode' => $applicant->postcode,
        ]);

        $user->employments()->create([
            'company_name' => $applicant->employer_name,
            'job_title' => $applicant->current_position,
            'employment_type' => 'Permanent',
            'salary' => 0,
            'start_date' => $applicant->start_work_date,
            'is_current' => true,
        ]);

        foreach ($application->heirs as $index => $heir) {
            $user->heirs()->create([
                'name' => $heir->name,
                'ic_number' => $heir->ic_number,
                'relationship' => $heir->relationship,
                'phone_number' => $heir->phone_number,
                'is_primary' => $index === 0,
            ]);
        }

        app(EmailVerificationOtpService::class)->send($user);

        return $user;
    }

    public function lookupMemberByIcNumber(string $icNumber): ?User
    {
        $icNumber = trim($icNumber);

        if ($icNumber === '') {
            return null;
        }

        return User::query()
            ->where('ic_number', $icNumber)
            ->where('status', 'active')
            ->first();
    }

    protected function sendSubmissionConfirmation(MembershipApplication $application): void
    {
        $application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']);

        Notification::route('mail', $application->applicant->email)
            ->notify(new MembershipApplicationSubmittedNotification($application));
    }

    protected function sendResultNotification(MembershipApplication $application): void
    {
        $application->loadMissing('applicant');

        $notification = $application->board_result === BoardResult::Pass->value
            ? new MembershipApplicationPassedNotification($application)
            : new MembershipApplicationFailedNotification($application);

        Notification::route('mail', $application->applicant->email)->notify($notification);
    }

    protected function generateMemberNumber(): int
    {
        return ((int) User::max('member_number')) + 1;
    }

    /**
     * @param  array{proposer_ic_number?: string|null, supporter_ic_number?: string|null}  $references
     */
    protected function syncReferences(MembershipApplication $application, array $references, string $applicantIcNumber): void
    {
        $referenceMap = [
            'references.proposer_ic_number' => ['field' => 'proposer_ic_number', 'type' => ReferenceType::Proposer],
            'references.supporter_ic_number' => ['field' => 'supporter_ic_number', 'type' => ReferenceType::Supporter],
        ];

        $resolvedUserIds = [];
        $applicantIcNumber = trim($applicantIcNumber);

        foreach ($referenceMap as $errorKey => $config) {
            if (! array_key_exists($config['field'], $references)) {
                continue;
            }

            $icNumber = trim((string) ($references[$config['field']] ?? ''));

            if ($icNumber === '') {
                $application->references()->where('reference_type', $config['type']->value)->delete();

                continue;
            }

            if ($applicantIcNumber !== '' && strcasecmp($icNumber, $applicantIcNumber) === 0) {
                throw ValidationException::withMessages([
                    $errorKey => ['Pencadang/penyokong tidak boleh sama dengan pemohon.'],
                ]);
            }

            $member = $this->lookupMemberByIcNumber($icNumber);

            if (! $member) {
                throw ValidationException::withMessages([
                    $errorKey => ['Ahli dengan nombor kad pengenalan ini tidak dijumpai.'],
                ]);
            }

            if (in_array($member->id, $resolvedUserIds, true)) {
                throw ValidationException::withMessages([
                    $errorKey => ['Pencadang dan penyokong mestilah ahli yang berbeza.'],
                ]);
            }

            $resolvedUserIds[] = $member->id;

            $application->references()->updateOrCreate(
                ['reference_type' => $config['type']->value],
                ['user_id' => $member->id]
            );
        }
    }

    protected function generateApplicationNumber(): string
    {
        $year = now()->year;
        $prefix = "APP-{$year}-";

        $latestNumber = MembershipApplication::query()
            ->where('application_number', 'like', "{$prefix}%")
            ->orderByDesc('application_number')
            ->value('application_number');

        $sequence = 1;

        if ($latestNumber && preg_match('/-(\d+)$/', $latestNumber, $matches)) {
            $sequence = ((int) $matches[1]) + 1;
        }

        return sprintf('%s%05d', $prefix, $sequence);
    }
}
