<?php

namespace App\Domain\Compliance\Services;

use App\Domain\Compliance\Models\AccountDeletionRequest;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Identity\Models\Person;
use App\Domain\Identity\Services\GuardianResolver;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * Deleting an account, in a system that is also an accounting system.
 *
 * Apple 5.1.1(v) and Google both refuse an app that can create an account and
 * not delete it, so this is a submission blocker rather than a refinement. But
 * "delete everything" is not available here: invoices, payments and ledger rows
 * are an academy's books, and a member must not be able to erase them by
 * tapping a button.
 *
 * So deletion means **redaction**: the person's identifying details are
 * destroyed, the account stops working, and the financial record survives with
 * the person's name replaced. The store requirement is satisfied — the account
 * is gone and the personal data with it — without handing every member a delete
 * key to the academy's accounts.
 *
 * Three deliberate delays between the tap and the destruction:
 *
 *  1. A cooling-off window. Deletion is irreversible and people tap things when
 *     they are angry.
 *  2. An outstanding-balance check. A member with an unpaid invoice is not
 *     stuck — the request is *blocked*, the reason is shown, and it proceeds
 *     once settled.
 *  3. An active-child check, because a guardian deleting their own login while
 *     a child is mid-enrolment orphans the child's record.
 */
class AccountDeletionService
{
    /** How long a member has to change their mind. */
    private const COOLING_OFF_DAYS = 7;

    public function __construct(private readonly GuardianResolver $guardians) {}

    /**
     * @return array<int, string> the reasons this account cannot be deleted yet
     */
    public function blockers(User $user): array
    {
        $blockers = [];
        $participantIds = $this->guardians->participantIdsFor($user);

        if ($participantIds !== []) {
            $due = (int) Invoice::withoutGlobalScopes()
                ->where('academy_id', $user->academy_id)
                ->where('billable_type', Participant::class)
                ->whereIn('billable_id', $participantIds)
                ->whereIn('status', ['sent', 'partially_paid', 'overdue'])
                ->sum('due_amount');

            if ($due > 0) {
                $blockers[] = 'يوجد مبلغ مستحق: ' . format_money($due);
            }

            $active = Participant::withoutGlobalScopes()
                ->whereIn('id', $participantIds)
                ->whereHas('activeEnrollments')
                ->count();

            if ($active > 0) {
                $blockers[] = 'يوجد ' . $active . ' اشتراك نشط — أوقف الاشتراك أولاً';
            }
        }

        return $blockers;
    }

    public function request(User $user, ?string $reason, ?string $password, ?string $ip = null): AccountDeletionRequest
    {
        // Re-authenticate. An unattended phone must not be able to delete
        // somebody's account, and this is the last irreversible action here.
        if ($password === null || ! Hash::check($password, $user->password)) {
            throw new DomainException('كلمة المرور غير صحيحة');
        }

        $existing = AccountDeletionRequest::withoutGlobalScopes()
            ->where('user_id', $user->id)
            ->whereIn('status', ['pending', 'blocked'])
            ->first();

        if ($existing) {
            return $existing;
        }

        $blockers = $this->blockers($user);

        return AccountDeletionRequest::create([
            'academy_id' => $user->academy_id,
            'user_id' => $user->id,
            'status' => $blockers === [] ? 'pending' : 'blocked',
            'reason' => $reason,
            'requested_at' => now(),
            'eligible_at' => now()->addDays(self::COOLING_OFF_DAYS),
            'blockers' => $blockers,
            'request_ip' => $ip,
        ]);
    }

    public function cancel(AccountDeletionRequest $request): void
    {
        AccountDeletionRequest::withoutGlobalScopes()
            ->where('id', $request->id)
            ->whereIn('status', ['pending', 'blocked'])
            ->update(['status' => 'cancelled', 'cancelled_at' => now(), 'updated_at' => now()]);
    }

