<?php

namespace App\Domain\Identity\Services;

use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Helpers\CredentialNormalizer;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * Merges two accounts that belong to the same person.
 *
 * 2026_08_30_000004 normalised phone numbers and deliberately left duplicates
 * in place — it logged the ids that "need a manual merge" rather than failing
 * the migration, which was the right call: a unique index would have hard-failed
 * on at least one live client and then blocked every later migration on that
 * client forever.
 *
 * This is the manual merge it was waiting for. Until duplicates are cleared,
 * `->first()` on a phone number stays an account-selection primitive, which is
 * why AuthService refuses an ambiguous login rather than guessing.
 *
 * The merge moves references rather than deleting rows: a deleted user id in a
 * financial record is worse than a duplicate account.
 */
class AccountMergeService
{
    /**
     * Accounts sharing a phone number or a case-insensitive email.
     *
     * @return Collection<int, array{key: string, kind: string, users: Collection<int, User>}>
     */
    public function findDuplicates(int $academyId): Collection
    {
        $users = User::withoutGlobalScopes()
            ->where('academy_id', $academyId)
            ->whereNull('deleted_at')
            ->get(['id', 'name', 'name_ar', 'email', 'phone', 'person_id', 'status', 'last_login_at', 'email_is_synthetic']);

        $groups = collect();

        $byPhone = $users->filter(fn ($u) => filled($u->phone))
            ->groupBy(fn ($u) => CredentialNormalizer::phone($u->phone));

        foreach ($byPhone as $phone => $group) {
            if ($group->count() > 1) {
                $groups->push(['key' => (string) $phone, 'kind' => 'phone', 'users' => $group->values()]);
            }
        }

        $byEmail = $users->filter(fn ($u) => filled($u->email) && ! $u->email_is_synthetic)
            ->groupBy(fn ($u) => CredentialNormalizer::email($u->email));

        foreach ($byEmail as $email => $group) {
            if ($group->count() > 1) {
                $groups->push(['key' => (string) $email, 'kind' => 'email', 'users' => $group->values()]);
            }
        }

        return $groups;
    }

    /**
     * Point everything at $keep and retire $merge.
     *
     * Ownership columns are moved rather than rows deleted. `created_by` on an
     * invoice is a fact about who did something, and rewriting it to a
     * different person would be a lie; only the columns that mean "this
     * account" are moved, and the retired row is soft-deleted so the id keeps
     * resolving in anything that still references it.
     */
    public function merge(User $keep, User $merge, User $actor): void
    {
        if ($keep->id === $merge->id) {
            throw new DomainException('لا يمكن دمج حساب مع نفسه');
        }

        if ((int) $keep->academy_id !== (int) $merge->academy_id) {
            throw new DomainException('لا يمكن دمج حسابين من أكاديميتين مختلفتين');
        }

        if ($keep->person_id && $merge->person_id && (int) $keep->person_id !== (int) $merge->person_id) {
            throw new DomainException(
                'الحسابان مرتبطان بشخصين مختلفين — راجع البيانات قبل الدمج'
            );
        }

        DB::transaction(function () use ($keep, $merge, $actor) {
            // Anything that identifies "the member behind the account".
            $this->repoint('guardians', 'user_id', $merge->id, $keep->id);
            $this->repoint('people', 'user_id', $merge->id, $keep->id);
            $this->repoint('device_tokens', 'user_id', $merge->id, $keep->id);
            $this->repoint('notification_preferences', 'user_id', $merge->id, $keep->id);
            $this->repoint('service_requests', 'user_id', $merge->id, $keep->id);
            $this->repoint('contact_messages', 'user_id', $merge->id, $keep->id);
            $this->repoint('portal_invitations', 'consumed_by', $merge->id, $keep->id);
            $this->repoint('login_history', 'user_id', $merge->id, $keep->id);

            // A person keeps whichever real credentials existed across the two.
            if ($keep->email_is_synthetic && ! $merge->email_is_synthetic) {
                $realEmail = $merge->email;

                // Free the address before claiming it: users.email is UNIQUE.
                User::withoutGlobalScopes()->where('id', $merge->id)->update([
                    'email' => 'merged-' . $merge->id . '@portal.invalid',
                    'email_is_synthetic' => true,
                ]);

                $keep->email = $realEmail;
                $keep->email_is_synthetic = false;
            }

            if (blank($keep->phone) && filled($merge->phone)) {
                $keep->phone = $merge->phone;
            }

            if (! $keep->person_id && $merge->person_id) {
                $keep->person_id = $merge->person_id;
            }

            $keep->save();

            // `inactive`, not a new 'merged' status: users_status_check allows
            // exactly active|inactive|suspended|pending, and widening a CHECK
            // for a bookkeeping nicety is not worth a migration on every
            // tenant. The soft delete is what actually says this is retired.
            User::withoutGlobalScopes()->where('id', $merge->id)->update([
                'status' => 'inactive',
                'phone' => null,
                'updated_at' => now(),
            ]);

            User::withoutGlobalScopes()->where('id', $merge->id)->delete();

            DB::table('audit_logs')->insert([
                'academy_id' => $keep->academy_id,
                'user_id' => $actor->id,
                'auditable_type' => User::class,
                'auditable_id' => $keep->id,
                'event' => 'account_merged',
                'new_values' => json_encode(['merged_user_id' => $merge->id, 'kept_user_id' => $keep->id]),
                'is_financial' => false,
                'created_at' => now(),
            ]);
        });
    }

    private function repoint(string $table, string $column, int $from, int $to): void
    {
        if (! \Illuminate\Support\Facades\Schema::hasTable($table)
            || ! \Illuminate\Support\Facades\Schema::hasColumn($table, $column)) {
            return;
        }

        DB::table($table)->where($column, $from)->update([$column => $to]);
    }
}
