<?php

namespace App\Http\Controllers;

use App\Models\Activity;
use App\Models\Expense;
use App\Models\Group;
use App\Models\GroupInvitation;
use App\Models\Person;
use App\Models\SharedList;
use App\Models\SharedListItem;
use App\Models\SharedPurchase;
use App\Models\SpaceMember;
use App\Models\User;
use App\Support\WeshareMath;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;

class AppPagesController extends Controller
{
    private function adoptExistingUserIntoSpaceByEmail(string $spaceId, string $email, ?Person $spacePerson = null): void
    {
        $email = strtolower(trim($email));
        if ($email === '') {
            return;
        }

        $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first();
        if (! $user) {
            return;
        }

        SpaceMember::query()->firstOrCreate(
            ['space_id' => $spaceId, 'user_id' => $user->id],
            ['role' => 'member', 'created_at' => now()]
        );

        if ($spacePerson) {
            $spacePerson->auth_user_id = $user->id;
            if (trim((string) $spacePerson->email) === '') {
                $spacePerson->email = $email;
            }
            $spacePerson->save();
        }

        if ($user->profile) {
            // This app currently assumes a single active space; switching makes the invited data visible immediately.
            $user->profile->default_space_id = $spaceId;
            $user->profile->save();
        }
    }

    public function home(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;

        $mePerson = $spaceId
            ? Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first()
            : null;

        $groups = $spaceId
            ? Group::query()
                ->where('space_id', $spaceId)
                ->when($mePerson, fn ($q) => $q->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id)))
                ->with(['members'])
                ->orderBy('created_at', 'desc')
                ->get()
            : collect();

        $expenses = $spaceId
            ? Expense::query()
                ->where('space_id', $spaceId)
                ->when($mePerson, function ($q) use ($mePerson, $spaceId) {
                    $groupIds = Group::query()
                        ->where('space_id', $spaceId)
                        ->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id))
                        ->pluck('id')
                        ->all();
                    $listIds = SharedList::query()
                        ->where('space_id', $spaceId)
                        ->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id))
                        ->pluck('id')
                        ->all();

                    return $q->where(function ($qq) use ($groupIds, $listIds) {
                        $qq->whereIn('group_id', $groupIds)
                            ->orWhereIn('shared_list_id', $listIds);
                    });
                })
                ->with(['paidBy'])
                ->orderBy('expense_date', 'desc')
                ->get()
            : collect();

        $activities = $spaceId
            ? Activity::query()
                ->where('space_id', $spaceId)
                ->with(['user'])
                ->orderBy('activity_at', 'desc')
                ->get()
            : collect();

        $sharedLists = $spaceId
            ? SharedList::query()
                ->where('space_id', $spaceId)
                ->when($mePerson, fn ($q) => $q->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id)))
                ->with(['members', 'items'])
                ->orderBy('created_at', 'desc')
                ->get()
            : collect();

        $sharedPurchases = $spaceId
            ? SharedPurchase::query()
                ->where('space_id', $spaceId)
                ->when($mePerson, fn ($q) => $q->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id)))
                ->with(['members'])
                ->orderBy('created_at', 'desc')
                ->get()
            : collect();

        // BalanceSummary parity with React: overall balance across all expenses.
        $balance = 0.0;
        if ($mePerson) {
            $paidByMe = (float) $expenses->where('paid_by_person_id', $mePerson->id)->sum('amount');
            $myShare = 0.0;
            foreach ($expenses as $exp) {
                $split = is_array($exp->split_among) ? $exp->split_among : [];
                foreach ($split as $row) {
                    $uid = (string) (data_get($row, 'user.id') ?? data_get($row, 'userId') ?? '');
                    if ($uid !== $mePerson->id) {
                        continue;
                    }
                    $myShare += (float) (data_get($row, 'amount') ?? 0);
                }
            }
            $balance = round($paidByMe - $myShare, 2);
        }

        return view('app.home', [
            'groups' => $groups,
            'expenses' => $expenses,
            'activities' => $activities,
            'sharedLists' => $sharedLists,
            'sharedPurchases' => $sharedPurchases,
            'mePerson' => $mePerson,
            'overallBalance' => $balance,
        ]);
    }

    public function onboarding(): View
    {
        return view('app.onboarding');
    }

    public function completeOnboarding(Request $request)
    {
        $user = $request->user();
        abort_unless($user, 403);

        $data = $request->validate([
            'display_name' => ['required', 'string', 'min:1', 'max:80'],
            'avatar' => ['required', 'string', 'max:8'],
        ]);

        $profile = $user->profile;
        abort_unless($profile, 403);

        $profile->display_name = trim($data['display_name']);
        $profile->avatar = trim($data['avatar']);
        $profile->onboarding_completed_at = now();
        $profile->updated_at = now();
        $profile->save();

        $spaceId = $profile->default_space_id;
        if ($spaceId) {
            $me = Person::query()
                ->where('space_id', $spaceId)
                ->where('auth_user_id', $user->id)
                ->first();
            if ($me) {
                $me->name = $profile->display_name;
                $me->avatar = $profile->avatar;
                $me->save();
            }
        }

        return redirect()->route('app.home');
    }

    public function groups(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;

        $mePerson = $spaceId ? Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first() : null;

        $groups = $spaceId
            ? Group::query()
                ->where('space_id', $spaceId)
                ->when($mePerson, fn ($q) => $q->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id)))
                ->with(['members'])
                ->orderBy('created_at', 'desc')
                ->get()
            : collect();

        // Preload expenses once for balance badges on cards.
        $expenses = $spaceId
            ? Expense::query()->where('space_id', $spaceId)->with(['paidBy'])->get()
            : collect();
        // Filter expenses to only groups I belong to.
        if ($mePerson) {
            $groupIds = $groups->pluck('id')->all();
            $expenses = $expenses->whereIn('group_id', $groupIds)->values();
        }

        return view('app.groups', [
            'groups' => $groups,
            'expenses' => $expenses,
            'mePersonId' => $mePerson?->id,
        ]);
    }

    public function groupDetail(string $id): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;

        $group = $spaceId
            ? Group::query()
                ->where('space_id', $spaceId)
                ->where('id', $id)
                ->with(['members'])
                ->firstOrFail()
            : abort(404);

        $expenses = Expense::query()
            ->where('space_id', $spaceId)
            ->where('group_id', $id)
            ->whereNull('shared_list_id')
            ->with(['paidBy'])
            ->orderBy('expense_date', 'desc')
            ->get();

        $mePerson = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($mePerson && $group->members->contains('id', $mePerson->id), 403);

        $balances = WeshareMath::groupBalances($group, $expenses);
        $myBal = collect($balances)->firstWhere('userId', $mePerson?->id);

        return view('app.group-detail', [
            'group' => $group,
            'expenses' => $expenses,
            'balances' => $balances,
            'myAmount' => (float) ($myBal['amount'] ?? 0),
        ]);
    }

    public function groupMembers(string $id): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()
            ->where('space_id', $spaceId)
            ->where('id', $id)
            ->with(['members'])
            ->firstOrFail();

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);

        $allUsers = Person::query()
            ->where('space_id', $spaceId)
            ->orderBy('created_at', 'asc')
            ->get();

        $ids = $group->members->pluck('id')->all();
        $notInGroup = $allUsers->reject(fn ($u) => in_array($u->id, $ids, true))->values();

        $invitations = GroupInvitation::query()
            ->where('space_id', $spaceId)
            ->where('group_id', $group->id)
            ->orderBy('created_at', 'desc')
            ->get();

        return view('app.group-members', [
            'group' => $group,
            'notInGroup' => $notInGroup,
            'invitations' => $invitations,
            'mePerson' => $me,
        ]);
    }

    public function expenseDetail(string $groupId, string $expenseId): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()->where('space_id', $spaceId)->where('id', $groupId)->with(['members'])->first();
        $expense = Expense::query()
            ->where('space_id', $spaceId)
            ->where('id', $expenseId)
            ->with(['paidBy'])
            ->firstOrFail();

        abort_if(! $group || $expense->group_id !== $groupId, 404);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);

        return view('app.expense-detail', [
            'group' => $group,
            'expense' => $expense,
        ]);
    }

    public function activity(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;

        $activities = $spaceId
            ? Activity::query()
                ->where('space_id', $spaceId)
                ->with(['user'])
                ->orderBy('activity_at', 'desc')
                ->get()
            : collect();

        $mePerson = $spaceId ? Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first() : null;
        if ($spaceId && $mePerson) {
            $groupNames = Group::query()
                ->where('space_id', $spaceId)
                ->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id))
                ->pluck('name')
                ->filter()
                ->values()
                ->all();
            $listNames = SharedList::query()
                ->where('space_id', $spaceId)
                ->whereHas('members', fn ($mq) => $mq->where('people.id', $mePerson->id))
                ->pluck('name')
                ->filter()
                ->values()
                ->all();

            $activities = $activities->filter(function ($a) use ($groupNames, $listNames, $mePerson) {
                if ((string) $a->user_person_id === (string) $mePerson->id) return true;
                if ($a->group_name && in_array((string) $a->group_name, $groupNames, true)) return true;
                if ($a->list_name && in_array((string) $a->list_name, $listNames, true)) return true;
                return false;
            })->values();
        }

        return view('app.activity', [
            'activities' => $activities,
            'mePerson' => $mePerson,
        ]);
    }

    public function account(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();

        $groups = $me
            ? Group::query()
                ->where('space_id', $spaceId)
                ->whereHas('members', fn ($mq) => $mq->where('people.id', $me->id))
                ->with(['members'])
                ->orderBy('created_at', 'desc')
                ->get()
            : collect();

        $expenses = Expense::query()
            ->where('space_id', $spaceId)
            ->orderBy('expense_date', 'desc')
            ->get();
        if ($me) {
            $groupIds = $groups->pluck('id')->all();
            $listIds = SharedList::query()
                ->where('space_id', $spaceId)
                ->whereHas('members', fn ($mq) => $mq->where('people.id', $me->id))
                ->pluck('id')
                ->all();
            $expenses = $expenses->filter(fn ($e) => in_array((string) $e->group_id, $groupIds, true) || ($e->shared_list_id && in_array((string) $e->shared_list_id, $listIds, true)))->values();
        }

        $overallBalance = 0.0;
        if ($me) {
            foreach ($expenses as $exp) {
                $paid = (string) $exp->paid_by_person_id === (string) $me->id ? (float) $exp->amount : 0.0;
                $share = 0.0;
                $split = is_array($exp->split_among) ? $exp->split_among : [];
                foreach ($split as $row) {
                    $uid = (string) (data_get($row, 'user.id') ?? data_get($row, 'userId') ?? '');
                    if ($uid !== (string) $me->id) {
                        continue;
                    }
                    $share += (float) (data_get($row, 'amount') ?? 0);
                }
                $overallBalance += ($paid - $share);
            }
        }
        $overallBalance = round((float) $overallBalance, 2);

        $invitations = GroupInvitation::query()
            ->where('space_id', $spaceId)
            ->orderBy('created_at', 'desc')
            ->get();

        return view('app.account', [
            'mePerson' => $me,
            'invitations' => $invitations,
            'groups' => $groups,
            'overallBalance' => $overallBalance,
            'profile' => $user?->profile,
        ]);
    }

    public function updateAccountPreferences(Request $request)
    {
        $user = $request->user();
        abort_unless($user, 403);

        $data = $request->validate([
            'locale' => ['nullable', 'in:en,pt'],
            'currency' => ['nullable', 'in:USD,BRL,EUR'],
        ]);

        $profile = $user->profile;
        abort_unless($profile, 403);

        if (isset($data['locale'])) {
            $profile->locale = $data['locale'];
        }
        if (isset($data['currency'])) {
            $profile->currency = $data['currency'];
        }
        $profile->updated_at = now();
        $profile->save();

        return redirect()->route('app.account');
    }

    public function editAccountProfile(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $profile = $user->profile;
        abort_unless($profile, 403);

        return view('app.profile-edit', [
            'profile' => $profile,
        ]);
    }

    public function updateAccountProfile(Request $request)
    {
        $user = $request->user();
        abort_unless($user, 403);

        $data = $request->validate([
            'display_name' => ['required', 'string', 'min:1', 'max:80'],
            'avatar' => ['required', 'string', 'max:8'],
        ]);

        $profile = $user->profile;
        abort_unless($profile, 403);

        $profile->display_name = trim($data['display_name']);
        $profile->avatar = trim($data['avatar']);
        $profile->updated_at = now();
        $profile->save();

        $spaceId = $profile->default_space_id;
        if ($spaceId) {
            $me = Person::query()
                ->where('space_id', $spaceId)
                ->where('auth_user_id', $user->id)
                ->first();
            if ($me) {
                $me->name = $profile->display_name;
                $me->avatar = $profile->avatar;
                $me->save();
            }
        }

        return redirect()->route('app.account');
    }

    public function revokeGroupInvitation(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $inv = GroupInvitation::query()->where('space_id', $spaceId)->where('id', $id)->firstOrFail();
        $inv->status = 'revoked';
        $inv->save();

        return redirect()->route('app.account');
    }

    public function insights(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();

        $groupIds = Group::query()
            ->where('space_id', $spaceId)
            ->whereHas('members', fn ($mq) => $mq->where('people.id', $me->id))
            ->pluck('id')
            ->all();

        $start = now()->startOfMonth()->toDateString();
        $end = now()->endOfMonth()->toDateString();

        $expenses = Expense::query()
            ->where('space_id', $spaceId)
            ->whereIn('group_id', $groupIds)
            ->whereBetween('expense_date', [$start, $end])
            ->with(['paidBy'])
            ->orderBy('expense_date', 'desc')
            ->get();

        $totalSpent = round((float) $expenses->sum('amount'), 2);
        $expenseCount = $expenses->count();
        $avgExpense = $expenseCount > 0 ? round($totalSpent / $expenseCount, 2) : 0.0;

        $paidByMe = round((float) $expenses->where('paid_by_person_id', $me->id)->sum('amount'), 2);

        $myShare = 0.0;
        foreach ($expenses as $exp) {
            $split = is_array($exp->split_among) ? $exp->split_among : [];
            foreach ($split as $row) {
                $uid = (string) (data_get($row, 'user.id') ?? data_get($row, 'userId') ?? '');
                if ($uid !== $me->id) {
                    continue;
                }
                $myShare += (float) (data_get($row, 'amount') ?? 0);
            }
        }
        $myShare = round($myShare, 2);

        $topPayerId = $expenses->groupBy('paid_by_person_id')->map(fn ($g) => (float) $g->sum('amount'))->sortDesc()->keys()->first();
        $topPayer = $topPayerId
            ? Person::query()->where('space_id', $spaceId)->where('id', (string) $topPayerId)->first()
            : null;
        $topPayerAmount = $topPayerId ? (float) $expenses->where('paid_by_person_id', (string) $topPayerId)->sum('amount') : null;

        $topCategory = null;
        if ($expenses->count() > 0) {
            $byCat = $expenses->groupBy('category')->map(function ($g) {
                $first = $g->first();
                return [
                    'category' => (string) ($first?->category ?? ''),
                    'emoji' => (string) ($first?->emoji ?? '🧾'),
                    'amount' => round((float) $g->sum('amount'), 2),
                ];
            })->values()->sortByDesc('amount')->first();
            $topCategory = $byCat ?: null;
        }

        // Fairness: compare total paid vs total share for each person present in the month.
        $people = Person::query()->where('space_id', $spaceId)->orderBy('created_at', 'asc')->get();
        $paidMap = [];
        $shareMap = [];
        foreach ($people as $p) {
            $paidMap[$p->id] = 0.0;
            $shareMap[$p->id] = 0.0;
        }
        foreach ($expenses as $exp) {
            $paidMap[(string) $exp->paid_by_person_id] = ($paidMap[(string) $exp->paid_by_person_id] ?? 0.0) + (float) $exp->amount;
            $split = is_array($exp->split_among) ? $exp->split_among : [];
            foreach ($split as $row) {
                $uid = (string) (data_get($row, 'user.id') ?? data_get($row, 'userId') ?? '');
                if ($uid === '') {
                    continue;
                }
                $shareMap[$uid] = ($shareMap[$uid] ?? 0.0) + (float) (data_get($row, 'amount') ?? 0);
            }
        }

        $fairness = [];
        foreach ($people as $p) {
            $paid = round((float) ($paidMap[$p->id] ?? 0.0), 2);
            $share = round((float) ($shareMap[$p->id] ?? 0.0), 2);
            if ($paid <= 0.009 && $share <= 0.009) {
                continue;
            }
            $pct = $share > 0.009 ? ($paid / $share) * 100 : ($paid > 0 ? 100.0 : 0.0);
            $pct = max(0.0, min(200.0, $pct));
            $status = 'fair';
            if ($paid - $share > 0.5) {
                $status = 'overpaying';
            } elseif ($share - $paid > 0.5) {
                $status = 'underpaying';
            }
            $fairness[] = [
                'user' => $p,
                'totalPaid' => $paid,
                'totalShare' => $share,
                'contributionPct' => $pct,
                'status' => $status,
            ];
        }
        usort($fairness, fn ($a, $b) => $b['contributionPct'] <=> $a['contributionPct']);

        $monthLabel = now()->format('F Y');

        return view('app.insights', [
            'mePerson' => $me,
            'monthLabel' => $monthLabel,
            'totalSpent' => $totalSpent,
            'expenseCount' => $expenseCount,
            'avgExpense' => $avgExpense,
            'myPaid' => $paidByMe,
            'myShare' => $myShare,
            'topPayer' => $topPayer,
            'topPayerAmount' => $topPayerAmount ? round((float) $topPayerAmount, 2) : null,
            'topCategory' => $topCategory,
            'fairness' => $fairness,
        ]);
    }

    public function addExpense(Request $request): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;

        $me = $spaceId ? Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first() : null;

        $groups = $spaceId
            ? Group::query()->where('space_id', $spaceId)->with(['members'])->orderBy('created_at', 'desc')->get()
            : collect();

        if ($me) {
            $groups = $groups->filter(fn ($g) => $g->members?->contains('id', $me->id))->values();
        }

        return view('app.add-expense', [
            'group' => $request->query('group'),
            'groups' => $groups,
            'mePerson' => $me,
        ]);
    }

    public function storeExpense(Request $request)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;

        $data = $request->validate([
            'group_id' => ['required', 'string'],
            'paid_by_person_id' => ['required', 'string'],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'description' => ['required', 'string', 'min:1', 'max:200'],
            'expense_date' => ['required', 'date'],
            'category' => ['required', 'string'],
            'emoji' => ['required', 'string', 'max:8'],
        ]);

        abort_unless($spaceId, 403);

        $group = Group::query()->where('space_id', $spaceId)->where('id', $data['group_id'])->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);
        abort_unless($group->members->contains('id', $data['paid_by_person_id']), 422);

        $amount = round((float) $data['amount'], 2);
        $members = $group->members->values();
        $n = max(1, $members->count());
        $cents = (int) round($amount * 100);
        $base = intdiv($cents, $n);
        $rem = $cents - ($base * $n);
        $split = [];
        foreach ($members as $i => $m) {
            $split[] = [
                'userId' => $m->id,
                'amount' => ($base + ($i < $rem ? 1 : 0)) / 100,
            ];
        }

        $id = 'e_' . bin2hex(random_bytes(8));

        Expense::create([
            'id' => $id,
            'space_id' => $spaceId,
            'group_id' => $group->id,
            'shared_list_id' => null,
            'description' => trim($data['description']),
            'amount' => $amount,
            'currency' => $group->currency ?? 'USD',
            'paid_by_person_id' => $data['paid_by_person_id'],
            'split_type' => 'equal',
            'split_among' => $split,
            'category' => $data['category'],
            'emoji' => $data['emoji'],
            'expense_date' => $data['expense_date'],
            'is_recurring' => false,
        ]);

        Activity::create([
            'id' => 'a_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'activity_type' => 'expense',
            'description' => 'added an expense',
            'i18n' => null,
            'user_person_id' => $data['paid_by_person_id'],
            'amount' => $amount,
            'group_name' => $group->name,
            'list_name' => null,
            'activity_at' => now(),
        ]);

        return redirect()->route('app.group.detail', ['id' => $group->id]);
    }

    public function createGroup(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();
        $people = Person::query()->where('space_id', $spaceId)->orderBy('created_at', 'asc')->get();

        return view('app.create-group', [
            'mePerson' => $me,
            'people' => $people,
        ]);
    }

    public function storeGroup(Request $request)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();

        $data = $request->validate([
            'name' => ['required', 'string', 'min:1', 'max:80'],
            'type' => ['required', 'in:home,trip,event'],
            'member_ids' => ['array'],
            'member_ids.*' => ['string'],
            'invite_email' => ['nullable', 'email', 'max:180'],
            'invite_avatar' => ['nullable', 'string', 'max:8'],
        ]);

        $typeEmoji = ['home' => '🏠', 'trip' => '✈️', 'event' => '🎉'];
        $emoji = $typeEmoji[$data['type']] ?? '📁';

        $groupId = 'g_' . bin2hex(random_bytes(6));

        $group = Group::create([
            'id' => $groupId,
            'space_id' => $spaceId,
            'name' => trim($data['name']),
            'emoji' => $emoji,
            'type' => $data['type'],
            'currency' => $user?->profile?->currency ?: 'USD',
            'total_expenses' => 0,
            'created_at' => now()->toDateString(),
        ]);

        $memberIds = collect($data['member_ids'] ?? [])->filter()->unique()->values();
        $memberIds->push($me->id);

        if (! empty($data['invite_email'])) {
            $email = strtolower(trim($data['invite_email']));
            $inviteAvatar = trim((string) ($data['invite_avatar'] ?? ''));
            $existing = Person::query()->where('space_id', $spaceId)->where('email', $email)->first();
            $invited = $existing ?: Person::create([
                'id' => 'p_' . bin2hex(random_bytes(6)),
                'space_id' => $spaceId,
                'auth_user_id' => null,
                'email' => $email,
                'name' => Str::of($email)->before('@')->toString(),
                'avatar' => ($inviteAvatar !== '' ? $inviteAvatar : '🙂'),
                'created_at' => now(),
            ]);

            $memberIds->push($invited->id);

            // If the invited email already belongs to an existing user, attach them to this space now.
            $this->adoptExistingUserIntoSpaceByEmail($spaceId, $email, $invited);

            GroupInvitation::create([
                'id' => 'gi_' . bin2hex(random_bytes(6)),
                'space_id' => $spaceId,
                'group_id' => $group->id,
                'invited_email' => $email,
                'invited_name' => $invited->name,
                'invited_person_id' => $invited->id,
                'status' => 'pending',
                'invited_by_person_id' => $me->id,
                'created_at' => now(),
            ]);
        }

        $people = Person::query()->where('space_id', $spaceId)->whereIn('id', $memberIds->filter()->unique()->all())->get();
        $group->members()->syncWithoutDetaching($people->pluck('id')->all());

        Activity::create([
            'id' => 'a_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'activity_type' => 'group_created',
            'description' => 'created a group',
            'i18n' => null,
            'user_person_id' => $me->id,
            'amount' => null,
            'group_name' => $group->name,
            'list_name' => null,
            'activity_at' => now(),
        ]);

        return redirect()->route('app.group.members', ['id' => $group->id]);
    }

    public function addMemberToGroup(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);

        $data = $request->validate([
            'person_id' => ['nullable', 'string'],
            'invite_email' => ['nullable', 'email', 'max:180'],
            'invite_avatar' => ['nullable', 'string', 'max:8'],
            'resplit' => ['nullable', 'in:0,1'],
        ]);

        $newIds = collect();
        if (! empty($data['person_id'])) {
            $person = Person::query()->where('space_id', $spaceId)->where('id', $data['person_id'])->firstOrFail();
            $newIds->push($person->id);
        }

        if (! empty($data['invite_email'])) {
            $email = strtolower(trim((string) $data['invite_email']));
            $inviteAvatar = trim((string) ($data['invite_avatar'] ?? ''));
            $existing = Person::query()->where('space_id', $spaceId)->where('email', $email)->first();
            $invited = $existing ?: Person::create([
                'id' => 'p_' . bin2hex(random_bytes(6)),
                'space_id' => $spaceId,
                'auth_user_id' => null,
                'email' => $email,
                'name' => Str::of($email)->before('@')->toString(),
                'avatar' => ($inviteAvatar !== '' ? $inviteAvatar : '🙂'),
                'created_at' => now(),
            ]);
            $newIds->push($invited->id);

            // If the invited email already belongs to an existing user, attach them to this space now.
            $this->adoptExistingUserIntoSpaceByEmail($spaceId, $email, $invited);

            // Track invitation for visibility / revocation (even if we also add immediately).
            $existsInv = GroupInvitation::query()
                ->where('space_id', $spaceId)
                ->where('group_id', $group->id)
                ->where('invited_email', $email)
                ->where('status', 'pending')
                ->exists();
            if (! $existsInv) {
                GroupInvitation::create([
                    'id' => 'gi_' . bin2hex(random_bytes(6)),
                    'space_id' => $spaceId,
                    'group_id' => $group->id,
                    'invited_email' => $email,
                    'invited_name' => $invited->name,
                    'invited_person_id' => $invited->id,
                    'status' => 'pending',
                    'invited_by_person_id' => $me->id,
                    'created_at' => now(),
                ]);
            }
        }

        $newIds = $newIds->filter()->unique()->values();
        abort_unless($newIds->count() > 0, 422);
        $group->members()->syncWithoutDetaching($newIds->all());

        // Mark pending invites as accepted if the person is now a member.
        if (! empty($data['invite_email'])) {
            $email = strtolower(trim((string) $data['invite_email']));
            GroupInvitation::query()
                ->where('space_id', $spaceId)
                ->where('group_id', $group->id)
                ->where('invited_email', $email)
                ->where('status', 'pending')
                ->update(['status' => 'accepted']);
        }

        // Optionally resplit equal expenses by adding new member shares.
        if (($data['resplit'] ?? '0') === '1') {
            $expenses = Expense::query()
                ->where('space_id', $spaceId)
                ->where('group_id', $group->id)
                ->where('split_type', 'equal')
                ->whereNull('shared_list_id')
                ->get();

            foreach ($expenses as $exp) {
                $members = $group->members->values();
                $n = max(1, $members->count());
                $cents = (int) round(((float) $exp->amount) * 100);
                $base = intdiv($cents, $n);
                $rem = $cents - ($base * $n);
                $split = [];
                foreach ($members as $i => $m) {
                    $split[] = [
                        'userId' => $m->id,
                        'amount' => ($base + ($i < $rem ? 1 : 0)) / 100,
                    ];
                }
                $exp->split_among = $split;
                $exp->save();
            }
        }

        return redirect()->route('app.group.members', ['id' => $group->id]);
    }

    public function removeMemberFromGroup(Request $request, string $id, string $personId)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);

        abort_unless($group->members->contains('id', $personId), 404);
        abort_if($group->members->count() <= 1, 422);

        $group->members()->detach([$personId]);

        // If there are pending invitations for that person, revoke them too.
        GroupInvitation::query()
            ->where('space_id', $spaceId)
            ->where('group_id', $group->id)
            ->where('invited_person_id', $personId)
            ->where('status', 'pending')
            ->update(['status' => 'revoked']);

        return redirect()->route('app.group.members', ['id' => $group->id]);
    }

    public function updateGroup(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);

        $data = $request->validate([
            'name' => ['required', 'string', 'min:1', 'max:80'],
            'emoji' => ['required', 'string', 'max:8'],
            'type' => ['required', 'in:home,trip,event'],
            'currency' => ['required', 'in:USD,BRL,EUR'],
        ]);

        $group->name = trim($data['name']);
        $group->emoji = trim($data['emoji']);
        $group->type = (string) $data['type'];
        $group->currency = (string) $data['currency'];
        $group->save();

        return redirect()->route('app.group.members', ['id' => $group->id]);
    }

    public function revokeGroupInvitationFromGroup(Request $request, string $id, string $invitationId)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);

        $inv = GroupInvitation::query()
            ->where('space_id', $spaceId)
            ->where('group_id', $group->id)
            ->where('id', $invitationId)
            ->firstOrFail();

        $inv->status = 'revoked';
        $inv->save();

        return redirect()->route('app.group.members', ['id' => $group->id]);
    }

    public function settleUp(string $id): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()
            ->where('space_id', $spaceId)
            ->where('id', $id)
            ->with(['members'])
            ->firstOrFail();

        $expenses = Expense::query()
            ->where('space_id', $spaceId)
            ->where('group_id', $group->id)
            ->whereNull('shared_list_id')
            ->with(['paidBy'])
            ->orderBy('expense_date', 'desc')
            ->get();

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();
        $settlements = WeshareMath::simplifiedSettlementsForGroup($group, $expenses);

        return view('app.settle-up', [
            'group' => $group,
            'mePerson' => $me,
            'settlements' => $settlements,
        ]);
    }

    public function storeGroupSettlement(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $group = Group::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $group->members->contains('id', $me->id), 403);

        $data = $request->validate([
            'from_person_id' => ['required', 'string'],
            'to_person_id' => ['required', 'string', 'different:from_person_id'],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'paid_on' => ['required', 'date'],
        ]);

        abort_unless($group->members->contains('id', $data['from_person_id']), 422);
        abort_unless($group->members->contains('id', $data['to_person_id']), 422);

        $amount = round((float) $data['amount'], 2);

        Expense::create([
            'id' => 'e_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'group_id' => $group->id,
            'shared_list_id' => null,
            'description' => 'Settlement',
            'amount' => $amount,
            'currency' => $group->currency ?? 'USD',
            'paid_by_person_id' => $data['from_person_id'],
            'split_type' => 'custom',
            'split_among' => [
                ['userId' => $data['to_person_id'], 'amount' => $amount],
            ],
            'category' => 'settlement',
            'emoji' => '↔️',
            'expense_date' => $data['paid_on'],
            'is_recurring' => false,
        ]);

        Activity::create([
            'id' => 'a_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'activity_type' => 'settlement',
            'description' => 'recorded a settlement',
            'i18n' => null,
            'user_person_id' => $data['from_person_id'],
            'amount' => $amount,
            'group_name' => $group->name,
            'list_name' => null,
            'activity_at' => now(),
        ]);

        return redirect()->route('app.group.detail', ['id' => $group->id]);
    }

    public function sharedListsIntro(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;

        $me = $spaceId
            ? Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first()
            : null;

        $sharedLists = $spaceId
            ? SharedList::query()
                ->where('space_id', $spaceId)
                ->when($me, fn ($q) => $q->whereHas('members', fn ($mq) => $mq->where('people.id', $me->id)))
                ->with(['members', 'items'])
                ->orderBy('created_at', 'desc')
                ->get()
            : collect();

        $expenses = $spaceId
            ? Expense::query()
                ->where('space_id', $spaceId)
                ->when($me, function ($q) use ($me, $spaceId) {
                    $listIds = SharedList::query()
                        ->where('space_id', $spaceId)
                        ->whereHas('members', fn ($mq) => $mq->where('people.id', $me->id))
                        ->pluck('id')
                        ->all();

                    return $q->whereIn('shared_list_id', $listIds);
                })
                ->with(['paidBy'])
                ->orderBy('expense_date', 'desc')
                ->get()
            : collect();

        return view('app.shared-lists-index', [
            'sharedLists' => $sharedLists,
            'expenses' => $expenses,
            'mePerson' => $me,
        ]);
    }

    public function sharedListsNew(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();
        $people = Person::query()->where('space_id', $spaceId)->orderBy('created_at', 'asc')->get();

        return view('app.shared-lists-new', [
            'mePerson' => $me,
            'people' => $people,
        ]);
    }

    public function sharedListDetail(string $id): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()
            ->where('space_id', $spaceId)
            ->where('id', $id)
            ->with(['members', 'items', 'items.paidBy', 'items.addedBy'])
            ->firstOrFail();

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $latestCheckout = Expense::query()
            ->where('space_id', $spaceId)
            ->where('shared_list_id', $list->id)
            ->where('category', 'shared_list')
            ->with(['paidBy'])
            ->orderBy('expense_date', 'desc')
            ->orderBy('id', 'desc')
            ->first();

        return view('app.shared-list-detail', [
            'list' => $list,
            'mePerson' => $me,
            'latestCheckout' => $latestCheckout,
        ]);
    }

    public function sharedListMembers(string $id): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()
            ->where('space_id', $spaceId)
            ->where('id', $id)
            ->with(['members'])
            ->firstOrFail();

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $notInList = Person::query()
            ->where('space_id', $spaceId)
            ->whereNotIn('id', $list->members->pluck('id')->all())
            ->orderBy('created_at', 'asc')
            ->get();

        return view('app.shared-list-members', [
            'list' => $list,
            'notInList' => $notInList,
        ]);
    }

    public function addMemberToSharedList(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $data = $request->validate([
            'person_id' => ['nullable', 'string'],
            'invite_email' => ['nullable', 'email', 'max:180'],
            'invite_avatar' => ['nullable', 'string', 'max:8'],
        ]);

        $newIds = collect();
        if (! empty($data['person_id'])) {
            $p = Person::query()->where('space_id', $spaceId)->where('id', $data['person_id'])->firstOrFail();
            $newIds->push($p->id);
        }

        if (! empty($data['invite_email'])) {
            $email = strtolower(trim($data['invite_email']));
            $inviteAvatar = trim((string) ($data['invite_avatar'] ?? ''));
            $existing = Person::query()->where('space_id', $spaceId)->where('email', $email)->first();
            $invited = $existing ?: Person::create([
                'id' => 'p_' . bin2hex(random_bytes(6)),
                'space_id' => $spaceId,
                'auth_user_id' => null,
                'email' => $email,
                'name' => \Illuminate\Support\Str::of($email)->before('@')->toString(),
                'avatar' => ($inviteAvatar !== '' ? $inviteAvatar : '🙂'),
                'created_at' => now(),
            ]);
            $newIds->push($invited->id);

            // If the invited email already belongs to an existing user, attach them to this space now.
            $this->adoptExistingUserIntoSpaceByEmail($spaceId, $email, $invited);
        }

        $newIds = $newIds->filter()->unique()->values();
        if ($newIds->count() > 0) {
            $list->members()->syncWithoutDetaching($newIds->all());
        }

        return redirect()->route('app.shared-lists.members', ['id' => $list->id]);
    }

    public function settleSharedList(string $id): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()
            ->where('space_id', $spaceId)
            ->where('id', $id)
            ->with(['members'])
            ->firstOrFail();

        $expenses = Expense::query()
            ->where('space_id', $spaceId)
            ->where('shared_list_id', $list->id)
            ->with(['paidBy'])
            ->orderBy('expense_date', 'desc')
            ->get();

        // Reuse group settlement algorithm by building a fake Group-like container:
        $fakeGroup = new Group();
        $fakeGroup->id = '__list__' . $list->id;
        $fakeGroup->setRelation('members', $list->members);

        $settlements = WeshareMath::simplifiedSettlementsForGroup($fakeGroup, $expenses);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();

        return view('app.shared-list-settle', [
            'list' => $list,
            'mePerson' => $me,
            'settlements' => $settlements,
        ]);
    }

    public function storeSharedListSettlement(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $data = $request->validate([
            'from_person_id' => ['required', 'string'],
            'to_person_id' => ['required', 'string', 'different:from_person_id'],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'paid_on' => ['required', 'date'],
        ]);

        abort_unless($list->members->contains('id', $data['from_person_id']), 422);
        abort_unless($list->members->contains('id', $data['to_person_id']), 422);

        $amount = round((float) $data['amount'], 2);

        Expense::create([
            'id' => 'e_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'group_id' => '',
            'shared_list_id' => $list->id,
            'description' => 'List settlement',
            'amount' => $amount,
            'currency' => $user?->profile?->currency ?: 'USD',
            'paid_by_person_id' => $data['from_person_id'],
            'split_type' => 'custom',
            'split_among' => [
                ['userId' => $data['to_person_id'], 'amount' => $amount],
            ],
            'category' => 'settlement',
            'emoji' => '↔️',
            'expense_date' => $data['paid_on'],
            'is_recurring' => false,
        ]);

        Activity::create([
            'id' => 'a_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'activity_type' => 'settlement',
            'description' => 'recorded a list settlement',
            'i18n' => null,
            'user_person_id' => $data['from_person_id'],
            'amount' => $amount,
            'group_name' => null,
            'list_name' => $list->name,
            'activity_at' => now(),
        ]);

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function checkoutSharedList(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();

        $data = $request->validate([
            'paid_by_person_id' => ['required', 'string'],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'description' => ['nullable', 'string', 'max:200'],
            'receipt' => ['nullable', 'file', 'max:10240', 'mimes:jpg,jpeg,png,webp,pdf'],
        ]);

        abort_unless($list->members->contains('id', $data['paid_by_person_id']), 422);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        if ($request->hasFile('receipt')) {
            $path = $request->file('receipt')->store('receipts', 'public');
            $list->receipt_path = $path;
            $list->save();
        }

        $amount = round((float) $data['amount'], 2);
        $members = $list->members->values();
        $n = max(1, $members->count());
        $cents = (int) round($amount * 100);
        $base = intdiv($cents, $n);
        $rem = $cents - ($base * $n);
        $split = [];
        foreach ($members as $i => $m) {
            $split[] = [
                'userId' => $m->id,
                'amount' => ($base + ($i < $rem ? 1 : 0)) / 100,
            ];
        }

        Expense::create([
            'id' => 'e_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'group_id' => '',
            'shared_list_id' => $list->id,
            'description' => trim($data['description'] ?? 'Checkout'),
            'amount' => $amount,
            'currency' => $user?->profile?->currency ?: 'USD',
            'paid_by_person_id' => $data['paid_by_person_id'],
            'split_type' => 'equal',
            'split_among' => $split,
            'category' => 'shared_list',
            'emoji' => '🧾',
            'expense_date' => now()->toDateString(),
            'is_recurring' => false,
        ]);

        // Checkout means the list is "purchased": mark all items as done.
        SharedListItem::query()
            ->where('shared_list_id', $list->id)
            ->update(['done' => true]);

        Activity::create([
            'id' => 'a_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'activity_type' => 'expense',
            'description' => 'checked out a shared list',
            'i18n' => null,
            'user_person_id' => $data['paid_by_person_id'],
            'amount' => $amount,
            'group_name' => null,
            'list_name' => $list->name,
            'activity_at' => now(),
        ]);

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function settleSharedListReceiptAll(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $checkout = Expense::query()
            ->where('space_id', $spaceId)
            ->where('shared_list_id', $list->id)
            ->where('category', 'shared_list')
            ->orderBy('expense_date', 'desc')
            ->orderBy('id', 'desc')
            ->firstOrFail();

        $paidById = $checkout->paid_by_person_id;
        abort_unless($paidById && $list->members->contains('id', $paidById), 422);

        $amount = round((float) $checkout->amount, 2);
        $members = $list->members->values();
        $n = max(1, $members->count());
        $cents = (int) round($amount * 100);
        $base = intdiv($cents, $n);
        $rem = $cents - ($base * $n);

        foreach ($members as $i => $m) {
            if ($m->id === $paidById) {
                continue;
            }
            $share = ($base + ($i < $rem ? 1 : 0)) / 100;
            if ($share <= 0) {
                continue;
            }

            Expense::create([
                'id' => 'e_' . bin2hex(random_bytes(8)),
                'space_id' => $spaceId,
                'group_id' => '',
                'shared_list_id' => $list->id,
                'description' => 'List settlement',
                'amount' => $share,
                'currency' => $user?->profile?->currency ?: 'USD',
                'paid_by_person_id' => $m->id,
                'split_type' => 'custom',
                'split_among' => [
                    ['userId' => $paidById, 'amount' => $share],
                ],
                'category' => 'settlement',
                'emoji' => '↔️',
                'expense_date' => now()->toDateString(),
                'is_recurring' => false,
            ]);

            Activity::create([
                'id' => 'a_' . bin2hex(random_bytes(8)),
                'space_id' => $spaceId,
                'activity_type' => 'settlement',
                'description' => 'recorded a list settlement',
                'i18n' => null,
                'user_person_id' => $m->id,
                'amount' => $share,
                'group_name' => null,
                'list_name' => $list->name,
                'activity_at' => now(),
            ]);
        }

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function sharedPurchasesIntro(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;

        $me = $spaceId
            ? Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first()
            : null;

        $sharedPurchases = $spaceId
            ? SharedPurchase::query()
                ->where('space_id', $spaceId)
                ->when($me, fn ($q) => $q->whereHas('members', fn ($mq) => $mq->where('people.id', $me->id)))
                ->with(['members'])
                ->orderBy('created_at', 'desc')
                ->get()
            : collect();

        return view('app.shared-purchases-index', [
            'sharedPurchases' => $sharedPurchases,
        ]);
    }

    public function sharedPurchasesNew(): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();
        $people = Person::query()->where('space_id', $spaceId)->orderBy('created_at', 'asc')->get();

        return view('app.shared-purchases-new', [
            'mePerson' => $me,
            'people' => $people,
        ]);
    }

    public function sharedPurchaseDetail(string $id): View
    {
        $user = request()->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $purchase = SharedPurchase::query()
            ->where('space_id', $spaceId)
            ->where('id', $id)
            ->with(['members'])
            ->firstOrFail();

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $purchase->members->contains('id', $me->id), 403);

        return view('app.shared-purchase-detail', [
            'purchase' => $purchase,
        ]);
    }

    public function storeSharedList(Request $request)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();

        $data = $request->validate([
            'name' => ['required', 'string', 'min:1', 'max:80'],
            'emoji' => ['required', 'string', 'max:8'],
            'member_ids' => ['array'],
            'member_ids.*' => ['string'],
            'invite_email' => ['nullable', 'email', 'max:180'],
            'invite_avatar' => ['nullable', 'string', 'max:8'],
        ]);

        $id = 'sl_' . bin2hex(random_bytes(6));

        $list = SharedList::create([
            'id' => $id,
            'space_id' => $spaceId,
            'name' => trim($data['name']),
            'emoji' => $data['emoji'],
            'created_at' => now()->toDateString(),
        ]);

        $memberIds = collect($data['member_ids'] ?? [])->filter()->unique()->values();
        $memberIds->push($me->id);

        // Add invite email as a person + member.
        if (! empty($data['invite_email'])) {
            $email = strtolower(trim($data['invite_email']));
            $inviteAvatar = trim((string) ($data['invite_avatar'] ?? ''));
            $existing = Person::query()->where('space_id', $spaceId)->where('email', $email)->first();
            $invited = $existing ?: Person::create([
                'id' => 'p_' . bin2hex(random_bytes(6)),
                'space_id' => $spaceId,
                'auth_user_id' => null,
                'email' => $email,
                'name' => Str::of($email)->before('@')->toString(),
                'avatar' => ($inviteAvatar !== '' ? $inviteAvatar : '🙂'),
                'created_at' => now(),
            ]);
            $memberIds->push($invited->id);

            // If the invited email already belongs to an existing user, attach them to this space now.
            $this->adoptExistingUserIntoSpaceByEmail($spaceId, $email, $invited);
        }

        $people = Person::query()->where('space_id', $spaceId)->whereIn('id', $memberIds->all())->get();
        $list->members()->syncWithoutDetaching($people->pluck('id')->all());

        Activity::create([
            'id' => 'a_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'activity_type' => 'list_created',
            'description' => 'created a shared list',
            'i18n' => null,
            'user_person_id' => $me->id,
            'amount' => null,
            'group_name' => null,
            'list_name' => $list->name,
            'activity_at' => now(),
        ]);

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function addSharedListItem(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $data = $request->validate([
            'text' => ['required', 'string', 'min:1', 'max:120'],
        ]);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        SharedListItem::create([
            'id' => 'sli_' . bin2hex(random_bytes(6)),
            'shared_list_id' => $list->id,
            'item_text' => trim($data['text']),
            'amount' => null,
            'paid_by_person_id' => null,
            'expense_id' => null,
            'done' => false,
            'added_by_person_id' => $me?->id,
        ]);

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function toggleSharedListItem(Request $request, string $id, string $itemId)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $item = SharedListItem::query()->where('shared_list_id', $list->id)->where('id', $itemId)->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);
        $turningOn = ! $item->done;
        $item->done = $turningOn;

        // When an item is marked done and has an amount, create an Expense so it
        // impacts balances app-wide using the same math as groups/lists.
        if ($turningOn && $item->amount !== null && (float) $item->amount > 0) {
            $paidById = $item->paid_by_person_id;
            if ($paidById === null) {
                $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
                $paidById = $me?->id;
            }

            abort_unless($paidById && $list->members->contains('id', $paidById), 422);

            $amount = round((float) $item->amount, 2);
            $members = $list->members->values();
            $n = max(1, $members->count());
            $cents = (int) round($amount * 100);
            $base = intdiv($cents, $n);
            $rem = $cents - ($base * $n);
            $split = [];
            foreach ($members as $i => $m) {
                $split[] = [
                    'userId' => $m->id,
                    'amount' => ($base + ($i < $rem ? 1 : 0)) / 100,
                ];
            }

            $expenseId = 'e_' . bin2hex(random_bytes(8));
            Expense::create([
                'id' => $expenseId,
                'space_id' => $spaceId,
                'group_id' => '',
                'shared_list_id' => $list->id,
                'description' => trim($item->item_text),
                'amount' => $amount,
                'currency' => $user?->profile?->currency ?: 'USD',
                'paid_by_person_id' => $paidById,
                'split_type' => 'equal',
                'split_among' => $split,
                'category' => 'shared_list_item',
                'emoji' => '🛒',
                'expense_date' => now()->toDateString(),
                'is_recurring' => false,
            ]);

            $item->expense_id = $expenseId;
        }

        // If item is un-done, remove linked expense (if any).
        if (! $turningOn && $item->expense_id) {
            Expense::query()->where('space_id', $spaceId)->where('id', $item->expense_id)->delete();
            $item->expense_id = null;
        }

        $item->save();

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function updateSharedListItem(Request $request, string $id, string $itemId)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $item = SharedListItem::query()->where('shared_list_id', $list->id)->where('id', $itemId)->firstOrFail();

        $data = $request->validate([
            'text' => ['required', 'string', 'min:1', 'max:120'],
        ]);

        $item->item_text = trim($data['text']);
        $item->save();

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function deleteSharedListItem(Request $request, string $id, string $itemId)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $item = SharedListItem::query()->where('shared_list_id', $list->id)->where('id', $itemId)->firstOrFail();
        if ($item->expense_id) {
            Expense::query()->where('space_id', $spaceId)->where('id', $item->expense_id)->delete();
        }
        $item->delete();

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function updateSharedListNotes(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $list = SharedList::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $list->members->contains('id', $me->id), 403);

        $data = $request->validate([
            'notes' => ['nullable', 'string', 'max:2000'],
        ]);

        $list->notes = $data['notes'] ?? null;
        $list->save();

        return redirect()->route('app.shared-lists.detail', ['id' => $list->id]);
    }

    public function storeSharedPurchase(Request $request)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->firstOrFail();

        $data = $request->validate([
            'name' => ['required', 'string', 'min:1', 'max:80'],
            'emoji' => ['required', 'string', 'max:8'],
            'target_amount' => ['nullable', 'numeric', 'min:0.01'],
            'notes' => ['nullable', 'string', 'max:500'],
            'member_ids' => ['array'],
            'member_ids.*' => ['string'],
            'invite_email' => ['nullable', 'email', 'max:180'],
            'invite_avatar' => ['nullable', 'string', 'max:8'],
        ]);

        $id = 'sp_' . bin2hex(random_bytes(6));

        $purchase = SharedPurchase::create([
            'id' => $id,
            'space_id' => $spaceId,
            'name' => trim($data['name']),
            'emoji' => $data['emoji'],
            'status' => 'planning',
            'notes' => trim($data['notes'] ?? ''),
            'target_amount' => isset($data['target_amount']) ? round((float) $data['target_amount'], 2) : null,
            'created_at' => now()->toDateString(),
        ]);

        $memberIds = collect($data['member_ids'] ?? [])->filter()->unique()->values();
        $memberIds->push($me->id);

        if (! empty($data['invite_email'])) {
            $email = strtolower(trim($data['invite_email']));
            $inviteAvatar = trim((string) ($data['invite_avatar'] ?? ''));
            $existing = Person::query()->where('space_id', $spaceId)->where('email', $email)->first();
            $invited = $existing ?: Person::create([
                'id' => 'p_' . bin2hex(random_bytes(6)),
                'space_id' => $spaceId,
                'auth_user_id' => null,
                'email' => $email,
                'name' => Str::of($email)->before('@')->toString(),
                'avatar' => ($inviteAvatar !== '' ? $inviteAvatar : '🙂'),
                'created_at' => now(),
            ]);
            $memberIds->push($invited->id);

            // If the invited email already belongs to an existing user, attach them to this space now.
            $this->adoptExistingUserIntoSpaceByEmail($spaceId, $email, $invited);
        }

        $people = Person::query()->where('space_id', $spaceId)->whereIn('id', $memberIds->all())->get();
        $purchase->members()->syncWithoutDetaching($people->pluck('id')->all());

        Activity::create([
            'id' => 'a_' . bin2hex(random_bytes(8)),
            'space_id' => $spaceId,
            'activity_type' => 'purchase_created',
            'description' => 'created a shared purchase',
            'i18n' => null,
            'user_person_id' => $me->id,
            'amount' => $purchase->target_amount,
            'group_name' => null,
            'list_name' => null,
            'activity_at' => now(),
        ]);

        return redirect()->route('app.shared-purchases.detail', ['id' => $purchase->id]);
    }

    public function updateSharedPurchase(Request $request, string $id)
    {
        $user = $request->user();
        $spaceId = $user?->profile?->default_space_id;
        abort_unless($spaceId, 403);

        $purchase = SharedPurchase::query()->where('space_id', $spaceId)->where('id', $id)->with(['members'])->firstOrFail();
        $me = Person::query()->where('space_id', $spaceId)->where('auth_user_id', $user->id)->first();
        abort_unless($me && $purchase->members->contains('id', $me->id), 403);

        $data = $request->validate([
            'status' => ['nullable', 'in:planning,decided,bought'],
            'notes' => ['nullable', 'string', 'max:500'],
        ]);

        if (isset($data['status'])) {
            $purchase->status = $data['status'];
        }
        if (array_key_exists('notes', $data)) {
            $purchase->notes = trim($data['notes'] ?? '');
        }
        $purchase->save();

        return redirect()->route('app.shared-purchases.detail', ['id' => $purchase->id]);
    }
}