    /**
     * Carry out the redaction.
     *
     * Only the account holder's own person record is redacted — never a child's.
     * A child belongs to the academy's records and may have a second guardian;
     * erasing them because one adult left would destroy somebody else's data.
     */
    public function complete(AccountDeletionRequest $request, ?User $actor = null): void
    {
        $user = User::withoutGlobalScopes()->findOrFail($request->user_id);

        $blockers = $this->blockers($user);

        if ($blockers !== []) {
            AccountDeletionRequest::withoutGlobalScopes()->where('id', $request->id)->update([
                'status' => 'blocked',
                'blockers' => json_encode($blockers),
                'updated_at' => now(),
            ]);

            throw new DomainException('لا يمكن إتمام الحذف: ' . implode(' · ', $blockers));
        }

        if ($request->eligible_at?->isFuture()) {
            throw new DomainException('فترة التراجع لم تنتهِ بعد');
        }

        DB::transaction(function () use ($request, $user, $actor) {
            $marker = 'حساب محذوف';

            if ($user->person_id) {
                $person = Person::withoutGlobalScopes()->find($user->person_id);

                // Redact only if this person is not still a participant in the
                // academy — an adult member's own record is part of the
                // enrolment, not just a login.
                $isParticipant = $person && Participant::withoutGlobalScopes()
                    ->where('person_id', $person->id)
                    ->whereHas('activeEnrollments')
                    ->exists();

                if ($person && ! $isParticipant) {
                    $person->forceFill([
                        'name' => $marker,
                        'name_ar' => $marker,
                        'email' => null,
                        'phone' => null,
                        'phone_secondary' => null,
                        'national_id' => null,
                        'address' => null,
                        'date_of_birth' => null,
                        'photo_path' => null,
                        'medical_notes' => null,
                        'emergency_contact_name' => null,
                        'emergency_contact_phone' => null,
                        'notes' => null,
                        'redacted_at' => now(),
                    ])->save();
                }
            }

            // The login is destroyed outright: a random password nobody holds,
            // a non-routable address, no phone to resolve, and every token gone.
            $user->forceFill([
                'name' => $marker,
                'name_ar' => $marker,
                'email' => 'deleted-' . $user->id . '@portal.invalid',
                'email_is_synthetic' => true,
                'phone' => null,
                'password' => Hash::make(Str::random(64)),
                'status' => 'inactive',
                'remember_token' => null,
            ])->save();

            $user->tokens()->delete();

            DB::table('device_tokens')->where('user_id', $user->id)->delete();

            $user->delete();

            AccountDeletionRequest::withoutGlobalScopes()->where('id', $request->id)->update([
                'status' => 'completed',
                'completed_at' => now(),
                'handled_by' => $actor?->id,
                'updated_at' => now(),
            ]);
        });
    }

    /**
     * Everything this account holds, as a structured export.
     *
     * The other half of the same obligation: a member is entitled to leave with
     * their data, not only to stop existing.
     */
    public function export(User $user): array
    {
        $participantIds = $this->guardians->participantIdsFor($user);

        return [
            'exported_at' => now()->toIso8601String(),
            'account' => [
                'name' => $user->name_ar ?: $user->name,
                'phone' => $user->phone,
                'email' => $user->email_is_synthetic ? null : $user->email,
                'created_at' => $user->created_at?->toIso8601String(),
                'last_login_at' => $user->last_login_at?->toIso8601String(),
            ],
            'members' => Participant::withoutGlobalScopes()
                ->whereIn('id', $participantIds)
                ->with('person')
                ->get()
                ->map(fn (Participant $p) => [
                    'name' => $p->person?->name_ar ?? $p->person?->name,
                    'date_of_birth' => $p->person?->date_of_birth?->toDateString(),
                    'membership_type' => $p->membership_type,
                    'status' => $p->status instanceof \BackedEnum ? $p->status->value : $p->status,
                    'registered_at' => $p->created_at?->toIso8601String(),
                ])->all(),
            'invoices' => Invoice::withoutGlobalScopes()
                ->where('billable_type', Participant::class)
                ->whereIn('billable_id', $participantIds)
                ->with('items')
                ->get()
                ->map(fn (Invoice $i) => [
                    'number' => $i->number,
                    'issued' => $i->issue_date?->toDateString(),
                    'total' => $i->total_amount,
                    'paid' => $i->paid_amount,
                    'status' => $i->status->value,
                    'items' => $i->items->map(fn ($item) => [
                        'description' => $item->description,
                        'quantity' => $item->quantity,
                        'total' => $item->total_amount,
                    ])->all(),
                ])->all(),
            'consents' => \App\Domain\Compliance\Models\Consent::withoutGlobalScopes()
                ->where('given_by', $user->id)
                ->orderBy('granted_at')
                ->get(['type', 'granted', 'document_version', 'granted_at'])
                ->map(fn ($c) => [
                    'type' => $c->type instanceof \BackedEnum ? $c->type->value : $c->type,
                    'granted' => $c->granted,
                    'version' => $c->document_version,
                    'at' => $c->granted_at?->toIso8601String(),
                ])->all(),
            // Amounts are in piasters, as everywhere else in this system.
            'note' => 'كل المبالغ بالقروش (١٠٠ قرش = ١ جنيه).',
        ];
    }
}
