<?php

namespace App\Domain\Portal\Services;

use App\Domain\Identity\Models\Guardian;
use App\Domain\Identity\Models\Person;
use App\Domain\Identity\Models\Role;
use App\Domain\Identity\Services\PhoneVerificationService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Participant\Services\DuplicateDetectionService;
use App\Domain\Participant\Services\ParticipantService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Helpers\CredentialNormalizer;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * A stranger becomes an account, and a child becomes a pending participant.
 *
 * This is the only path in the product that writes into `people` and
 * `participants` with no member of staff in the loop, which is why almost all
 * of it is refusal:
 *
 *  - The phone must be verified **at the moment of writing**, not merely at
 *    some earlier point in the session.
 *  - `DuplicateDetectionService` runs on every signup and its findings are
 *    stored on the row, so staff see "this looks like an existing member"
 *    instead of a second Person quietly appearing beside the first.
 *  - A self-registered participant lands **unapproved**. It is a row awaiting
 *    review, not a member.
 *
 * It does not enrol and it does not take money. `EnrollmentService::enroll()`
 * requires a `User $actor` who is authorised to enrol, and a self-registering
 * guardian is a user but is not that. Loosening it for this path would loosen
 * it for every path; the member asks, and staff enrol.
 */
class PortalRegistrationService
{
    public function __construct(
        private readonly PhoneVerificationService $verification,
        private readonly DuplicateDetectionService $duplicates,
        private readonly ParticipantService $participants,
    ) {}

    /**
     * @param  array{guardian_name: string, phone: string, email?: ?string,
     *               password: string, child_name: string,
     *               child_date_of_birth: ?string, child_gender: ?string,
     *               branch_id: ?int}  $data
     * @return array{user: User, participant: Participant, duplicates: array}
     */
    public function register(array $data, int $academyId, ?string $ip = null): array
    {
        $phone = CredentialNormalizer::phone($data['phone'] ?? null);

        if (! $phone) {
            throw new DomainException('رقم الهاتف غير صالح');
        }

        // Re-checked here rather than trusted from the wizard's own state: the
        // step that verified the code may have been an hour ago, or may have
        // been skipped by a request that never went through the wizard.
        if (! $this->verification->isVerified($academyId, $phone)) {
            throw new DomainException('لم يتم التحقق من رقم الهاتف');
        }

        if (User::withoutGlobalScopes()->where('academy_id', $academyId)->where('phone', $phone)->exists()) {
            throw new DomainException('يوجد حساب بهذا الرقم بالفعل — سجّل الدخول بدلاً من إنشاء حساب جديد');
        }

        return DB::transaction(function () use ($data, $academyId, $phone, $ip) {
            // The account is created first, with no person attached, and then
            // stamps itself as the creator of both people.
            //
            // `people.created_by` is NOT NULL and there is no staff member in
            // this flow, so something has to be the author. A system user would
            // be a lie about who typed this, and relaxing the column would
            // weaken it for every other path. `users.person_id` is nullable, so
            // the account can exist for the moment it takes to become the
            // author of its own record — which is also the honest answer: the
            // person who registered created these rows.
            $user = $this->createAccount($academyId, $phone, $data, $data['password']);

            $guardianPerson = Person::create([
                'academy_id' => $academyId,
                'name_ar' => $data['guardian_name'],
                'name' => $data['guardian_name'],
                'phone' => $phone,
                'email' => CredentialNormalizer::email($data['email'] ?? null),
                'source' => 'online',
                'created_by' => $user->id,
                'user_id' => $user->id,
            ]);

            $user->forceFill(['person_id' => $guardianPerson->id])->save();

            $childPerson = Person::create([
                'academy_id' => $academyId,
                'name_ar' => $data['child_name'],
                'name' => $data['child_name'],
                'date_of_birth' => $data['child_date_of_birth'] ?? null,
                'gender' => $data['child_gender'] ?? null,
                'source' => 'online',
                'created_by' => $user->id,
            ]);

            // Run before the participant is written, so the findings are stored
            // on the row staff will actually review.
            $candidates = $this->duplicates->findPotentialDuplicates(
                $data['child_name'],
                null,
                null,
                $data['child_date_of_birth'] ?? null,
            );

            // Through ParticipantService, not around it. It allocates the
            // participant number, refuses a person who is already a member,
            // stamps the audit columns and raises ParticipantRegistered — all
            // of which a second creation path would silently skip. The actor is
            // the registering guardian, which is the truth about who did this.
            //
            // register() already lands the row in 'registered', which is
            // exactly the pending state this flow wants: every list that
            // filters on active keeps it out until somebody looks at it.
            $participant = $this->participants->register([
                'person_id' => $childPerson->id,
                'branch_id' => $data['branch_id'] ?? null,
                'registration_source' => 'online',
                'membership_type' => 'non_member',
            ], $user);

            $participant->forceFill([
                'approved_at' => null,
                'duplicate_candidates' => $candidates->take(5)->map(fn ($c) => [
                    'participant_id' => $c->id ?? null,
                    'name' => $c->person?->name_ar ?? $c->name_ar ?? null,
                ])->values()->all(),
            ])->save();

            // Through the model, not DB::table(): the uuid comes from the
            // HasUuid trait's creating hook, and a raw insert skips it straight
            // into a NOT NULL violation.
            $guardian = Guardian::create([
                'academy_id' => $academyId,
                'person_id' => $guardianPerson->id,
                'user_id' => $user->id,
                'relationship_type' => 'guardian',
                'is_emergency_contact' => true,
                'is_financial_responsible' => true,
                'can_pickup' => true,
            ]);

            $participant->guardians()->attach($guardian->id, [
                'relationship_type' => 'guardian',
                'is_primary' => true,
                'is_emergency_contact' => true,
                'can_pickup' => true,
                'receives_notifications' => true,
                // The registering adult can pay for the child they registered.
                'can_authorize_payment' => true,
                'created_at' => now(),
            ]);

            return [
                'user' => $user,
                'participant' => $participant,
                'duplicates' => $candidates->all(),
            ];
        });
    }

