<?php

namespace App\Services;

use App\Models\MediaFile;
use App\Models\Project;
use App\Models\Subscription;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Support\Facades\DB;

/**
 * Hard-delete a user and everything that's theirs, keeping the financial record
 * for accounting. Shared by the admin user-delete and the GDPR self-service
 * account deletion (Account/ProfileController::destroy) so erasure behaves
 * identically whoever triggers it.
 *
 * Subscriptions + invoices SURVIVE (user_id null-out + email snapshot) — that's
 * a legal-obligation retention carve-out, GDPR-compatible. Everything else
 * (tenant → projects → screens → media) is removed.
 */
class UserPurger
{
    public function purge(User $user): void
    {
        $email = $user->email;

        // Which workspaces does deleting this user actually destroy? Only the
        // ones where this user is the LAST inhabitant — no OTHER pivot member
        // and no OTHER active-pointer user. Candidates are the user's active
        // pointer (users.tenant_id) PLUS every workspace they own via the pivot.
        //
        // Why the last-inhabitant test (not "delete users.tenant_id"): the
        // active pointer is set by WorkspaceController::switch to ANY workspace
        // the user belongs to, including a SHARED one owned by someone else.
        // Deleting by it tore down a teammate's whole workspace (projects +
        // media) when a mere member was deleted while that workspace was their
        // active pointer. The last-inhabitant test leaves any still-populated
        // workspace standing while still cleaning up the user's own solo space
        // — even on legacy/backfilled installs where a personal workspace has a
        // tenant_id pointer but no pivot row. Self-hosted has no tenant rows /
        // pivots, so the candidate set is empty and the shared pool is safe.
        $candidateIds = collect([$user->tenant_id])
            ->merge($user->workspaces()->wherePivot('role', 'owner')->pluck('tenants.id'))
            ->filter()
            ->unique()
            ->values();

        $deletableTenantIds = $candidateIds
            ->reject(function ($tid) use ($user): bool {
                $hasOtherMember = DB::table('tenant_user')
                    ->where('tenant_id', $tid)
                    ->where('user_id', '!=', $user->id)
                    ->exists();
                $hasOtherUser = User::query()
                    ->where('tenant_id', $tid)
                    ->where('id', '!=', $user->id)
                    ->exists();

                return $hasOtherMember || $hasOtherUser; // someone else is still here → keep it
            })
            ->values()
            ->all();

        // Collect storage paths up front (before DB rows go).
        $projectIds = $deletableTenantIds !== []
            ? Project::query()->whereIn('tenant_id', $deletableTenantIds)->pluck('id')->all()
            : [];

        DB::transaction(function () use ($user, $deletableTenantIds, $email) {
            // Snapshot the email onto every subscription so the orphaned row
            // stays attributable on the admin Subscriptions / Invoices pages.
            Subscription::query()
                ->where('user_id', $user->id)
                ->update(['user_email_snapshot' => $email]);

            // Drop the user's memberships first — shared workspaces (owned by
            // others) lose this member but stay intact.
            $user->workspaces()->detach();

            // Tenant cascade nukes projects → screens for the deletable spaces.
            if ($deletableTenantIds !== []) {
                Tenant::query()->whereIn('id', $deletableTenantIds)->delete();
            }

            // User goes last. provider_customers cascades; subscriptions
            // null-out their user_id (nullOnDelete) and survive with the
            // snapshot for accounting.
            $user->delete();
        });

        // Disk cleanup outside the transaction — orphaning files is recoverable;
        // a registry walk inside a tx that might roll back is not. MediaStorage
        // looks up each row's disk before deleting, so cloud-stored files clean
        // up correctly; the batch collapses N audit rows into one.
        if ($projectIds !== []) {
            MediaStorage::beginBatch('media.user.purge');
            try {
                MediaFile::query()
                    ->where('owner_type', 'project')
                    ->whereIn('owner_id', $projectIds)
                    ->get()
                    ->each(function (MediaFile $row): void {
                        MediaStorage::delete($row->path);
                    });
            } finally {
                MediaStorage::endBatch("Purged user {$email} project media");
            }
        }
    }
}