    /**
     * Approve a self-registered participant.
     *
     * Approval is what a staff member does after looking at the duplicate
     * candidates — it is deliberately not automatic and deliberately not
     * something the member can trigger.
     */
    public function approve(Participant $participant, User $actor): void
    {
        $updated = Participant::withoutGlobalScopes()
            ->where('id', $participant->id)
            ->whereNull('approved_at')
            ->update([
                'approved_at' => now(),
                'approved_by' => $actor->id,
                'status' => 'active',
                'updated_at' => now(),
            ]);

        if ($updated === 0) {
            throw new DomainException('تمت مراجعة هذا التسجيل بالفعل');
        }
    }

    private function createAccount(int $academyId, string $phone, array $data, string $password): User
    {
        $normalizedEmail = CredentialNormalizer::email($data['email'] ?? null);

        $user = new User();
        $user->academy_id = $academyId;
        $user->person_id = null;   // set once the Person exists
        $user->name = $data['guardian_name'];
        $user->name_ar = $data['guardian_name'];
        $user->phone = $phone;
        $user->status = 'active';

        // users.email is NOT NULL UNIQUE and cannot be relaxed; a .invalid
        // address is guaranteed never to resolve, so it cannot bounce off the
        // mail server, and email_is_synthetic is what every mail path checks.
        if ($normalizedEmail && ! User::withoutGlobalScopes()->where('email', $normalizedEmail)->exists()) {
            $user->email = $normalizedEmail;
            $user->email_is_synthetic = false;
        } else {
            $user->email = 'p' . Str::uuid() . '@portal.invalid';
            $user->email_is_synthetic = true;
        }

        $user->password = Hash::make($password);

        $role = Role::withoutGlobalScopes()
            ->where('academy_id', $academyId)
            ->where('slug', 'parent')
            ->first();

        $user->role_id = $role?->id;
        $user->save();

        if ($role) {
            DB::table('role_user')->insert([
                'user_id' => $user->id,
                'role_id' => $role->id,
                'created_at' => now(),
            ]);
        }

        return $user;
    }
}
