<?php

namespace App\Http\Controllers;

use App\Models\Client;
use App\Models\ClientDocument;
use App\Models\AccountType;
use App\Models\ClientLocation;
use App\Models\ClientContract;
use App\Models\ClientContact;
use App\Models\Country;
use App\Models\CustomerMember;
use App\Models\Status;
use App\Models\Locker;
use App\Models\LockerGroup;
use App\Models\LockerRegistration;
use App\Models\LockerRegistrationItem;
use App\Models\MembershipPlan;
use App\Models\ReferredClient;
use App\Models\Payment;
use App\Models\Invoice;
use App\Models\AccountRefund;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use App\Models\LockerBlockLog;

class ClientController extends Controller
{
    public function index(Request $request)
    {
        $query = Client::with([
            'status',
            'customerMembers' => fn ($query) => $query->orderByDesc('is_active')->orderBy('id'),
            'accountType',

            // LOAD ONLY ACTIVE REGISTRATION
            'lockerRegistrations' => function ($q) {
                $q->where('status', 'Active')
                    ->with(['items', 'locker'])
                    ->latest(); // latest renewal
            }
        ])->byTenant();

        if ($request->filled('search')) {
            $search = trim((string) $request->search);
            $type = $request->input('type', 'name');

            if ($type === 'all') {
                $query->where(function ($query) use ($search) {
                    $query->where('name', 'like', "%{$search}%")
                        ->orWhereRaw("CONCAT(LPAD(id, GREATEST(4, CHAR_LENGTH(id)), '0'), '-', LPAD(account_type_id, 2, '0')) LIKE ?", ["%{$search}%"])
                        ->orWhereHas('customerMembers', function ($members) use ($search) {
                            $members->where(function ($members) use ($search) {
                                $members->where('name', 'like', "%{$search}%")
                                    ->orWhere('proof_number', 'like', "%{$search}%");
                            });
                        })
                        ->orWhereHas('lockerRegistrations', function ($registrations) use ($search) {
                            $registrations->where('status', 'Active')->where(function ($registrations) use ($search) {
                                $registrations->whereHas('items', fn($items) => $items->where('status', 'active')->where('locker_number', 'like', "%{$search}%"));
                            });
                        });
                });
            } elseif ($type === 'name') {
                $query->where('name', 'like', "%{$search}%");
            } elseif ($type === 'account_number') {
                $query->whereRaw("CONCAT(LPAD(id, GREATEST(4, CHAR_LENGTH(id)), '0'), '-', LPAD(account_type_id, 2, '0')) LIKE ?", ["%{$search}%"]);
            } elseif ($type === 'safebox_number') {
                $query->whereHas('lockerRegistrations', function ($registrations) use ($search) {
                    $registrations->where('status', 'Active')->whereHas('items', function ($items) use ($search) {
                        $items->where('status', 'active')->where('locker_number', 'like', "%{$search}%");
                    });
                });
            } elseif (in_array($type, ['ic_number', 'passport'], true)) {
                $query->whereHas('customerMembers', function ($memberQuery) use ($search, $type) {
                    $memberQuery->where('proof_type', $type)
                        ->where('proof_number', 'like', "%{$search}%");
                });
            }
        }

        if ($request->filled('status')) {
            $query->whereHas('status', function ($q) use ($request) {
                $q->where('name', $request->status);
            });
        }

        $clients = $query->orderBy('id', 'desc')->paginate(20)->withQueryString();

        return view('clients.index', compact('clients'));
    }

    public function referralDetails(Request $request)
    {
        $query = CustomerMember::with(['client', 'statusRelation'])
            ->whereHas('client', fn($client) => $client->byTenant())
            ->whereHas('referralRegistrations')
            ->withCount('referralRegistrations')
            ->withSum('referralRegistrations as referred_commission', 'referred_commission')
            ->withSum('referralRegistrations as commission_used', 'commission_used');

        if ($request->filled('search')) {
            $search = $request->search;
            $query->where(function ($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                    ->orWhere('mobile', 'like', "%{$search}%")
                    ->orWhere('proof_type', 'like', "%{$search}%")
                    ->orWhere('proof_number', 'like', "%{$search}%")
                    ->orWhereHas('client', function ($clientQuery) use ($search) {
                        $clientQuery->where('phone', 'like', "%{$search}%")
                            ->orWhere('name', 'like', "%{$search}%");
                    })
                    ->orWhereHas('referralRegistrations', function ($registrationQuery) use ($search) {
                        $registrationQuery->whereHas('client', function ($clientQuery) use ($search) {
                            $clientQuery->where('name', 'like', "%{$search}%")
                                ->orWhere('phone', 'like', "%{$search}%");
                        });
                    });
            });
        }

        if ($request->filled('status')) {
            if ($request->status === 'active') {
                $query->where('is_active', true);
            } elseif ($request->status === 'inactive') {
                $query->where('is_active', false);
            } elseif ($request->status === 'blocked') {
                $query->whereHas('statusRelation', function ($statusQuery) {
                    $statusQuery->where('name', 'blocked');
                });
            }
        }

        $summaryQuery = clone $query;
        $summary = [
            'total_members' => $summaryQuery->count(),
            'total_referral_amount' => (clone $summaryQuery)->get()->sum('referred_commission'),
            'total_commission_used' => (clone $summaryQuery)->get()->sum('commission_used'),
            // 'total_amount' =>
            'total_balance' => (clone $summaryQuery)->get()->sum('referred_commission') - (clone $summaryQuery)->get()->sum('commission_used'),
        ];

        $records = $query->orderBy('name')->paginate(20)->withQueryString();

        $status = $request->status;
        $search = $request->search;

        return view('clients.referral-details', compact('records', 'summary', 'status', 'search'));
    }

    public function create()
    {
        $statuses = \App\Models\Status::all();
        $lockers = \App\Models\Locker::all();
        $account_type = \App\Models\AccountType::where('status', 1)->get();
        $membership_plan = MembershipPlan::select('id', 'name', 'duration_days', 'duration_months', 'price')->where('status', 1)->get();
        $locker_groups = LockerGroup::select('id', 'name')->get();
        $countries = Country::select('id', 'name')->where('status', 1)->get();

        return view('clients.create-edit', compact('statuses', 'lockers', 'account_type', 'membership_plan', 'locker_groups', 'countries'));
    }

    public function store(Request $request)
    {
        $request->merge([
            'city' => preg_replace('/\s+/u', ' ', trim((string) $request->city)),
        ]);

        $accountTypeName = strtolower(trim((string) AccountType::whereKey($request->account_type)->value('type_name')));
        $documentRules = [];
        $documentMessages = [];
        $documentLabels = [
            'joint_consent_form' => 'Joint Account Consent Form',
            'ssm_document' => 'SSM Registration Document',
            'board_resolution' => 'Board Resolution',
            'agm_document' => 'AGM Document',
            'ros_approval' => 'ROS Approval Document',
            'change_resolution' => 'Change of Resolution',
        ];

        $requiredDocuments = match (true) {
            str_contains($accountTypeName, 'joint') => ['joint_consent_form'],
            str_contains($accountTypeName, 'corporate') => ['ssm_document', 'board_resolution'],
            str_contains($accountTypeName, 'trust') => ['agm_document', 'ros_approval'],
            default => [],
        };

        foreach ($requiredDocuments as $documentType) {
            $documentRules[$documentType] = 'required|file|mimes:pdf,jpg,jpeg,png|max:10240';
            $documentRules[$documentType . '_verified'] = 'required|accepted';
            $documentMessages[$documentType . '.mimes'] = 'The ' . $documentLabels[$documentType] . ' must be a PDF, JPG, JPEG, or PNG file.';
            $documentMessages[$documentType . '.max'] = 'The ' . $documentLabels[$documentType] . ' must not exceed 10 MB.';
            $documentMessages[$documentType . '_verified.accepted'] = 'Please confirm that the ' . $documentLabels[$documentType] . ' was received and checked.';
        }

        $validator = Validator::make($request->all(), array_merge([
            'account_type' => 'required|integer|exists:account_types,id',
            'auth_count' => 'required|numeric',
            'name' => 'required|string|max:255',
            'phone' => 'required|digits_between:10,11',
            'email' => 'nullable|email',
            'landline' => 'nullable|string|max:20',
            'address' => 'required|string|max:500',
            'country' => 'required|string|max:100',
            'state' => 'required|string|max:100',
            'city' => ['bail', 'required', 'string', 'min:2', 'max:100', 'regex:/^[\pL\pM\pN\s\.\'(),-]+$/u'],
            'postal_code' => 'required|string|max:10',
            'registration_number' => 'nullable|string|max:30',
            'tax_type' => 'required|string|in:SST,GST',
            'tax_amount' => 'nullable|numeric|min:0',
            'stamping_fee' => 'required|numeric',
            'key_deposit' => 'required|numeric',
            'insurance_charges' => 'required|numeric',
            'discount_amount' => 'required|numeric',
            'final_amount' => 'nullable|numeric|min:0',
            'payment_mode' => 'required|string|max:50',
            'safebox_count' => 'required|numeric',
            'selected_lockers_data' => 'required|string|json',
            'locker_charges' => 'required|array|size:' . (int) $request->safebox_count,
            'locker_charges.*.stamping_fee' => 'required|numeric|min:0|max:999999.99',
            'locker_charges.*.key_deposit' => 'required|numeric|min:0|max:999999.99',
            'locker_charges.*.insurance_charges' => 'required|numeric|min:0|max:999999.99',
            'locker_charges.*.discount_amount' => 'required|numeric|min:0|max:999999.99',
            'payment_terms' => 'required|string|max:50',
            // 'contract_start_date' => 'required|date|after_or_equal:today',
            'contract_start_date' => 'required|date',
            // 'contract_end_date' => 'required|date|after_or_equal:contract_start_date',
            'contract_end_date' => 'required|date',
            'referred_member_by' => 'nullable|string|max:50',
            'referred_commission' => 'nullable|numeric',
            'members' => 'required|array|size:' . (int) $request->auth_count,
            'members.*.name' => 'required|string|min:2|max:255',
            'members.*.gender' => 'required|string|in:male,female',
            'members.*.email' => 'nullable|email|distinct',
            'members.*.mobile' => 'required|digits_between:10,11|distinct',
            'members.*.proof_type' => 'required|string|in:passport,ic_number',
            'members.*.proof_number' => ['bail', 'required', 'string', 'distinct', function ($attribute, $value, $fail) use ($request) {
                $type = $request->input(str_replace('.proof_number', '.proof_type', $attribute));
                if ($type === 'ic_number' && !preg_match('/\A[0-9]{12}\z/', $value)) {
                    $fail('IC Number must be exactly 12 digits (e.g., 123456789012).');
                } elseif ($type === 'passport' && !preg_match('/\A[A-Za-z0-9]{6,12}\z/', $value)) {
                    $fail('Passport must be 6 to 12 letters or digits.');
                }
            }],
            'members.*.proof_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png,gif|max:5120',
            'members.*.address' => 'nullable|string|max:500',
            'members.*.finger_auth' => 'nullable|boolean',
            'members.*.face_auth' => 'required|accepted',
            'members.*.required_auth' => 'nullable|boolean',
            'members.*.existing_member_id' => 'nullable|integer|exists:customer_members,id',
        ], $documentRules), array_merge([
            'members.*.face_auth.accepted' => 'Face Authentication is required for every member.',
            'city.required' => 'Please enter the city.',
            'city.min' => 'The city must contain at least 2 characters.',
            'city.max' => 'The city must not exceed 100 characters.',
            'city.regex' => 'The city may only contain letters, numbers, spaces, periods, apostrophes, commas, parentheses, and hyphens.',
            'locker_charges.required' => 'Select lockers and enter charges for each locker.',
            'locker_charges.size' => 'Enter charges for every selected locker.',
            'locker_charges.*.stamping_fee.required' => 'Enter the stamping fee for every selected locker.',
            'locker_charges.*.stamping_fee.min' => 'Stamping fee cannot be negative.',
            'locker_charges.*.key_deposit.required' => 'Enter the key deposit for every selected locker.',
            'locker_charges.*.key_deposit.min' => 'Key deposit cannot be negative.',
            'locker_charges.*.insurance_charges.required' => 'Enter the insurance charge for every selected locker.',
            'locker_charges.*.insurance_charges.min' => 'Insurance charge cannot be negative.',
            'locker_charges.*.discount_amount.required' => 'Enter the discount for every selected locker.',
            'locker_charges.*.discount_amount.min' => 'Discount cannot be negative.',
        ], $documentMessages));

        $documentAttributes = [];
        foreach ($documentLabels as $field => $label) {
            $documentAttributes[$field] = $label;
            $documentAttributes[$field . '_verified'] = $label . ' confirmation';
        }
        $validator->setAttributeNames($documentAttributes);

        $validator->after(function ($validator) use ($request) {
            $selectedMemberIds = collect($request->input('members', []))
                ->pluck('existing_member_id')
                ->filter();

            if ($selectedMemberIds->count() !== $selectedMemberIds->unique()->count()) {
                $validator->errors()->add('members', 'The same existing member cannot be selected more than once.');
            }

            $selectedLockers = collect(json_decode((string) $request->selected_lockers_data, true));
            $selectedLockerIds = $selectedLockers->pluck('id')->map(fn ($id) => (string) $id)->sort()->values();
            $chargeLockerIds = collect(array_keys($request->input('locker_charges', [])))->map(fn ($id) => (string) $id)->sort()->values();
            if ($selectedLockerIds->isEmpty() || $selectedLockerIds->duplicates()->isNotEmpty()) {
                $validator->errors()->add('selected_lockers_data', 'Select the required number of unique lockers.');
            } elseif ($selectedLockerIds->all() !== $chargeLockerIds->all()) {
                $validator->errors()->add('locker_charges', 'Enter charges for every selected locker.');
            }
        });

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ], 422);
        }

        $uploadedMemberProofs = [];
        $uploadedClientDocuments = [];

        try {
            DB::beginTransaction();

            // Decode locker data
            $lockersData = json_decode($request->selected_lockers_data, true);

            if (!$lockersData || !is_array($lockersData)) {
                throw ValidationException::withMessages([
                    'selected_lockers_data' => 'Invalid locker selection. Select the lockers again.',
                ]);
            }

            // Validate each locker
            $years = max(1, (int) $request->payment_terms);
            $totalAmount = 0.0;
            $totalStampingFee = 0.0;
            $totalKeyDeposit = 0.0;
            $totalInsuranceCharges = 0.0;
            $totalDiscountAmount = 0.0;

            foreach ($lockersData as &$locker) {
                if (!isset($locker['id'], $locker['number'], $locker['size'], $locker['type'])) {
                    throw ValidationException::withMessages([
                        'selected_lockers_data' => 'Invalid locker details. Select the lockers again.',
                    ]);
                }

                $lockerRecord = \App\Models\Locker::whereKey($locker['id'])->lockForUpdate()->first();

                if (!$lockerRecord || strtolower((string) $lockerRecord->status) !== 'available') {
                    throw ValidationException::withMessages([
                        'selected_lockers_data' => "Locker {$locker['number']} is no longer available. Select another locker.",
                    ]);
                }

                $charges = $request->input('locker_charges.' . $lockerRecord->id, []);
                $rentalAmount = round((float) $lockerRecord->locker_amount * $years, 2);
                $discountAmount = round((float) ($charges['discount_amount'] ?? 0), 2);
                if ($discountAmount > $rentalAmount) {
                    throw ValidationException::withMessages([
                        "locker_charges.{$lockerRecord->id}.discount_amount" => "Discount for locker {$lockerRecord->locker_number} cannot exceed its rental amount.",
                    ]);
                }

                $locker['number'] = $lockerRecord->locker_number;
                $locker['size'] = $lockerRecord->locker_size;
                $locker['type'] = $lockerRecord->locker_type;
                $locker['amount'] = (float) $lockerRecord->locker_amount;
                $locker['rental_amount'] = $rentalAmount;
                $locker['stamping_fee'] = round((float) ($charges['stamping_fee'] ?? 0), 2);
                $locker['key_deposit'] = round((float) ($charges['key_deposit'] ?? 0), 2);
                $locker['insurance_charges'] = round((float) ($charges['insurance_charges'] ?? 0), 2);
                $locker['discount_amount'] = $discountAmount;

                $totalAmount += $rentalAmount;
                $totalStampingFee += $locker['stamping_fee'];
                $totalKeyDeposit += $locker['key_deposit'];
                $totalInsuranceCharges += $locker['insurance_charges'];
                $totalDiscountAmount += $discountAmount;
            }
            unset($locker);

            $subtotalAfterDiscount = max(0, $totalAmount - $totalDiscountAmount);
            $taxAmount = in_array($request->tax_type, ['SST', 'GST'], true)
                ? round($subtotalAfterDiscount * 0.08, 2)
                : 0.0;
            $paymentTotal = round(
                $subtotalAfterDiscount + $taxAmount + $totalStampingFee
                + $totalKeyDeposit + $totalInsuranceCharges,
                2
            );

            // Create Client
            $client = \App\Models\Client::create([
                'name' => $request->name,
                'phone' => $request->phone,
                'email' => $request->filled('email') ? $request->email : null,
                'landline' => $request->landline,
                'address' => $request->address,
                'country' => $request->country,
                'state' => $request->state,
                'city' => $request->city,
                'postal_code' => $request->postal_code,
                'account_type_id' => $request->account_type,
                'tax_type' => $request->tax_type,
                'registration_number' => $request->registration_number,
                'auth_count' => $request->auth_count,
                'is_appointment' => '1',
                'status_id' => '1',
                'tenant_id' => config('app.default_tenant_id', 1),
            ]);

            foreach ($requiredDocuments as $documentType) {
                $path = $request->file($documentType)->store("client-documents/{$client->id}", 'local');

                if (!$path) {
                    throw new \RuntimeException("Unable to store {$documentLabels[$documentType]}.");
                }

                $uploadedClientDocuments[] = $path;
                ClientDocument::create([
                    'client_id' => $client->id,
                    'document_type' => $documentType,
                    'file_path' => $path,
                    'is_verified' => true,
                ]);
            }

            // Create ONE Registration
            $registration = \App\Models\LockerRegistration::create([
                'client_id' => $client->id,
                'auth_count' => $request->auth_count,
                'locker_password' => '123456',
                'registration_date' => now()->toDateString(),
                'safebox_count' => $request->safebox_count,
                'contract_start_date' => $request->contract_start_date,
                'contract_end_date' => $request->contract_end_date,
                'payment_terms' => $request->payment_terms,
                'stamping_fee' => $totalStampingFee,
                'key_deposit' => $totalKeyDeposit,
                'insurance_charges' => $totalInsuranceCharges,
                'discount_amount' => $totalDiscountAmount,
                'tax_amount' => $taxAmount,
                'referred_member_by' => $request->referred_member_by,
                'referred_commission' => $request->referred_commission,
                'status' => 'Active',
            ]);

            // Insert Locker Items
            foreach ($lockersData as $locker) {

                \App\Models\LockerRegistrationItem::create([
                    'registration_id' => $registration->id,
                    'locker_id' => $locker['id'],

                    'locker_number' => $locker['number'],
                    'size' => $locker['size'],
                    'type' => $locker['type'],
                    'locker_group' => $locker['locker_group'] ?? null,
                    'locker_position' => $locker['locker_position'] ?? null,

                    'start_date' => $request->contract_start_date,
                    'end_date' => $request->contract_end_date,

                    'amount' => $locker['rental_amount'],
                    'stamping_fee' => $locker['stamping_fee'],
                    'key_deposit' => $locker['key_deposit'],
                    'insurance_charges' => $locker['insurance_charges'],
                    'discount_amount' => $locker['discount_amount'],

                    'status' => 'active',
                    'added_at' => now(),
                ]);

                // Update locker status
                \App\Models\Locker::where('id', $locker['id'])
                    ->update(['status' => 'Occupied']);

            }

            foreach ($request->input('members', []) as $index => $memberData) {
                $proofFilePath = null;
                $proofFile = $request->file("members.{$index}.proof_file");
                $existingMember = !empty($memberData['existing_member_id'])
                    ? CustomerMember::findOrFail($memberData['existing_member_id'])
                    : null;

                if (!$existingMember) {
                    $duplicateIdentity = CustomerMember::where('proof_type', $memberData['proof_type'])
                        ->whereRaw(
                            "UPPER(REPLACE(REPLACE(proof_number, '-', ''), ' ', '')) = ?",
                            [strtoupper(str_replace(['-', ' '], '', $memberData['proof_number']))]
                        )
                        ->exists();

                    if ($duplicateIdentity) {
                        throw \Illuminate\Validation\ValidationException::withMessages([
                            "members.{$index}.proof_number" => 'This identity already exists. Search and select the existing member.',
                        ]);
                    }
                }

                if ($proofFile) {
                    $filename = time() . '_' . $index . '_' . preg_replace('/\s+/', '_', $proofFile->getClientOriginalName());
                    $uploadDir = public_path('uploads/member-proofs');

                    if (!file_exists($uploadDir)) {
                        mkdir($uploadDir, 0755, true);
                    }

                    $proofFile->move($uploadDir, $filename);
                    $proofFilePath = 'uploads/member-proofs/' . $filename;
                    $uploadedMemberProofs[] = public_path($proofFilePath);
                } elseif ($existingMember && $existingMember->proof_file && file_exists(public_path($existingMember->proof_file))) {
                    $uploadDir = public_path('uploads/member-proofs');
                    if (!file_exists($uploadDir)) {
                        mkdir($uploadDir, 0755, true);
                    }
                    $extension = pathinfo($existingMember->proof_file, PATHINFO_EXTENSION);
                    $filename = time() . '_' . $index . '_existing_member_proof.' . $extension;
                    $proofFilePath = 'uploads/member-proofs/' . $filename;
                    copy(public_path($existingMember->proof_file), public_path($proofFilePath));
                    $uploadedMemberProofs[] = public_path($proofFilePath);
                }

                // Existing data is used to prefill the form, but submitted edits belong
                // only to this new customer's member record.
                $identity = (object) $memberData;

                $client->customerMembers()->create([
                    'name' => $identity->name,
                    'gender' => $identity->gender,
                    'email' => !empty($identity->email) ? $identity->email : null,
                    'mobile' => $identity->mobile,
                    'proof_type' => $identity->proof_type,
                    'proof_number' => $identity->proof_number,
                    'proof_file' => $proofFilePath,
                    'address' => $identity->address ?? null,
                    'finger_auth' => (bool) ($memberData['finger_auth'] ?? false),
                    'face_auth' => (bool) $memberData['face_auth'],
                    'required_auth' => (bool) ($memberData['required_auth'] ?? false),
                    'status' => 2,
                    'is_active' => true,
                ]);
            }

            $firstLocker = $lockersData[0] ?? null;

            // Create Invoice
            $invoice = \App\Models\Invoice::create([
                'client_id' => $client->id,
                'registration_id' => $registration->id,
                'invoice_number' => \App\Models\Invoice::generateInvoiceNumber(),
                'invoice_date' => now(),
                'due_date' => now()->addDays(30),
                'tax_amount' => $taxAmount,
                'stamping_fee' => $totalStampingFee,
                'key_deposit' => $totalKeyDeposit,
                'insurance_charges' => $totalInsuranceCharges,
                'discount_amount' => $totalDiscountAmount,
                'subtotal' => $totalAmount,
                'total_amount' => $paymentTotal,
                'paid_amount' => 0,
                'balance_amount' => $paymentTotal,
                'payment_mode' => $request->payment_mode ?? 'cash',
                'status' => 'pending',
                'payment_type' => $request->payment_type ?? 'create',
                'client_name' => $client->name,
                'client_phone' => $client->phone,
                'client_address' => $client->address,
                'membership_plan' => $registration->membershipPlan->name ?? '',
                'contract_start_date' => $request->contract_start_date,
                'contract_end_date' => $request->contract_end_date,
                'notes' => 'create',
                // New database columns
                'account_type_id' => $request->account_type_id,
                'email' => $client->email ?? '',
                'safebox' => $client->safebox ?? '',
                'postal_code' => $client->postal_code ?? 0,
                'city' => $client->city ?? '',
                'state' => $client->state ?? '',
                'country' => $client->country ?? '',
                'registration_number' => $client->registration_number ?? '',
                'tax_type' => $request->tax_type ?? 'gst',
                'safebox_count' => $request->safebox_count ?? 1,
                'payment_terms' => $request->payment_terms ?? 30,
                'referred_member_by' => $request->referred_member_by,
                'referred_commission' => $request->referred_commission ?? 0,
                'commission_used' => $request->commission_used ?? 0,
                'registration_date' => $registration->registration_date ?? now(),
                'locker_id' => $firstLocker['id'] ?? 0,
                'locker_number' => collect($lockersData)->pluck('number')->implode(', '),
                'size' => $firstLocker['size'] ?? '',
                'type' => $firstLocker['type'] ?? '',
                'locker_group' => $firstLocker['locker_group'] ?? '',
                'locker_position' => $firstLocker['locker_position'] ?? '',
                'start_date' => $request->start_date ?? now(),
                'end_date' => $request->end_date ?? now()->addYear(),
                // 'amount' => $request->amount ?? 0,
                'amount' => $totalAmount,
                'items_data' => collect($lockersData)->map(fn ($locker) => [
                    'locker_id' => $locker['id'],
                    'locker_number' => $locker['number'],
                    'size' => $locker['size'],
                    'type' => $locker['type'],
                    'amount' => $locker['amount'] ?? 0,
                    'stamping_fee' => $locker['stamping_fee'] ?? 0,
                    'key_deposit' => $locker['key_deposit'] ?? 0,
                    'insurance_charges' => $locker['insurance_charges'] ?? 0,
                    'discount_amount' => $locker['discount_amount'] ?? 0,
                ])->values()->all(),
            ]);

            DB::commit();

            return response()->json([
                'success' => true,
                'message' => 'Customer, locker, and members registered successfully',
                'redirect' => route('customers.invoice.show', [
                    'client' => $client->id,
                    'registration' => $registration->id,
                    'invoice_id' => $invoice->id,
                ])
            ]);
        } catch (\Illuminate\Validation\ValidationException $e) {
            DB::rollBack();

            foreach ($uploadedMemberProofs as $uploadedMemberProof) {
                if (file_exists($uploadedMemberProof)) {
                    unlink($uploadedMemberProof);
                }
            }

            Storage::disk('local')->delete($uploadedClientDocuments);

            return response()->json([
                'success' => false,
                'errors' => $e->errors(),
            ], 422);
        } catch (\Exception $e) {
            DB::rollBack();

            foreach ($uploadedMemberProofs as $uploadedMemberProof) {
                if (file_exists($uploadedMemberProof)) {
                    unlink($uploadedMemberProof);
                }
            }

            Storage::disk('local')->delete($uploadedClientDocuments);

            return response()->json([
                'success' => false,
                'message' => 'Error: ' . $e->getMessage()
            ], 500);
        }
    }

    public function show($id)
    {
        $client = Client::with([
            'lockerRegistrations' => fn ($query) => $query->latest(),
            'lockerRegistrations.locker',
            'lockerRegistrations.membershipPlan',
            'lockerRegistrations.items',
            'lockerRegistrations.payments',
            'invoices' => fn ($query) => $query->with('payments')->latest('invoice_date')->latest('id'),
            'customerMembers' => fn ($query) => $query->orderByDesc('is_active')->orderBy('id'),
            'documents' => fn ($query) => $query->latest(),
        ])
            ->findOrFail($id);

        // Surface closure blockers before the form is submitted so staff can
        // resolve outstanding invoices/payments without completing the form first.
        // closeAccount() calculates them again at submission time for safety.
        $closureBlockers = $client->isClosed() ? [] : $this->accountClosureBlockers($client);

        return view('clients.show', compact('client', 'closureBlockers'));
    }

    public function viewDocument($clientId, $documentId)
    {
        $document = ClientDocument::where('client_id', $clientId)->findOrFail($documentId);
        $expectedDirectory = 'client-documents/' . $clientId . '/';

        abort_unless(str_starts_with($document->file_path, $expectedDirectory), 404);
        abort_unless(Storage::disk('local')->exists($document->file_path), 404, 'Document file not found.');

        $labels = [
            'joint_consent_form' => 'Joint Account Consent Form',
            'ssm_document' => 'SSM Registration Document',
            'board_resolution' => 'Board Resolution',
            'agm_document' => 'AGM Document',
            'ros_approval' => 'ROS Approval Document',
        ];
        $extension = pathinfo($document->file_path, PATHINFO_EXTENSION);
        $filename = ($labels[$document->document_type] ?? str($document->document_type)->headline())
            . ($extension ? '.' . $extension : '');

        return response()->file(
            Storage::disk('local')->path($document->file_path),
            ['Content-Disposition' => 'inline; filename="' . addslashes($filename) . '"']
        );
    }

    public function renewal($clientId, $registrationId)
    {
        $client = Client::findOrFail($clientId);
        if ($resp = $this->closedClientJsonOrRedirect($client)) return $resp;
        $registration = LockerRegistration::with(['locker', 'membershipPlan', 'items' => function ($query) {
            $query->where('status', 'active');
        }, 'items.locker'])
            ->where('id', $registrationId)
            ->where('status', 'Active')
            ->where('client_id', $clientId)
            ->firstOrFail();

        $membership_plan = MembershipPlan::select('id', 'name', 'duration_days', 'duration_months', 'price')
            ->where('status', 1)
            ->orderBy('sort_order')
            ->get();

        $penaltyRules = \App\Models\LateRenewalPenalty::where('status', true)
            ->get()
            ->keyBy('membership_plan_id');
        $expiryDate = $registration->contract_end_date
            ? \Carbon\Carbon::parse($registration->contract_end_date)->startOfDay()
            : null;
        $daysOverdue = $expiryDate && today()->gt($expiryDate) ? $expiryDate->diffInDays(today()) : 0;
        $renewalLockerCount = max(1, $registration->items->count() ?: (int) $registration->safebox_count);

        return view('clients.renewal', compact('client', 'registration', 'membership_plan', 'penaltyRules', 'daysOverdue', 'renewalLockerCount'));
    }

    public function createLocker($id)
    {
        $client = Client::byTenant()->findOrFail($id);
        if ($resp = $this->closedClientJsonOrRedirect($client)) return $resp;
        $membership_plan = MembershipPlan::select('id', 'name', 'duration_days', 'duration_months', 'price')
            ->where('status', 1)
            ->get();
        $locker_groups = LockerGroup::select('id', 'name')->get();

        return view('clients.add-locker', compact('client', 'membership_plan', 'locker_groups'));
    }

    public function changeLocker($id, Request $request)
    {
        $client = Client::byTenant()->with([
            'lockerRegistrations' => function ($query) {
                $query->where('status', 'Active')->latest();
            },
            'lockerRegistrations.items' => function ($query) {
                $query->where('status', 'active');
            },
            'lockerRegistrations.items.locker',
            'lockerRegistrations.payments' => function ($query) {
                $query->latest();
            }
        ])->findOrFail($id);
        if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;
        if ($blocker = $this->lockerChangeBlocker($client)) {
            return redirect()->route('customers.show', $client->id)->with('error', $blocker);
        }
        $membership_plan = MembershipPlan::select('id', 'name', 'duration_days', 'duration_months', 'price')
            ->where('status', 1)
            ->get();
        $locker_groups = LockerGroup::select('id', 'name')->get();

        $taxAmount = (float) ($request->tax_amount ?? 0);
        $finalAmount = (float) ($request->final_amount ?? 0);
        $totalAmount = 0; // Initialize total amount
        $paymentTotal = $finalAmount > 0 ? $finalAmount : ($totalAmount + $taxAmount);

        // Extract locker data from client's existing registrations
        $lockersData = [];
        $firstLocker = null;
        $registration = null;
        if ($client->lockerRegistrations->isNotEmpty()) {
            $registration = $client->lockerRegistrations->first();
            foreach ($registration->items as $item) {
                if ($item->locker) {
                    $lockerData = [
                        'id' => $item->locker->id,
                        'number' => $item->locker_number,
                        'size' => $item->size,
                        'type' => $item->type,
                        'locker_group' => $item->locker_group,
                        'locker_position' => $item->locker_position,
                    ];
                    $lockersData[] = $lockerData;
                    if ($firstLocker === null) {
                        $firstLocker = $lockerData;
                    }
                }
            }
        }

        $invoicePreview = [

            'client_id' => $client->id,

            'registration_id' => $registration->id,

            'invoice_number' => \App\Models\Invoice::generateInvoiceNumber(),

            'invoice_date' => now(),

            'due_date' => now()->addDays(30),

            'tax_amount' => $taxAmount,

            'stamping_fee' => $request->stamping_fee ?? 0,

            'key_deposit' => $request->key_deposit ?? 0,

            'insurance_charges' => $request->insurance_charges ?? 0,

            'discount_amount' => $request->discount_amount ?? 0,

            'total_amount' => $paymentTotal,

            'paid_amount' => 0,

            'balance_amount' => $paymentTotal,

            'payment_mode' => $request->payment_mode ?? 'cash',

            'status' => 'pending',

            'payment_type' => $request->payment_type ?? 'change',

            'client_name' => $client->name,

            'client_phone' => $client->phone,

            'client_address' => $client->address,

            'membership_plan' => $registration->membershipPlan->name ?? '',

            'contract_start_date' => $request->contract_start_date ?? now(),

            'contract_end_date' => $request->contract_end_date ?? now()->addYear(),

            'notes' => 'change',

            // New database columns

            'account_type_id' => $request->account_type_id ?? 1,

            'email' => $client->email ?? '',

            'safebox' => $client->safebox ?? '',

            'postal_code' => $client->postal_code ?? 0,

            'city' => $client->city ?? '',

            'state' => $client->state ?? '',

            'country' => $client->country ?? '',

            'registration_number' => $client->registration_number ?? '',

            'tax_type' => $request->tax_type ?? 'gst',

            'safebox_count' => $request->safebox_count ?? 1,

            'payment_terms' => $request->payment_terms ?? 30,

            'referred_member_by' => $request->referred_member_by,

            'referred_commission' => $request->referred_commission ?? 0,

            'commission_used' => $request->commission_used ?? 0,

            'registration_date' => $registration->registration_date ?? now(),

            'locker_id' => $firstLocker['id'] ?? 0,

            'locker_number' => collect($lockersData)->pluck('number')->implode(', '),

            'size' => $firstLocker['size'] ?? '',

            'type' => $firstLocker['type'] ?? '',

            'locker_group' => $firstLocker['locker_group'] ?? '',

            'locker_position' => $firstLocker['locker_position'] ?? '',

            'start_date' => $request->start_date ?? now(),

            'end_date' => $request->end_date ?? now()->addYear(),

            'amount' => $request->amount ?? 0,

        ];

        // Get the latest payment for payment mode
        $payments = null;
        if ($client->lockerRegistrations->isNotEmpty()) {
            $latestRegistration = $client->lockerRegistrations->first();
            if ($latestRegistration->payments->isNotEmpty()) {
                $payments = $latestRegistration->payments->first();
            }
        }
        return view('clients.change-locker', compact('client', 'membership_plan', 'locker_groups', 'payments', 'invoicePreview'));
    }

    public function storeLocker(Request $request, $id)
    {
        $client = Client::byTenant()->findOrFail($id);
        if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;
        $validator = Validator::make($request->all(), [
            'safebox_count' => 'required|numeric|min:1',
            'selected_lockers_data' => 'required|string|json',
            'payment_terms' => 'required|string|max:50',
            'contract_start_date' => 'required|date',
            'contract_end_date' => 'nullable|date|after_or_equal:contract_start_date',
            'stamping_fee' => 'required|numeric',
            'key_deposit' => 'required|numeric',
            'insurance_charges' => 'required|numeric',
            'discount_amount' => 'required|numeric',
            'tax_amount' => 'required|numeric',
            'payment_mode' => 'required|string|max:50',
            'referred_member_by' => 'nullable|string|max:50',
            'referred_commission' => 'nullable|numeric',
        ]);

        if ($validator->fails()) {
            return redirect()->back()->withErrors($validator)->withInput();
        }

        try {
            DB::beginTransaction();

            $lockersData = json_decode($request->selected_lockers_data, true);

            if (!$lockersData || !is_array($lockersData)) {
                return redirect()->back()->with('error', 'Invalid locker selection format.');
            }

            foreach ($lockersData as $locker) {
                if (!isset($locker['id'], $locker['number'], $locker['size'], $locker['type'])) {
                    return redirect()->back()->with('error', 'Invalid locker data.');
                }

                $lockerRecord = \App\Models\Locker::find($locker['id']);
                if (!$lockerRecord || $lockerRecord->status !== 'Available') {
                    return redirect()->back()->with('error', "Locker {$locker['number']} is not available.");
                }
            }

            $registration = \App\Models\LockerRegistration::create([
                'client_id' => $client->id,
                'auth_count' => $client->auth_count ?? 0,
                'locker_password' => '123456',
                'registration_date' => now()->toDateString(),
                'safebox_count' => $request->safebox_count,
                'contract_start_date' => $request->contract_start_date,
                'contract_end_date' => $request->contract_end_date,
                'payment_terms' => $request->payment_terms,
                'stamping_fee' => $request->stamping_fee,
                'key_deposit' => $request->key_deposit,
                'insurance_charges' => $request->insurance_charges,
                'discount_amount' => $request->discount_amount,
                'tax_amount' => $request->tax_amount,
                'referred_member_by' => $request->referred_member_by,
                'referred_commission' => $request->referred_commission,
                'status' => 'Active',
            ]);

            $totalAmount = 0;
            foreach ($lockersData as $locker) {
                \App\Models\LockerRegistrationItem::create([
                    'registration_id' => $registration->id,
                    'locker_id' => $locker['id'],
                    'locker_number' => $locker['number'],
                    'size' => $locker['size'],
                    'type' => $locker['type'],
                    'locker_group' => $locker['locker_group'] ?? null,
                    'locker_position' => $locker['locker_position'] ?? null,
                    'start_date' => $request->contract_start_date,
                    'end_date' => $request->contract_end_date,
                    'amount' => $locker['amount'] ?? 0,
                    'stamping_fee' => $locker['stamping_fee'] ?? ((float) $request->stamping_fee / max(1, count($lockersData))),
                    'key_deposit' => $locker['key_deposit'] ?? ((float) $request->key_deposit / max(1, count($lockersData))),
                    'insurance_charges' => $locker['insurance_charges'] ?? ((float) $request->insurance_charges / max(1, count($lockersData))),
                    'discount_amount' => $locker['discount_amount'] ?? ((float) $request->discount_amount / max(1, count($lockersData))),
                    'status' => 'active',
                    'added_at' => now(),
                ]);

                \App\Models\Locker::find($locker['id'])?->update(['status' => 'Occupied']);
                $totalAmount += (float) ($locker['amount'] ?? 0);
            }

            $paymentTotal = (float) ($request->final_amount ?? 0);
            if ($paymentTotal <= 0) {
                $paymentTotal = $totalAmount + (float) ($request->tax_amount ?? 0);
            }

            // Keep the invoice's legacy single-locker columns populated for
            // schemas where these fields are still required. All lockers are
            // represented by the registration items and locker_number list.
            $firstLocker = $lockersData[0];

            $invoice = Invoice::create([
                'invoice_number' => Invoice::generateInvoiceNumber(),
                'client_id' => $client->id,
                'registration_id' => $registration->id,
                'invoice_date' => now(),
                'payment_type' => 'add',
                'status' => 'pending',
                'paid_amount' => 0,
                'balance_amount' => $paymentTotal,
                'total_amount' => $paymentTotal,
                'amount' => $totalAmount,
                'payment_mode' => $request->payment_mode,
                'tax_type' => $request->tax_type,
                'tax_amount' => $request->tax_amount,
                'stamping_fee' => $request->stamping_fee,
                'key_deposit' => $request->key_deposit,
                'insurance_charges' => $request->insurance_charges,
                'discount_amount' => $request->discount_amount,
                'client_name' => $client->name,
                'client_phone' => $client->phone,
                'email' => $client->email,
                'client_address' => $client->address,
                'account_type_id' => $client->account_type_id,
                'membership_plan' => $registration->membershipPlan->name ?? '',
                'payment_terms' => $request->payment_terms,
                'contract_start_date' => $request->contract_start_date,
                'contract_end_date' => $request->contract_end_date,
                'safebox_count' => count($lockersData),
                'locker_number' => collect($lockersData)->pluck('number')->implode(', '),
                'locker_id' => $firstLocker['id'],
                'size' => $firstLocker['size'],
                'type' => $firstLocker['type'],
                'locker_group' => $firstLocker['locker_group'] ?? '',
                'locker_position' => $firstLocker['locker_position'] ?? '',
                'start_date' => $request->contract_start_date,
                'end_date' => $request->contract_end_date,
                'notes' => 'add',
                'items_data' => collect($lockersData)->map(fn ($locker) => [
                    'locker_id' => $locker['id'],
                    'locker_number' => $locker['number'],
                    'size' => $locker['size'],
                    'type' => $locker['type'],
                    'amount' => $locker['amount'] ?? 0,
                ])->values()->all(),
            ]);
            DB::commit();

            return redirect()->route('customers.invoice.show', ['client' => $client->id, 'registration' => $registration->id, 'invoice_id' => $invoice->id])->with('success', 'Locker added successfully. Invoice is pending payment.');
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error('Error adding locker for client ' . $id . ': ' . $e->getMessage());

            return redirect()->back()->with('error', 'Unable to add locker. Please try again.');
        }
    }

    public function updateLocker(Request $request, $id)
    {
        $client = Client::byTenant()->findOrFail($id);
        if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;
        if ($blocker = $this->lockerChangeBlocker($client)) {
            return redirect()->route('customers.show', $client->id)->with('error', $blocker);
        }

        $validator = Validator::make($request->all(), [
            'existing_locker_item_id' => 'required|integer|exists:locker_registration_items,id',
            'selected_lockers_data' => 'required|string|json',
            'tax_type' => 'required|string|max:50',
            'stamping_fee' => 'required|numeric|min:0',
            'insurance_charges' => 'required|numeric|min:0',
            'discount_amount' => 'required|numeric|min:0',
            'key_deposit' => 'required|numeric|min:0',
            'old_key_returned' => 'required|accepted',
            'refund_bank_name' => 'nullable|string|max:150',
            'refund_account_name' => 'nullable|string|max:150',
            'refund_account_number' => 'nullable|string|min:4|max:100',
            'refund_ifsc_code' => 'nullable|string|min:5|max:30',
            'payment_mode' => 'required|string|max:50',
            'contract_start_date' => 'nullable|date',
            'contract_end_date' => 'nullable|date|after_or_equal:contract_start_date',
            'reason' => 'nullable|string|max:1000',
        ]);

        if ($validator->fails()) {
            return redirect()->back()->withErrors($validator)->withInput();
        }

        try {

            DB::beginTransaction();

            // Existing locker item
            $existingItem = LockerRegistrationItem::with(['registration', 'locker'])
                ->whereKey($request->existing_locker_item_id)
                ->lockForUpdate()
                ->firstOrFail();

            // Verify ownership
            if ((int) $existingItem->registration->client_id !== (int) $client->id
                || strtolower(trim((string) $existingItem->registration->status)) !== 'active'
                || strtolower(trim((string) $existingItem->status)) !== 'active') {
                throw ValidationException::withMessages(['locker' => 'The selected locker is no longer active for this customer.']);
            }

            $registration = $existingItem->registration;

            // OLD locker details backup
            $oldLockerDetails = [
                'locker_id' => $existingItem->locker_id,
                'locker_number' => $existingItem->locker_number,
                'amount' => $existingItem->amount,
                'size' => $existingItem->size,
                'type' => $existingItem->type,
                'locker_group' => $existingItem->locker_group,
                'locker_position' => $existingItem->locker_position,
            ];

            // OLD registration data backup
            $oldRegistrationData = [
                'stamping_fee' => $registration->stamping_fee,
                'insurance_charges' => $registration->insurance_charges,
                'discount_amount' => $registration->discount_amount,
                'key_deposit' => $registration->key_deposit,
                'payment_terms' => $registration->payment_terms,
                'contract_start_date' => $registration->contract_start_date,
                'contract_end_date' => $registration->contract_end_date,
            ];

            // OLD tax type backup
            $oldTaxType = $client->tax_type;

            // Selected lockers decode
            $selectedLockersData = json_decode($request->selected_lockers_data, true);

            if (
                !$selectedLockersData ||
                !is_array($selectedLockersData) ||
                count($selectedLockersData) !== 1
            ) {
                throw ValidationException::withMessages(['locker' => 'Select exactly one replacement locker.']);
            }

            // First locker
            $firstSelectedLocker = $selectedLockersData[0];

            $newLocker = Locker::whereKey($firstSelectedLocker['id'])->lockForUpdate()->firstOrFail();

            if ($newLocker->status !== 'Available') {
                throw ValidationException::withMessages(['locker' => "Locker {$newLocker->locker_number} is not available."]);
            }

            if ($blocker = $this->lockerChangeBlocker($client, true)) {
                throw ValidationException::withMessages(['locker' => $blocker]);
            }

            // Free old locker
            Locker::where('id', $existingItem->locker_id)
                ->update([
                    'status' => 'Available'
                ]);

            // Mark old item changed
            $existingItem->update([
                'status' => 'changed',
                'removed_at' => now(),
            ]);

            // Dates
            $startDate = $request->contract_start_date
                ? \Carbon\Carbon::parse($request->contract_start_date)
                : today();

            $endDate = $request->contract_end_date
                ? \Carbon\Carbon::parse($request->contract_end_date)
                : (
                    $existingItem->end_date
                    ?? (
                        $registration->contract_end_date
                        ? \Carbon\Carbon::parse($registration->contract_end_date)
                        : now()->addYear()
                    )
                );

            $existingItem->update(['end_date' => $startDate->toDateString()]);

            $plan = MembershipPlan::find($registration->payment_terms);
            $planYears = $plan?->duration_months
                ? max(1, (int) ceil($plan->duration_months / 12))
                : max(1, (int) ceil(($plan?->duration_days ?? 365) / 365));
            $newLockerAmount = round((float) $newLocker->locker_amount * $planYears, 2);
            if ((float) $request->discount_amount > $newLockerAmount) {
                throw ValidationException::withMessages([
                    'discount_amount' => 'Discount cannot exceed the replacement locker amount of RM '
                        .number_format($newLockerAmount, 2).'.',
                ]);
            }

            $depositService = app(\App\Services\LockerDepositService::class);
            $oldDeposit = $depositService->requireUnambiguousBalance($existingItem);
            $newDeposit = round((float) $request->key_deposit, 2);
            $depositTransferred = min($oldDeposit, $newDeposit);
            $depositCharge = max(0, round($newDeposit - $oldDeposit, 2));
            $depositRefund = max(0, round($oldDeposit - $newDeposit, 2));

            if ($depositRefund > 0 && (! $request->filled('refund_bank_name')
                || ! $request->filled('refund_account_name')
                || ! $request->filled('refund_account_number')
                || ! $request->filled('refund_ifsc_code'))) {
                throw ValidationException::withMessages([
                    'refund_bank_name' => 'Bank details are required because RM '
                        .number_format($depositRefund, 2).' must be refunded.',
                ]);
            }

            // Create NEW locker item
            $newItem = LockerRegistrationItem::create([
                'registration_id' => $registration->id,
                'locker_id' => $newLocker->id,
                'locker_number' => $newLocker->locker_number,
                'size' => $newLocker->locker_size,
                'type' => $newLocker->locker_type,
                'locker_group' => $newLocker->locker_group_id,
                'locker_position' => $newLocker->locker_row_column,
                'amount' => $newLockerAmount,
                'stamping_fee' => $request->stamping_fee ?? 0,
                'key_deposit' => $newDeposit,
                'insurance_charges' => $request->insurance_charges ?? 0,
                'discount_amount' => $request->discount_amount ?? 0,
                'status' => 'active',
                'start_date' => $startDate,
                'end_date' => $endDate,
                'added_at' => now(),
            ]);

            // Occupy new locker
            Locker::where('id', $newLocker->id)
                ->update([
                    'status' => 'Occupied'
                ]);

            // Update client tax type
            $client->update([
                'tax_type' => $request->tax_type,
            ]);

            // Tax
            $taxAmount = (
                strtoupper($request->tax_type) === 'GST'
                || strtoupper($request->tax_type) === 'SST'
            )
                ? (
                    ($newLockerAmount - ($request->discount_amount ?? 0)) * 0.08
                )
                : 0;

            // Final amount
            $finalAmount =
                $newLockerAmount
                + $taxAmount
                + ($request->stamping_fee ?? 0)
                + ($request->insurance_charges ?? 0)
                + $depositCharge
                - ($request->discount_amount ?? 0);

            if ($finalAmount < 0.01) {
                $finalAmount = 0.01;
            }


            // Keep prior invoices and payments unchanged; bill the change separately.
            $payment = $registration->payments()->latest()->first();
            $invoice = Invoice::create([
                'client_id' => $client->id,
                'registration_id' => $registration->id,

                'invoice_number' => Invoice::generateInvoiceNumber(),

                'invoice_date' => now(),
                'due_date' => now()->addDays(30),

                'tax_amount' => $taxAmount,
                'subtotal' => $newLockerAmount,

                'stamping_fee' => $request->stamping_fee ?? 0,
                'key_deposit' => $depositCharge,
                'insurance_charges' => $request->insurance_charges ?? 0,
                'discount_amount' => $request->discount_amount ?? 0,

                'total_amount' => $finalAmount,
                'paid_amount' => 0,
                'balance_amount' => $finalAmount,

                'payment_mode' => $request->payment_mode,

                'status' => 'pending',
                'payment_type' => 'change',

                'client_name' => $client->name,
                'client_phone' => $client->phone,
                'client_address' => $client->address,

                'membership_plan' => $registration->membershipPlan->name ?? '',

                'contract_start_date' => $startDate,
                'contract_end_date' => $endDate,

                'notes' => 'change',

                'account_type_id' => $registration->account_type_id ?? 1,

                'email' => $client->email ?? '',
                'safebox' => $registration->safebox_count ?? '',

                'postal_code' => $client->postal_code ?? '',
                'city' => $client->city ?? '',
                'state' => $client->state ?? '',
                'country' => $client->country ?? '',

                'registration_number' => $registration->registration_number ?? '',

                'tax_type' => $request->tax_type ?? '',

                'safebox_count' => $registration->safebox_count ?? 1,
                'payment_terms' => $registration->payment_terms ?? 1,

                'referred_member_by' => $registration->referred_member_by,
                'referred_commission' => $registration->referred_commission ?? 0,
                'commission_used' => $registration->commission_used ?? 0,

                'registration_date' => $registration->registration_date ?? now(),

                'locker_id' => $newLocker->id,
                'locker_number' => $newLocker->locker_number,

                'size' => $newLocker->locker_size,
                'type' => $newLocker->locker_type,
                'locker_group' => $newLocker->locker_group_id,
                'locker_position' => $newLocker->locker_row_column,

                'start_date' => $startDate,
                'end_date' => $endDate,

                'amount' => $newLockerAmount,
                'items_data' => [[
                    'locker_id' => $newLocker->id,
                    'locker_number' => $newLocker->locker_number,
                    'size' => $newLocker->locker_size,
                    'type' => $newLocker->locker_type,
                    'amount' => $newLockerAmount,
                ]],
            ]);

            // Locker change history
            $lockerChange = \App\Models\LockerChange::create([

                'client_id' => $client->id,
                'old_item_id' => $existingItem->id,
                'new_item_id' => $newItem->id,
                'invoice_id' => $invoice->id,
                'key_returned' => true,

                'old_locker_id' => $oldLockerDetails['locker_id'],
                'old_locker_number' => $oldLockerDetails['locker_number'],

                'new_locker_id' => $newLocker->id,
                'new_locker_number' => $newLocker->locker_number,

                'changed_by_user_id' => auth()->id(),

                'reason' => $request->reason
                    ?? 'Locker change requested by user',

                'old_amount' => $oldLockerDetails['amount'],
                'new_amount' => $newLockerAmount,

                'effective_date' => $startDate,

                'status' => 'completed',

                // OLD
                'old_safebox_count' => $registration->safebox_count ?? 1,
                'old_size' => $oldLockerDetails['size'],
                'old_type' => $oldLockerDetails['type'],
                'old_locker_group' => $oldLockerDetails['locker_group'],
                'old_tax_type' => $oldTaxType,

                'old_selected_locker_data' => json_encode([
                    [
                        'id' => $oldLockerDetails['locker_id'],
                        'number' => $oldLockerDetails['locker_number'],
                        'size' => $oldLockerDetails['size'],
                        'type' => $oldLockerDetails['type'],
                        'locker_group' => $oldLockerDetails['locker_group'],
                        'locker_position' => $oldLockerDetails['locker_position'],
                        'amount' => $oldLockerDetails['amount'],
                    ]
                ]),

                'old_stamping_fee' => $oldRegistrationData['stamping_fee'],
                'old_insurance_charges' => $oldRegistrationData['insurance_charges'],
                'old_discount_amount' => $oldRegistrationData['discount_amount'],
                'old_key_deposit' => $oldDeposit,
                'new_key_deposit' => $newDeposit,
                'deposit_transferred' => $depositTransferred,
                'deposit_adjustment' => round($newDeposit - $oldDeposit, 2),
                'deposit_settlement' => $depositRefund > 0 ? 'partial_refund' : ($depositCharge > 0 ? 'additional_charge' : 'transferred'),
                'old_payment_mode' => $payment ? $payment->payment_mode : null,
                'old_tax_amount' => (
                    (
                        $oldRegistrationData['stamping_fee']
                        + $oldRegistrationData['insurance_charges']
                        + $oldLockerDetails['amount']
                        - $oldRegistrationData['discount_amount']
                    ) * 0.08
                ),
                'old_final_amount' => $payment ? $payment->total_amount : 0,
                'old_payment_terms' => $oldRegistrationData['payment_terms'],
                'old_contract_start_date' => $oldRegistrationData['contract_start_date'],
                'old_contract_end_date' => $oldRegistrationData['contract_end_date'],

                // NEW
                'new_safebox_count' => $registration->safebox_count ?? 1,
                'new_size' => $newLocker->locker_size,
                'new_type' => $newLocker->locker_type,
                'new_locker_group' => $newLocker->locker_group_id,
                'new_tax_type' => $request->tax_type,

                'new_selected_locker_data' => json_encode([
                    [
                        'id' => $newLocker->id,
                        'number' => $newLocker->locker_number,
                        'size' => $newLocker->locker_size,
                        'type' => $newLocker->locker_type,
                        'locker_group' => $newLocker->locker_group_id,
                        'locker_position' => $newLocker->locker_row_column,
                        'amount' => $newLockerAmount,
                    ]
                ]),

                'new_stamping_fee' => $request->stamping_fee,
                'new_insurance_charges' => $request->insurance_charges,
                'new_discount_amount' => $request->discount_amount,
                // 'new_key_deposit' => $request->key_deposit ?? 0,
                'new_payment_mode' => $request->payment_mode,
                'new_tax_amount' => $taxAmount,
                'new_final_amount' => $finalAmount,
                'new_payment_terms' => $registration->payment_terms,
                'new_contract_start_date' => $startDate,
                'new_contract_end_date' => $endDate,
            ]);

            $refund = null;
            if ($depositRefund > 0) {
                $refund = AccountRefund::create([
                    'client_id' => $client->id,
                    'refund_type' => 'locker_change',
                    'source_id' => $lockerChange->id,
                    'locker_number' => $oldLockerDetails['locker_number'],
                    'amount' => $depositRefund,
                    'bank_name' => trim((string) $request->refund_bank_name),
                    'account_name' => trim((string) $request->refund_account_name),
                    'account_number' => trim((string) $request->refund_account_number),
                    'ifsc_code' => strtoupper(trim((string) $request->refund_ifsc_code)),
                    'status' => 'pending',
                    'requested_at' => now(),
                    'created_by' => auth()->id(),
                ]);
                $lockerChange->update(['refund_id' => $refund->id]);
            }

            $depositService->record($existingItem, 'transfer_out', $depositTransferred, 'completed', 'locker_change', $lockerChange->id, [
                'related_item_id' => $newItem->id,
                'invoice_id' => $invoice->id,
                'notes' => "Deposit transferred from {$oldLockerDetails['locker_number']} to {$newLocker->locker_number}.",
            ]);
            $depositService->record($newItem, 'transfer_in', $depositTransferred, 'completed', 'locker_change', $lockerChange->id, [
                'related_item_id' => $existingItem->id,
                'invoice_id' => $invoice->id,
                'notes' => "Deposit transferred from {$oldLockerDetails['locker_number']}.",
            ]);
            $depositService->record($newItem, 'additional_charge', $depositCharge, 'pending', 'locker_change', $lockerChange->id, [
                'invoice_id' => $invoice->id,
                'notes' => 'Additional key deposit becomes refundable after this invoice is paid.',
            ]);
            $depositService->record($existingItem, 'refund', $depositRefund, 'pending', 'locker_change', $lockerChange->id, [
                'refund_id' => $refund?->id,
                'notes' => 'Key-deposit difference queued for refund.',
            ]);

            DB::commit();

            return redirect()
                ->route('customers.invoice.show', ['client' => $client->id, 'registration' => $registration->id, 'invoice_id' => $invoice->id])
                ->with('success', 'Locker changed successfully. Invoice is pending payment.');
        } catch (ValidationException $e) {

            DB::rollBack();

            return redirect()->back()->withErrors($e->errors())->withInput();
        } catch (\Exception $e) {

            DB::rollBack();

            Log::error('Error changing locker for client ' . $id . ': ' . $e->getMessage());

            Log::error($e->getTraceAsString());

            return redirect()
                ->back()
                ->with('error', 'Unable to change locker: ' . $e->getMessage())
                ->withInput();
        }
    }

    public function edit($id)
    {
        $client = Client::byTenant()->with(['customerMembers' => fn($query) => $query->where('is_active', 1), 'accountType', 'countryRel', 'stateRel', 'cityRel'])->findOrFail($id);
        if ($resp = $this->closedClientJsonOrRedirect($client)) return $resp;
        $account_type = \App\Models\AccountType::select('id', 'type_name')->where('status', 1)->get();
        $countries = Country::select('id', 'name')->where('status', 1)->get();
        return view('clients.edit', compact('client', 'account_type', 'countries'));
    }

    public function update(Request $request, $id)
    {
        $request->merge([
            'city' => preg_replace('/\s+/u', ' ', trim((string) $request->city)),
        ]);

        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'nullable|email',
            'phone' => 'required|digits_between:10,11',
            'landline' => 'nullable|string|max:20',
            'address' => 'required|string|max:500',
            'city' => ['bail', 'required', 'string', 'min:2', 'max:100', 'regex:/^[\pL\pM\pN\s\.\'(),-]+$/u'],
            'state' => 'required|integer|exists:states,id',
            'country' => 'required|integer|exists:countries,id',
            'postal_code' => 'required|string|max:20',
            'account_type' => 'required|integer|exists:account_types,id',
            'registration_number' => 'nullable|string|max:30',
            'tax_type' => 'required|string|in:SST,GST',
            'source_member_id' => 'nullable|integer',
        ], [
            'city.required' => 'Please enter the city.',
            'city.min' => 'The city must contain at least 2 characters.',
            'city.max' => 'The city must not exceed 100 characters.',
            'city.regex' => 'The city may only contain letters, numbers, spaces, periods, apostrophes, commas, parentheses, and hyphens.',
        ]);

        $validator->after(function ($validator) use ($request, $id) {
            $client = Client::byTenant()->with(['accountType', 'customerMembers' => fn($query) => $query->where('is_active', 1)])->find($id);
            if (!$client) return;

            if ((int) $request->account_type !== (int) $client->account_type_id) {
                $validator->errors()->add('account_type', 'Account Type cannot be changed after registration.');
                return;
            }

            $count = $client->customerMembers->count();
            if ((int) $request->account_type === 1 && $count !== 1) {
                $validator->errors()->add('account_type', 'Individual accounts require exactly 1 active member. Deactivate extra members first.');
            }

            if ($request->filled('source_member_id') && !$client->customerMembers->contains('id', (int) $request->source_member_id)) {
                $validator->errors()->add('source_member_id', 'Select an active member belonging to this customer.');
            }
        });

        if ($validator->fails()) {
            return back()->withErrors($validator)->withInput();
        }

        try {
            DB::beginTransaction();

            $client = Client::byTenant()->findOrFail($id);
            if ($client->isClosed()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Account is closed. No further operations are allowed for this client.',
                ], 403);
            }
            if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;

            $activeMembers = $client->customerMembers()->where('is_active', 1)->get();
            $minimumByType = [1 => 1, 2 => 2, 3 => 5, 4 => 5];
            $targetCount = max($activeMembers->count(), $minimumByType[(int) $request->account_type] ?? 1);
            $values = [
                'name' => $request->name,
                'email' => $request->filled('email') ? $request->email : null,
                'phone' => $request->phone,
                'landline' => $request->landline,
                'address' => $request->address,
                'city' => $request->city,
                'state' => $request->state,
                'country' => $request->country,
                'postal_code' => $request->postal_code,
                'account_type_id' => $request->account_type,
                'auth_count' => $targetCount,
                'authorized_count' => min(max((int) $client->authorized_count, 1), $targetCount),
                'registration_number' => $request->registration_number,
                'tax_type' => $request->tax_type,
            ];

            $typeName = strtolower((string) optional(\App\Models\AccountType::find($request->account_type))->type_name);
            if ((str_contains($typeName, 'individual') || str_contains($typeName, 'joint')) && $request->filled('source_member_id')) {
                $source = $activeMembers->firstWhere('id', (int) $request->source_member_id);
                $values = array_merge($values, [
                    'name' => $source->name,
                    'phone' => $source->mobile,
                    'email' => $source->email ?: null,
                    'address' => $source->address ?: $request->address,
                ]);
            }

            $client->update($values);

            $client->lockerRegistrations()->where('status', 'Active')->update(['auth_count' => $targetCount]);

            DB::commit();

            return redirect()->route('customers.show', $client)->with('success', 'Customer details updated successfully.');
        } catch (\Exception $e) {
            DB::rollBack();

            return back()->withInput()->with('error', 'Unable to update customer: ' . $e->getMessage());
        }
    }

    public function destroy($id)
    {
        try {
            $client = Client::byTenant()->findOrFail($id);

            // Check if client has active contracts
            if ($client->contracts()->where('status_id', Status::getActiveId())->count() > 0) {
                return response()->json(['success' => false, 'message' => 'Cannot delete client with active contracts'], 400);
            }

            $client->delete();

            return response()->json(['success' => true, 'message' => 'Client deleted successfully']);
        } catch (\Exception $e) {
            return response()->json(['success' => false, 'message' => 'Error deleting client: ' . $e->getMessage()], 500);
        }
    }

    public function locations()
    {
        $locations = ClientLocation::with('client')
            ->byTenant()
            ->paginate(20);

        // Get filter options
        $clients = \App\Models\Client::byTenant()->active()->pluck('name', 'id');

        return view('clients.locations', compact('locations', 'clients'));
    }

    public function contracts()
    {
        $contracts = ClientContract::with(['client', 'status'])
            ->byTenant()
            ->paginate(20);

        // Get filter options
        $clients = \App\Models\Client::byTenant()->active()->pluck('name', 'id');
        $statuses = \App\Models\Status::where('type', 'contract')->pluck('name', 'id');

        return view('clients.contracts', compact('contracts', 'clients', 'statuses'));
    }

    public function contacts($clientId)
    {
        $client = Client::with(['contacts'])->byTenant()->findOrFail($clientId);
        return view('clients.contacts', compact('client'));
    }

    public function storeContact(Request $request, $clientId)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|email',
            'phone' => 'required|string|max:20',
            'position' => 'nullable|string|max:255',
            'department' => 'nullable|string|max:255',
            'notes' => 'nullable|string|max:1000',
            'is_primary' => 'boolean',
        ]);

        if ($validator->fails()) {
            return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
        }

        try {
            DB::beginTransaction();

            $client = Client::byTenant()->findOrFail($clientId);

            $contact = $client->addContact([
                'name' => $request->name,
                'email' => $request->email,
                'phone' => $request->phone,
                'position' => $request->position,
                'department' => $request->department,
                'notes' => $request->notes,
                'is_primary' => $request->boolean('is_primary', false),
            ]);

            DB::commit();

            return response()->json(['success' => true, 'contact' => $contact]);
        } catch (\Exception $e) {
            DB::rollBack();
            return response()->json(['success' => false, 'message' => 'Error creating contact: ' . $e->getMessage()], 500);
        }
    }

    public function showContact($contactId)
    {
        $contact = ClientContact::with(['client'])->byTenant()->findOrFail($contactId);
        return response()->json($contact);
    }

    public function updateContact(Request $request, $contactId)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|email',
            'phone' => 'required|string|max:20',
            'position' => 'nullable|string|max:255',
            'department' => 'nullable|string|max:255',
            'notes' => 'nullable|string|max:1000',
            'is_primary' => 'boolean',
        ]);

        if ($validator->fails()) {
            return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
        }

        try {
            DB::beginTransaction();

            $contact = ClientContact::byTenant()->findOrFail($contactId);

            $contact->update([
                'name' => $request->name,
                'email' => $request->email,
                'phone' => $request->phone,
                'position' => $request->position,
                'department' => $request->department,
                'notes' => $request->notes,
            ]);

            if ($request->boolean('is_primary', false)) {
                $contact->makePrimary();
            }

            DB::commit();

            return response()->json(['success' => true, 'contact' => $contact]);
        } catch (\Exception $e) {
            DB::rollBack();
            return response()->json(['success' => false, 'message' => 'Error updating contact: ' . $e->getMessage()], 500);
        }
    }

    public function destroyContact($contactId)
    {
        try {
            $contact = ClientContact::byTenant()->findOrFail($contactId);

            if ($contact->is_primary) {
                return response()->json(['success' => false, 'message' => 'Cannot delete primary contact'], 400);
            }

            $contact->delete();

            return response()->json(['success' => true, 'message' => 'Contact deleted successfully']);
        } catch (\Exception $e) {
            return response()->json(['success' => false, 'message' => 'Error deleting contact: ' . $e->getMessage()], 500);
        }
    }

    public function members($customerId)
    {
        $client = Client::byTenant()
            ->with(['lockerRegistrations' => function ($query) {
                $query->with(['items', 'membershipPlan'])
                    ->where('status', 'Active');
            }])
            ->findOrFail($customerId);

        $members = $client->customerMembers()
            ->with('statusRelation')
            ->where('is_active', 1)
            ->get();

        // Calculate dynamically
        $authCount = $members->count();

        return view('clients.members', compact('client', 'members', 'authCount'));
    }

    public function manageMembers($customerId)
    {
        $client = Client::byTenant()
            ->with(['customerMembers' => fn ($query) => $query->where('is_active', 1)->orderBy('id')])
            ->findOrFail($customerId);

        abort_if($client->isClosed(), 403, 'Account is closed.');

        return view('clients.members-manage', compact('client'));
    }

    public function saveMemberChanges(Request $request, $customerId)
    {
        $client = Client::byTenant()->findOrFail($customerId);
        if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;

        $memberRules = [
            'name' => 'required|string|min:2|max:255',
            'gender' => 'required|in:male,female',
            'email' => 'nullable|email|max:255',
            'country_code' => 'required|in:+6',
            'mobile' => 'required|digits_between:10,11',
            'proof_type' => 'required|in:passport,ic_number',
            'proof_number' => 'required|string|min:6|max:12',
            'address' => 'nullable|string|max:500',
            'face_auth' => 'nullable|boolean',
            'required_auth' => 'nullable|boolean',
        ];

        $rules = [
            'existing' => 'required|array',
            'existing.*.id' => 'required|integer',
            'existing.*.active' => 'required|boolean',
            'existing.*.proof_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png,gif|max:5120',
            'new_members' => 'nullable|array',
            'new_members.*.proof_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png,gif|max:5120',
            'change_resolution' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
            'resolution_verified' => 'required|accepted',
        ];
        foreach ($memberRules as $field => $rule) {
            $rules["existing.*.{$field}"] = $rule;
            $rules["new_members.*.{$field}"] = $rule;
        }

        $messages = [
            'existing.*.name.required' => 'Enter the name for every active member.',
            'existing.*.name.min' => 'Each member name must contain at least 2 characters.',
            'existing.*.gender.required' => 'Select a gender for every active member.',
            'existing.*.email.email' => 'Enter a valid member email address, or leave it empty.',
            'existing.*.mobile.required' => 'Enter a mobile number for every active member.',
            'existing.*.mobile.digits_between' => 'Each member mobile number must contain 10 or 11 digits after +6.',
            'existing.*.proof_type.required' => 'Select an ID type for every active member.',
            'existing.*.proof_number.required' => 'Enter an ID number for every active member.',
            'new_members.*.name.required' => 'Enter a name for every new member.',
            'new_members.*.name.min' => 'Each new member name must contain at least 2 characters.',
            'new_members.*.gender.required' => 'Select a gender for every new member.',
            'new_members.*.email.email' => 'Enter a valid new-member email address, or leave it empty.',
            'new_members.*.mobile.required' => 'Enter a mobile number for every new member.',
            'new_members.*.mobile.digits_between' => 'Each new-member mobile number must contain 10 or 11 digits after +6.',
            'new_members.*.proof_type.required' => 'Select an ID type for every new member.',
            'new_members.*.proof_number.required' => 'Enter an ID number for every new member.',
            'change_resolution.required' => 'Upload the signed Change of Resolution document.',
            'change_resolution.mimes' => 'The Change of Resolution must be a PDF, JPG, JPEG, or PNG file.',
            'change_resolution.max' => 'The Change of Resolution must not be larger than 10 MB.',
            'resolution_verified.required' => 'Confirm that the Change of Resolution was received and checked.',
            'resolution_verified.accepted' => 'Confirm that the Change of Resolution was received and checked.',
        ];

        $validator = Validator::make($request->all(), $rules, $messages);
        $validator->after(function ($validator) use ($request, $client) {
            $activeExisting = collect($request->input('existing', []))->filter(fn ($member) => (bool) ($member['active'] ?? false));
            $newMembers = collect($request->input('new_members', []));
            $finalMembers = $activeExisting->concat($newMembers);

            if ($finalMembers->isEmpty()) {
                $validator->errors()->add('existing', 'At least one active authorized member is required.');
            }

            $requiredCount = $finalMembers->filter(fn ($member) => (bool) ($member['required_auth'] ?? false))->count();
            if ($requiredCount > max(1, (int) $client->authorized_count)) {
                $validator->errors()->add('required_auth', "Only {$client->authorized_count} mandatory member(s) can be selected. Increase People per visit or select fewer mandatory members.");
            }

            $identities = [];
            foreach ($finalMembers as $index => $member) {
                $proofType = $member['proof_type'] ?? '';
                $proofNumber = strtoupper(str_replace(['-', ' '], '', (string) ($member['proof_number'] ?? '')));
                $validIdentity = $proofType === 'ic_number'
                    ? preg_match('/^[0-9]{12}$/', $proofNumber)
                    : preg_match('/^[A-Z0-9]{6,12}$/', $proofNumber);
                if (!$validIdentity) {
                    $label = $proofType === 'ic_number'
                        ? 'IC Number must contain exactly 12 digits.'
                        : 'Passport number must contain 6 to 12 letters or digits.';
                    $validator->errors()->add('proof_number', $label);
                }

                $key = $proofType . ':' . $proofNumber;
                if (isset($identities[$key])) {
                    $validator->errors()->add('proof_number', 'The same IC/passport cannot be used for more than one active member.');
                    break;
                }
                $identities[$key] = true;
            }
        });

        if ($validator->fails()) {
            return back()->withErrors($validator)->withInput();
        }

        $storedPaths = [];
        $newPublicProofs = [];
        $oldPublicProofs = [];

        try {
            DB::beginTransaction();

            $lockedClient = Client::byTenant()->lockForUpdate()->findOrFail($customerId);
            $currentMembers = $lockedClient->customerMembers()->where('is_active', 1)->lockForUpdate()->get()->keyBy('id');
            $submittedExisting = collect($request->input('existing', []))->keyBy(fn ($member) => (int) $member['id']);

            if ($currentMembers->keys()->sort()->values()->all() !== $submittedExisting->keys()->sort()->values()->all()) {
                throw ValidationException::withMessages([
                    'existing' => 'The member list changed after this page was opened. Reload the page and try again.',
                ]);
            }

            $resolutionPath = $request->file('change_resolution')->store("client-documents/{$lockedClient->id}", 'local');
            if (!$resolutionPath) {
                throw new \RuntimeException('Unable to store the Change of Resolution.');
            }
            $storedPaths[] = $resolutionPath;
            $summary = [];
            $memberProofDirectory = public_path('uploads/member-proofs');
            if (!is_dir($memberProofDirectory)) {
                mkdir($memberProofDirectory, 0755, true);
            }

            foreach ($currentMembers as $memberId => $member) {
                $data = $submittedExisting->get($memberId);
                if (!(bool) $data['active']) {
                    $member->update(['status' => 3, 'is_active' => false, 'required_auth' => false]);
                    $summary[] = "Deactivated #{$member->id} {$member->name}";
                    continue;
                }

                $updates = [
                    'name' => $data['name'],
                    'gender' => $data['gender'],
                    'email' => $data['email'] ?: null,
                    'mobile' => $data['mobile'],
                    'proof_type' => $data['proof_type'],
                    'proof_number' => $data['proof_number'],
                    'address' => $data['address'] ?? null,
                    'face_auth' => (bool) ($data['face_auth'] ?? false),
                    'required_auth' => (bool) ($data['required_auth'] ?? false),
                ];

                $proofFile = $request->file("existing.{$memberId}.proof_file");
                if ($proofFile) {
                    $filename = uniqid('member_', true) . '.' . $proofFile->getClientOriginalExtension();
                    $proofFile->move($memberProofDirectory, $filename);
                    $updates['proof_file'] = 'uploads/member-proofs/' . $filename;
                    $newPublicProofs[] = $updates['proof_file'];
                    if ($member->proof_file) $oldPublicProofs[] = $member->proof_file;
                }

                $changed = collect($updates)->contains(fn ($value, $field) => (string) $member->{$field} !== (string) $value);
                $member->update($updates);
                if ($changed) $summary[] = "Updated #{$member->id} {$member->name}";
            }

            foreach ($request->input('new_members', []) as $index => $data) {
                $proofPath = null;
                $proofFile = $request->file("new_members.{$index}.proof_file");
                if ($proofFile) {
                    $filename = uniqid('member_', true) . '.' . $proofFile->getClientOriginalExtension();
                    $proofFile->move($memberProofDirectory, $filename);
                    $proofPath = 'uploads/member-proofs/' . $filename;
                    $newPublicProofs[] = $proofPath;
                }

                $member = $lockedClient->customerMembers()->create([
                    'name' => $data['name'],
                    'gender' => $data['gender'],
                    'email' => $data['email'] ?: null,
                    'mobile' => $data['mobile'],
                    'proof_type' => $data['proof_type'],
                    'proof_number' => $data['proof_number'],
                    'proof_file' => $proofPath,
                    'address' => $data['address'] ?? null,
                    'finger_auth' => false,
                    'face_auth' => (bool) ($data['face_auth'] ?? false),
                    'required_auth' => (bool) ($data['required_auth'] ?? false),
                    'status' => 2,
                    'is_active' => true,
                ]);
                $summary[] = "Added #{$member->id} {$member->name}";
            }

            $finalCount = $lockedClient->customerMembers()->where('is_active', 1)->count();
            $lockedClient->update([
                'auth_count' => $finalCount,
                'authorized_count' => min(max((int) $lockedClient->authorized_count, 1), $finalCount),
            ]);
            $lockedClient->lockerRegistrations()->where('status', 'Active')->update(['auth_count' => $finalCount]);

            ClientDocument::create([
                'client_id' => $lockedClient->id,
                'document_type' => 'change_resolution',
                'description' => $summary ? implode('; ', $summary) : 'Authorized member list reviewed',
                'file_path' => $resolutionPath,
                'is_verified' => true,
            ]);

            DB::commit();

            foreach ($oldPublicProofs as $oldProof) {
                if (file_exists(public_path($oldProof))) unlink(public_path($oldProof));
            }

            return redirect()->route('customers.members', $lockedClient->id)
                ->with('success', 'Authorized members and Change of Resolution saved successfully.');
        } catch (ValidationException $e) {
            DB::rollBack();
            Storage::disk('local')->delete($storedPaths);
            foreach ($newPublicProofs as $proof) if (file_exists(public_path($proof))) unlink(public_path($proof));
            return back()->withErrors($e->errors())->withInput();
        } catch (\Throwable $e) {
            DB::rollBack();
            Storage::disk('local')->delete($storedPaths);
            foreach ($newPublicProofs as $proof) if (file_exists(public_path($proof))) unlink(public_path($proof));
            Log::error('Unable to save authorized member changes', ['client_id' => $customerId, 'error' => $e->getMessage()]);
            return back()->withErrors(['general' => 'Unable to save member changes. No changes were applied.'])->withInput();
        }
    }


    public function storeMember(Request $request, $customerId)
    {
        $client = Client::byTenant()->findOrFail($customerId);
        if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;
        $request->merge([
            'status' => 2
        ]);

        $validator = Validator::make($request->all(), [
            'name' => 'required|string|min:2|max:255',
            'gender' => 'required|string|in:male,female',
            // 'email' => 'nullable|email|unique:customer_members,email,NULL,id,client_id,' . $customerId,
            'email' => 'required|email',
            'mobile' => 'required|digits_between:10,11',
            'proof_type' => 'required|string|in:passport,ic_number',
            'proof_number' => 'required|string|min:5|max:100',
            'proof_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png,gif|max:5120',
            'address' => 'nullable|string|max:500',
            'finger_auth' => 'boolean',
            'face_auth' => 'required|boolean',
            'required_auth' => 'boolean',
            'change_resolution' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
            'resolution_verified' => 'required|accepted',
            // 'status' => 2,
        ]);

        if ($validator->fails()) {
            return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
        }

        try {
            $resolutionPath = null;
            $proofFilePath = null;
            $client = Client::byTenant()->findOrFail($customerId);

            $authRequiredCount = $client->authorized_count; // authorized_count
            $currentRequiredCount = CustomerMember::where('client_id', $customerId)
                ->where('required_auth', 1)
                ->where('is_active', 1)
                ->count();

            if ($request->boolean('required_auth')) {
                if ($authRequiredCount <= $currentRequiredCount) {
                    return response()->json([
                        'success' => false,
                        'message' => "Cannot add more required members. Maximum limit reached: {$currentRequiredCount}/{$authRequiredCount}"
                    ], 422);
                }
            }

            $currentMembersCount = $client->customerMembers()
                ->where('is_active', 1)
                ->count();

            DB::beginTransaction();

            $resolutionPath = $request->file('change_resolution')
                ->store("client-documents/{$client->id}", 'local');
            if (!$resolutionPath) {
                throw new \RuntimeException('Unable to store the Change of Resolution.');
            }

            if ($request->hasFile('proof_file')) {
                $file = $request->file('proof_file');
                $filename = time() . '_' . preg_replace('/\s+/', '_', $file->getClientOriginalName());
                $uploadDir = public_path('uploads/member-proofs');
                if (!file_exists($uploadDir)) {
                    mkdir($uploadDir, 0755, true);
                }
                $file->move($uploadDir, $filename);
                $proofFilePath = 'uploads/member-proofs/' . $filename;
            }

            $member = $client->customerMembers()->create([
                'name' => $request->name,
                'gender' => $request->gender,
                'email' => $request->email,
                'mobile' => $request->mobile,
                'proof_type' => $request->proof_type,
                'proof_number' => $request->proof_number,
                'proof_file' => $proofFilePath,
                'address' => $request->address,
                'finger_auth' => $request->boolean('finger_auth'),
                'face_auth' => $request->boolean('face_auth'),
                'required_auth' => $request->boolean('required_auth'),
                'status' => $request->status ?? 1, // Default to active if not provided
                'is_active' => true,
                // 'tenant_id' => $client->tenant_id,
            ]);

            $newCount = $currentMembersCount + 1;
            $client->update(['auth_count' => $newCount]);
            $client->lockerRegistrations()->where('status', 'Active')->update(['auth_count' => $newCount]);

            ClientDocument::create([
                'client_id' => $client->id,
                'document_type' => 'change_resolution',
                'description' => "Member #{$member->id} {$member->name} added",
                'file_path' => $resolutionPath,
                'is_verified' => true,
            ]);

            DB::commit();

            // Return a clean array to avoid JSON serialization issues
            $response = [
                'success' => true,
                'member' => [
                    'id' => $member->id,
                    'name' => $member->name,
                    'gender' => $member->gender,
                    'email' => $member->email,
                    'mobile' => $member->mobile,
                    'address' => $member->address,
                    'finger_auth' => $member->finger_auth,
                    'face_auth' => $member->face_auth,
                    'required_auth' => $member->required_auth,
                    'is_finger_verified' => $member->is_finger_verified,
                    'status' => $member->status,
                    'is_active' => $member->is_active,
                    'created_at' => $member->created_at,
                    'updated_at' => $member->updated_at,
                ]
            ];

            // Clean any output buffers and return JSON
            if (ob_get_level()) {
                ob_clean();
            }

            return response()->json($response);
        } catch (\Exception $e) {
            if (DB::transactionLevel() > 0) {
                DB::rollBack();
            }
            if (!empty($resolutionPath)) {
                Storage::disk('local')->delete($resolutionPath);
            }
            if (!empty($proofFilePath) && file_exists(public_path($proofFilePath))) {
                unlink(public_path($proofFilePath));
            }
            // Clean any output buffers and return JSON
            if (ob_get_level()) {
                ob_clean();
            }
            return response()->json(['success' => false, 'message' => 'Error adding member: ' . $e->getMessage()], 500);
        }
    }

    public function showMember($memberId)
    {
        try {
            $member = CustomerMember::findOrFail($memberId);
            $client = Client::byTenant()->findOrFail($member->client_id);
            if ($client->isClosed()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Account is closed. No further operations are allowed for this client.',
                ], 403);
            }

            return response()->json([
                'success' => true,
                'member' => [
                    'id' => $member->id,
                    'name' => $member->name,
                    'gender' => $member->gender,
                    'email' => $member->email,
                    'mobile' => $member->mobile,
                    'proof_type' => $member->proof_type,
                    'proof_number' => $member->proof_number,
                    'proof_file' => $member->proof_file,
                    'address' => $member->address,
                    'finger_auth' => $member->finger_auth,
                    'face_auth' => $member->face_auth,
                    'required_auth' => $member->required_auth,
                    'is_finger_verified' => $member->is_finger_verified,
                    'status' => $member->status,
                    'is_active' => $member->is_active,
                    'created_at' => $member->created_at,
                    'updated_at' => $member->updated_at,
                ]
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Error loading member',
                'error' => $e->getMessage()
            ], 500);
        }
    }

    public function updateMember(Request $request, $memberId)
    {
        $member = CustomerMember::whereHas('client', fn($query) => $query->byTenant())->findOrFail($memberId);

        $validator = Validator::make($request->all(), [
            'name' => 'required|string|min:2|max:255',
            'gender' => 'required|string|in:male,female',
            'email' => 'nullable|email|unique:customer_members,email,' . $memberId . ',id,client_id,' . $member->client_id,
            'mobile' => 'required|string|max:20',
            'proof_type' => 'required|string|in:passport,ic_number',
            'proof_number' => 'required|string|min:5|max:100',
            'proof_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png,gif|max:5120',
            'address' => 'nullable|string|max:500',
            'face_auth' => 'required|accepted',
            'finger_auth' => 'nullable|boolean',
            'required_auth' => 'nullable|boolean',
            'change_resolution' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
            'resolution_verified' => 'required|accepted',
        ]);

        $validator->after(function ($validator) use ($request, $member) {
            $normalizedProof = strtoupper(str_replace(['-', ' '], '', (string) $request->proof_number));
            $duplicate = CustomerMember::where('client_id', $member->client_id)
                ->where('id', '!=', $member->id)
                ->where('proof_type', $request->proof_type)
                ->whereRaw("UPPER(REPLACE(REPLACE(proof_number, '-', ''), ' ', '')) = ?", [$normalizedProof])
                ->exists();
            if ($duplicate) {
                $validator->errors()->add('proof_number', 'This identity is already used by another member of this customer.');
            }
        });

        if ($validator->fails()) {
            return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
        }

        try {
            $resolutionPath = null;
            $newProofFilePath = null;
            $oldProofFilePath = null;

            $customerId = $member->client_id;

            $client = Client::byTenant()->findOrFail($customerId);
            if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;

            if ($request->boolean('required_auth') && !$member->required_auth) {
                $requiredMemberCount = $client->customerMembers()
                    ->where('is_active', 1)
                    ->where('required_auth', 1)
                    ->where('id', '!=', $member->id)
                    ->count();
                $peopleRequiredPerVisit = max(1, (int) $client->authorized_count);

                if ($requiredMemberCount >= $peopleRequiredPerVisit) {
                    return response()->json([
                        'success' => false,
                        'message' => "The mandatory-member limit is {$peopleRequiredPerVisit}, matching People per visit. Set the current mandatory member to Optional first, or increase People per visit.",
                    ], 422);
                }
            }

            DB::beginTransaction();

            $resolutionPath = $request->file('change_resolution')
                ->store("client-documents/{$client->id}", 'local');
            if (!$resolutionPath) {
                throw new \RuntimeException('Unable to store the Change of Resolution.');
            }

            $updateData = [
                'name' => $request->name,
                'gender' => $request->gender,
                'email' => $request->email,
                'mobile' => $request->mobile,
                'proof_type' => $request->proof_type,
                'proof_number' => $request->proof_number,
                'required_auth' => $request->boolean('required_auth'),
                'address' => $request->address,
                'finger_auth' => $request->boolean('finger_auth'),
                'face_auth' => $request->boolean('face_auth'),
            ];

            // Handle proof file upload
            if ($request->hasFile('proof_file')) {
                $oldProofFilePath = $member->proof_file;
                $file = $request->file('proof_file');
                $filename = time() . '_' . preg_replace('/\s+/', '_', $file->getClientOriginalName());
                $uploadDir = public_path('uploads/member-proofs');
                if (!file_exists($uploadDir)) {
                    mkdir($uploadDir, 0755, true);
                }
                $file->move($uploadDir, $filename);
                $newProofFilePath = 'uploads/member-proofs/' . $filename;
                $updateData['proof_file'] = $newProofFilePath;
            }

            $member->update($updateData);

            ClientDocument::create([
                'client_id' => $client->id,
                'document_type' => 'change_resolution',
                'description' => "Member #{$member->id} {$member->name} updated",
                'file_path' => $resolutionPath,
                'is_verified' => true,
            ]);

            DB::commit();

            if ($oldProofFilePath && file_exists(public_path($oldProofFilePath))) {
                unlink(public_path($oldProofFilePath));
            }

            // Clean any output buffers at start
            if (ob_get_level()) {
                ob_clean();
            }

            return response()->json([
                'success' => true,
                'member' => [
                    'id' => $member->id,
                    'name' => $member->name,
                    'email' => $member->email,
                    'mobile' => $member->mobile,
                    'address' => $member->address,
                    'finger_auth' => $member->finger_auth,
                    'is_finger_verified' => $member->is_finger_verified,
                    'created_at' => $member->created_at,
                    'updated_at' => $member->updated_at,
                ]
            ]);
        } catch (\Exception $e) {
            if (DB::transactionLevel() > 0) {
                DB::rollBack();
            }
            if (!empty($resolutionPath)) {
                Storage::disk('local')->delete($resolutionPath);
            }
            if (!empty($newProofFilePath) && file_exists(public_path($newProofFilePath))) {
                unlink(public_path($newProofFilePath));
            }
            // Clean any output buffers and return JSON
            if (ob_get_level()) {
                ob_clean();
            }
            return response()->json(['success' => false, 'message' => 'Error updating member: ' . $e->getMessage()], 500);
        }
    }

    public function verifyFinger(Request $request, $memberId)
    {
        try {
            // Clean any output buffers at the very start
            while (ob_get_level()) {
                ob_end_clean();
            }

            $member = CustomerMember::findOrFail($memberId);

            // Check if fingerprint authentication is enabled for this member
            if (!$member->finger_auth) {
                return response()->json([
                    'success' => false,
                    'message' => 'Fingerprint authentication is not enabled for this member.'
                ], 400);
            }

            // In a real implementation, this would integrate with fingerprint hardware
            // For now, we'll simulate verification by marking as verified
            $member->update([
                'status' => 1,
                'is_finger_verified' => true,
                'finger_verified_at' => now(),
            ]);

            return response()->json([
                'success' => true,
                'message' => 'Fingerprint verified successfully.',
                'member' => $member->only([
                    'id',
                    'name',
                    'email',
                    'mobile',
                    'address',
                    'finger_auth',
                    'is_finger_verified',
                    'status',
                    'is_active',
                    'created_at',
                    'updated_at'
                ])
            ]);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Member not found.'
            ], 404);
        } catch (\Exception $e) {
            Log::error('Error verifying fingerprint: ' . $e->getMessage(), [
                'member_id' => $memberId,
                'exception' => $e
            ]);

            return response()->json([
                'success' => false,
                'message' => 'An error occurred while verifying fingerprint.'
            ], 500);
        }
    }

    public function storeFace(Request $request, $memberId)
    {
        try {
            // Clean any output buffers at the very start
            while (ob_get_level()) {
                ob_end_clean();
            }

            $member = CustomerMember::findOrFail($memberId);

            // Check if face authentication is enabled for this member
            if (!$member->face_auth) {
                return response()->json([
                    'success' => false,
                    'message' => 'Face authentication is not enabled for this member.'
                ], 400);
            }

            // Validate request data
            $validator = Validator::make($request->all(), [
                'face_descriptor' => 'required|string',
                'landmarks' => 'nullable|string'
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Invalid face data provided.',
                    'errors' => $validator->errors()
                ], 422);
            }

            // Store face descriptor in database
            $member->update([
                'face_descriptor' => $request->face_descriptor,
                'is_face_verified' => 1,
                'status' => 1
            ]);

            return response()->json([
                'success' => true,
                'message' => 'Face data stored successfully.',
                'member' => $member->only([
                    'id',
                    'name',
                    'email',
                    'mobile',
                    'face_auth',
                    'face_descriptor',
                    'is_face_verified',
                    'status',
                    'updated_at'
                ])
            ]);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Member not found.'
            ], 404);
        } catch (\Exception $e) {
            Log::error('Error storing face data: ' . $e->getMessage(), [
                'member_id' => $memberId,
                'exception' => $e
            ]);

            return response()->json([
                'success' => false,
                'message' => 'An error occurred while storing face data.'
            ], 500);
        }
    }

    public function verifyFace(Request $request, $memberId)
    {
        try {
            // Clean any output buffers at the very start
            while (ob_get_level()) {
                ob_end_clean();
            }

            $member = CustomerMember::findOrFail($memberId);

            // Check if face authentication is enabled for this member
            if (!$member->face_auth) {
                return response()->json([
                    'success' => false,
                    'message' => 'Face authentication is not enabled for this member.'
                ], 400);
            }

            // Validate request data
            $validator = Validator::make($request->all(), [
                'face_descriptor' => 'required|string'
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Invalid face data provided.',
                    'errors' => $validator->errors()
                ], 422);
            }

            // Check if member has stored face descriptor
            if (!$member->face_descriptor) {
                return response()->json([
                    'success' => false,
                    'message' => 'No face data found for this member. Please capture face data first.'
                ], 400);
            }

            // Compare face descriptors
            $storedDescriptor = json_decode($member->face_descriptor, true);
            $currentDescriptor = json_decode($request->face_descriptor, true);

            if (!$storedDescriptor || !$currentDescriptor) {
                return response()->json([
                    'success' => false,
                    'message' => 'Invalid face descriptor format.'
                ], 400);
            }

            // Calculate similarity (cosine similarity)
            $similarity = $this->calculateFaceSimilarity($storedDescriptor, $currentDescriptor);

            // Threshold for verification (adjust as needed)
            $threshold = 0.6;

            if ($similarity >= $threshold) {
                // Update member verification status
                $member->update([
                    'is_face_verified' => true
                ]);

                return response()->json([
                    'success' => true,
                    'message' => 'Face verified successfully.',
                    'similarity' => round($similarity, 3),
                    'member' => $member->only([
                        'id',
                        'name',
                        'email',
                        'mobile',
                        'face_auth',
                        'is_face_verified'
                    ])
                ]);
            } else {
                return response()->json([
                    'success' => false,
                    'message' => 'Face verification failed. Similarity too low.',
                    'similarity' => round($similarity, 3),
                    'threshold' => $threshold
                ], 400);
            }
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Member not found.'
            ], 404);
        } catch (\Exception $e) {
            Log::error('Error verifying face: ' . $e->getMessage(), [
                'member_id' => $memberId,
                'exception' => $e
            ]);

            return response()->json([
                'success' => false,
                'message' => 'An error occurred while verifying face.'
            ], 500);
        }
    }

    private function calculateFaceSimilarity($descriptor1, $descriptor2)
    {
        // Calculate cosine similarity between two face descriptors
        $dotProduct = 0;
        $norm1 = 0;
        $norm2 = 0;

        for ($i = 0; $i < min(count($descriptor1), count($descriptor2)); $i++) {
            $dotProduct += $descriptor1[$i] * $descriptor2[$i];
            $norm1 += $descriptor1[$i] * $descriptor1[$i];
            $norm2 += $descriptor2[$i] * $descriptor2[$i];
        }

        $norm1 = sqrt($norm1);
        $norm2 = sqrt($norm2);

        if ($norm1 == 0 || $norm2 == 0) {
            return 0;
        }

        return $dotProduct / ($norm1 * $norm2);
    }

    public function destroyMember(Request $request, $memberId)
    {
        $validator = Validator::make($request->all(), [
            'change_resolution' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
            'resolution_verified' => 'required|accepted',
        ]);

        if ($validator->fails()) {
            return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
        }

        $resolutionPath = null;

        try {
            // Clean any output buffers at very start
            while (ob_get_level()) {
                ob_end_clean();
            }

            $member = CustomerMember::whereHas('client', fn($query) => $query->byTenant())->findOrFail($memberId);
            $client = Client::byTenant()->findOrFail($member->client_id);
            if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;

            $activeCount = $client->customerMembers()->where('is_active', 1)->count();
            if ($activeCount <= 1) {
                return response()->json(['success' => false, 'message' => 'A customer must have at least one active member.'], 422);
            }
            if ($member->required_auth && !$client->customerMembers()->where('is_active', 1)->where('required_auth', 1)->where('id', '!=', $member->id)->exists()) {
                return response()->json(['success' => false, 'message' => 'Assign Authentication Required to another member before deactivating this member.'], 422);
            }

            $resolutionPath = $request->file('change_resolution')
                ->store("client-documents/{$client->id}", 'local');
            if (!$resolutionPath) {
                throw new \RuntimeException('Unable to store the Change of Resolution.');
            }

            // Set status to 3 (Inactive) instead of deleting.
            DB::transaction(function () use ($member, $client, $activeCount, $resolutionPath) {
                $member->update(['status' => 3, 'is_active' => false]);
                $newCount = $activeCount - 1;
                $client->update([
                    'auth_count' => $newCount,
                    'authorized_count' => min(max((int) $client->authorized_count, 1), $newCount),
                ]);
                $client->lockerRegistrations()->where('status', 'Active')->update(['auth_count' => $newCount]);

                ClientDocument::create([
                    'client_id' => $client->id,
                    'document_type' => 'change_resolution',
                    'description' => "Member #{$member->id} {$member->name} deactivated",
                    'file_path' => $resolutionPath,
                    'is_verified' => true,
                ]);
            });

            return response()->json([
                'success' => true,
                'message' => 'Member deactivated successfully.',
                'member' => $member->only([
                    'id',
                    'name',
                    'email',
                    'mobile',
                    'address',
                    'finger_auth',
                    'is_finger_verified',
                    'status',
                    'is_active',
                    'created_at',
                    'updated_at'
                ])
            ]);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            if ($resolutionPath) {
                Storage::disk('local')->delete($resolutionPath);
            }
            return response()->json([
                'success' => false,
                'message' => 'Member not found.'
            ], 404);
        } catch (\Exception $e) {
            if ($resolutionPath) {
                Storage::disk('local')->delete($resolutionPath);
            }
            Log::error('Error deactivating member: ' . $e->getMessage(), [
                'member_id' => $memberId,
                'exception' => $e
            ]);

            return response()->json([
                'success' => false,
                'message' => 'An error occurred while deactivating the member.'
            ], 500);
        }
    }

    /**
     * Search members for dropdown
     */
    public function searchMembers(Request $request)
    {
        try {
            $query = CustomerMember::with('client:id,name')
                ->select('id', 'name', 'mobile', 'client_id');

            if ($request->filled('search')) {
                $search = $request->search;
                $query->where(function ($q) use ($search) {
                    $q->where('name', 'like', "%{$search}%")
                        ->orWhere('mobile', 'like', "%{$search}%");
                });
            }

            $members = $query->orderBy('name')->limit(20)->get();

            $formattedMembers = $members->map(function ($member) {
                return [
                    'id' => $member->id,
                    'name' => $member->name,
                    'phone' => $member->mobile,
                    'client_name' => $member->client ? $member->client->name : 'N/A',
                    'display_name' => $member->name . ' (' . ($member->client ? $member->client->name : 'N/A') . ')'
                ];
            });

            return response()->json($formattedMembers->values());
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'message' => 'Error searching members',
                'error' => $e->getMessage(),
            ], 500);
        }
    }

    // public function searchMemberIdentity(Request $request)
    // {
    //     $validated = $request->validate([
    //         'proof_type' => 'required|in:passport,ic_number',
    //         'proof_number' => 'required|string|min:3|max:100',
    //     ]);

    //     $normalizedProof = strtoupper(str_replace(['-', ' '], '', $validated['proof_number']));

    //     $members = CustomerMember::with('client:id,name')
    //         ->where('proof_type', $validated['proof_type'])
    //         ->whereRaw(
    //             "UPPER(REPLACE(REPLACE(proof_number, '-', ''), ' ', '')) LIKE ?",
    //             ['%' . $normalizedProof . '%']
    //         )
    //         ->orderBy('name')
    //         ->limit(20)
    //         ->get()
    //         ->map(fn ($member) => [
    //             'id' => $member->id,
    //             'client_id' => $member->client_id,
    //             'name' => $member->name,
    //             'gender' => $member->gender,
    //             'email' => $member->email,
    //             'mobile' => $member->mobile,
    //             'proof_type' => $member->proof_type,
    //             'proof_number' => $member->proof_number,
    //             'address' => $member->address,
    //             'client_name' => $member->client?->name,
    //         ]);

    //     return response()->json(['success' => true, 'members' => $members]);
    // }

    public function searchMemberIdentity(Request $request)
    {
        $validated = $request->validate([
            'proof_type' => 'required|in:passport,ic_number',
            'proof_number' => 'required|string|min:3|max:100',
        ]);

        $normalizedProof = strtoupper(str_replace(['-', ' '], '', $validated['proof_number']));

        // First search active members
        $members = CustomerMember::with('client:id,name')
            ->where('is_active', 1)
            ->where('proof_type', $validated['proof_type'])
            ->whereRaw(
                "UPPER(REPLACE(REPLACE(proof_number, '-', ''), ' ', '')) LIKE ?",
                ['%' . $normalizedProof . '%']
            )
            ->orderBy('name')
            ->limit(20)
            ->get();

        // If no active members found, search inactive members
        if ($members->isEmpty()) {
            $members = CustomerMember::with('client:id,name')
                ->where('is_active', 0)
                ->where('proof_type', $validated['proof_type'])
                ->whereRaw(
                    "UPPER(REPLACE(REPLACE(proof_number, '-', ''), ' ', '')) LIKE ?",
                    ['%' . $normalizedProof . '%']
                )
                ->orderBy('name')
                ->limit(20)
                ->get();
        }

        // Resolve the linked customer account once per client, then reuse the
        // same live checks shown by the Close Account readiness panel.
        $accountStatuses = $members->pluck('client_id')->filter()->unique()
            ->mapWithKeys(function ($clientId) {
                $client = Client::find($clientId);

                return [$clientId => [
                    'client_id' => (int) $clientId,
                    'client_name' => $client?->name,
                    'account_url' => $client ? route('customers.show', $client->id) : null,
                    'blockers' => $client ? $this->accountClosureBlockers($client) : [],
                ]];
            });

        $members = $members->map(function ($member) use ($accountStatuses) {
            return [
                'id' => $member->id,
                'client_id' => $member->client_id,
                'name' => $member->name,
                'gender' => $member->gender,
                'email' => $member->email,
                'mobile' => $member->mobile,
                'proof_type' => $member->proof_type,
                'proof_number' => $member->proof_number,
                'address' => $member->address,
                'client_name' => $member->client?->name,
                'is_active' => $member->is_active,
                'account_status' => $accountStatuses->get($member->client_id, [
                    'client_id' => $member->client_id,
                    'client_name' => $member->client?->name,
                    'account_url' => null,
                    'blockers' => [],
                ]),
            ];
        });

        return response()->json([
            'success' => true,
            'members' => $members
        ]);
    }

    /**
     * Get available lockers for selection
     */
    public function getAvailableLockers(Request $request)
    {
        try {
            $query = Locker::query();

            // Filter by size if specified
            if ($request->filled('size')) {
                $query->where('locker_size', $request->size);
            }

            // Filter by row if specified
            if ($request->filled('row')) {
                $query->where('locker_row_column', 'like', $request->row . 'C%');
            }

            // Filter by type if specified
            if ($request->filled('type')) {
                $query->where('locker_type', $request->type);
            }

            // Filter by group if specified
            if ($request->filled('group')) {
                $query->where('locker_group_id', $request->group);
            }

            // Filter by status if specified
            if ($request->filled('status')) {
                $query->where('status', ucfirst($request->status));
            }

            // If no status filter is specified, get all lockers for dashboard
            // If status is explicitly empty or not provided, get all lockers
            if (!$request->has('status') || $request->status === '') {
                $query->where('status', 'Available');
                $query->leftJoin('locker_groups', 'lockers.locker_group_id', '=', 'locker_groups.id')
                    ->select('lockers.*', 'locker_groups.name as name');

                $lockers = $query->orderBy('lockers.locker_number')->get();
            } else {
                // If status is specified (even if empty), filter by that status
                if ($request->status === '') {
                    // For client selection, default to available only
                    $query->where('status', 'Available');
                }
                $query->leftJoin('locker_groups', 'lockers.locker_group_id', '=', 'locker_groups.id')
                    ->select('lockers.*', 'locker_groups.name as name');

                $lockers = $query->orderBy('lockers.locker_number')->get();
            }

            return response()->json([
                'success' => true,
                'lockers' => $lockers
            ]);
        } catch (\Exception $e) {
            Log::error('Error fetching available lockers: ' . $e->getMessage());

            return response()->json([
                'success' => false,
                'message' => 'Error fetching lockers'
            ], 500);
        }
    }

    public function updateAuthCount(Request $request, $customerId)
    {
        try {
            $client = Client::findOrFail($customerId);
            if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;

            $validator = Validator::make($request->all(), [
                'auth_count' => 'required|integer|min:1|max:999'
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'success' => false,
                    'message' => $validator->errors()->first()
                ], 422);
            }

            $newAuthCount = $request->input('auth_count');

            // FIX 1: Get ACTIVE registration only
            $activeRegistration = $client->lockerRegistrations()
                ->where('status', 'Active')
                ->latest()
                ->first();

            $maxAuthCount = $activeRegistration ? $activeRegistration->auth_count : 0;

            // Prevent exceeding contract limit
            if ($newAuthCount > $maxAuthCount) {
                return response()->json([
                    'success' => false,
                    'message' => "Cannot exceed contract limit ({$maxAuthCount})"
                ], 422);
            }

            // FIX 2: Get current members count (ACTIVE ONLY and REQUIRED)
            $currentMembersCount = $client->customerMembers()
                ->where('required_auth', 1)
                ->where('is_active', 1)
                ->count();

            // Prevent reducing below current members
            if ($newAuthCount < $currentMembersCount) {
                return response()->json([
                    'success' => false,
                    'message' => "You already have {$currentMembersCount} members. Cannot set less than that."
                ], 422);
            }

            // Required members check (your logic)
            $currentRequiredCount = CustomerMember::where('client_id', $customerId)
                ->where('required_auth', 1)
                ->count();

            if ($newAuthCount < $currentRequiredCount) {
                return response()->json([
                    'success' => false,
                    'message' => "Minimum must be at least {$currentRequiredCount}"
                ], 422);
            }

            // SAVE
            $client->authorized_count = $newAuthCount;
            $client->save();

            return response()->json([
                'success' => true,
                'message' => "Authorized count updated to {$newAuthCount}"
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Error updating authorized count'
            ], 500);
        }
    }

    /**
     * Generate random password for locker
     */
    private function generateLockerPassword(): string
    {
        // Generate 6-character alphanumeric password
        $characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $password = '';

        for ($i = 0; $i < 6; $i++) {
            $password .= $characters[rand(0, strlen($characters) - 1)];
        }

        return $password;
    }

    public function getLockerItemDetails($itemId)
    {
        try {
            $lockerItem = LockerRegistrationItem::with([
                'registration',
                'registration.client',
                'registration.payments',
                'locker'
            ])->findOrFail($itemId);

            // Verify this item belongs to an active registration
            if ($lockerItem->registration->status !== 'Active' || $lockerItem->status !== 'active') {
                return response()->json([
                    'success' => false,
                    'message' => 'Locker item is not active'
                ], 404);
            }
            return response()->json([
                'success' => true,
                'locker_item' => [
                    'id' => $lockerItem->id,
                    'locker_number' => $lockerItem->locker_number,
                    'size' => $lockerItem->size,
                    'type' => $lockerItem->type,
                    'locker_group' => $lockerItem->locker_group,
                    'locker_position' => $lockerItem->locker_position,
                    'amount' => $lockerItem->amount,
                    'deposit_balance' => app(\App\Services\LockerDepositService::class)->balance($lockerItem),
                    'start_date' => $lockerItem->start_date,
                    'end_date' => $lockerItem->end_date,
                    'client' => [
                        'tax_type' => $lockerItem->registration->client->tax_type,
                    ],
                    'registration' => [
                        'safebox_count' => $lockerItem->registration->safebox_count,
                        'stamping_fee' => $lockerItem->registration->stamping_fee,
                        'key_deposit' => $lockerItem->registration->key_deposit,
                        'insurance_charges' => $lockerItem->registration->insurance_charges,
                        'discount_amount' => $lockerItem->registration->discount_amount,
                        'payment_terms' => $lockerItem->registration->payment_terms,
                        'contract_start_date' => $lockerItem->registration->contract_start_date,
                        'contract_end_date' => $lockerItem->registration->contract_end_date,
                        'payment_mode' => $lockerItem->registration->payments && $lockerItem->registration->payments->isNotEmpty()
                            ? $lockerItem->registration->payments->first()->payment_mode : null,
                    ]
                ]
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Error fetching locker details: ' . $e->getMessage()
            ], 500);
        }
    }

    public function import(Request $request)
    {
        $request->validate([
            'file' => 'required|mimes:csv,txt'
        ]);

        if (!$request->hasFile('file')) {
            return back()->with('error', 'No file uploaded');
        }

        /*
        |--------------------------------------------------------------------------
        | CREATE IMPORT FOLDER
        |--------------------------------------------------------------------------
        */

        $path = public_path('client/import');

        if (!file_exists($path)) {
            mkdir($path, 0777, true);
        }

        /*
        |--------------------------------------------------------------------------
        | SAVE FILE
        |--------------------------------------------------------------------------
        */

        $fileName = time() . '_' . $request->file('file')->getClientOriginalName();

        $request->file('file')->move($path, $fileName);

        $filePath = $path . '/' . $fileName;

        /*
        |--------------------------------------------------------------------------
        | OPEN CSV
        |--------------------------------------------------------------------------
        */

        $file = fopen($filePath, 'r');

        if (!$file) {
            return back()->with('error', 'Unable to open CSV file');
        }

        $results = [];

        // Skip Header
        fgetcsv($file);

        /*
        |--------------------------------------------------------------------------
        | LOOP CSV ROWS
        |--------------------------------------------------------------------------
        */

        while (($row = fgetcsv($file, 1000, ",")) !== false) {

            DB::beginTransaction();

            try {

                /*
                |--------------------------------------------------------------------------
                | ACCOUNT TYPE
                |--------------------------------------------------------------------------
                */

                $accountTypeId = $row[1] ?? 1;

                /*
                |--------------------------------------------------------------------------
                | LOCKER FETCH
                |--------------------------------------------------------------------------
                */

                $lockerNumber = trim($row[24] ?? '');

                $lockerNumber = str_pad($lockerNumber, 4, '0', STR_PAD_LEFT);

                $locker = \App\Models\Locker::where('locker_number', $lockerNumber)->first();

                if (!$locker) {

                    $results[] = [
                        'name' => $row[3] ?? '',
                        'phone' => $row[4] ?? '',
                        'locker' => $lockerNumber,
                        'status' => 'failed',
                        'message' => 'Locker Not Found'
                    ];

                    DB::rollBack();

                    continue;
                }

                /*
                |--------------------------------------------------------------------------
                | CHECK LOCKER STATUS
                |--------------------------------------------------------------------------
                */

                if ($locker->status != 'Available') {

                    $results[] = [
                        'name' => $row[3] ?? '',
                        'phone' => $row[4] ?? '',
                        'locker' => $lockerNumber,
                        'status' => 'failed',
                        'message' => 'Locker Already Occupied'
                    ];

                    DB::rollBack();

                    continue;
                }

                /*
                |--------------------------------------------------------------------------
                | TAX
                |--------------------------------------------------------------------------
                */

                $taxType = strtoupper(trim($row[13] ?? 'GST'));

                $stampingFee = (float) ($row[14] ?? 0);
                $keyDeposit = (float) ($row[15] ?? 0);
                $insuranceCharges = (float) ($row[16] ?? 0);
                $discountAmount = (float) ($row[17] ?? 0);

                $lockerAmount = $locker->locker_amount ?? 0;

                $subtotal = $lockerAmount - $discountAmount;

                $taxAmount = 0;

                if ($taxType == 'GST' || $taxType == 'SST') {
                    $taxAmount = ($subtotal * 8) / 100;
                }

                $finalAmount =
                    $subtotal +
                    $taxAmount +
                    $stampingFee +
                    $keyDeposit +
                    $insuranceCharges;

                /*
                |--------------------------------------------------------------------------
                | DATES
                |--------------------------------------------------------------------------
                */

                $contractStartDate = !empty($row[28])
                    ? date('Y-m-d', strtotime($row[28]))
                    : null;

                $contractEndDate = !empty($row[29])
                    ? date('Y-m-d', strtotime($row[29]))
                    : null;

                // echo $contractEndDate; exit;

                $paymentTerms = $row[19] ?? 1;
                /*
                |--------------------------------------------------------------------------
                | CLIENT CREATE
                |--------------------------------------------------------------------------
                */

                $client = \App\Models\Client::create([

                    'status_id' => $row[0] ?? 1,
                    'account_type_id' => $accountTypeId,
                    'auth_count' => $row[2] ?? 1,

                    'name' => $row[3] ?? '',
                    'phone' => $row[4] ?? '',
                    'email' => $row[5] ?? null,

                    'landline' => $row[6] ?? '',

                    'address' => $row[7] ?? '',

                    'country_id' => $row[8] ?? null,
                    'state_id' => $row[9] ?? null,
                    'city_id' => $row[10] ?? null,

                    // 'postal_code' => $row[11] ?? null,

                    'postal_code' => (
                        isset($row[11]) &&
                        trim($row[11]) !== '' &&
                        strtoupper(trim($row[11])) !== 'NULL'
                    )
                        ? trim($row[11])
                        : null,

                    'registration_number' => $row[12] ?? '',

                    'tax_type' => $taxType,
                    'tax_amount' => $taxAmount,

                    'stamping_fee' => $stampingFee,
                    'key_deposit' => $keyDeposit,
                    'insurance_charges' => $insuranceCharges,
                    'discount_amount' => $discountAmount,

                    'final_amount' => $finalAmount,

                    'payment_mode' => $row[18] ?? 'cash',
                    'payment_terms' => $paymentTerms,

                    'contract_start_date' => $contractStartDate,
                    'contract_end_date' => $contractEndDate,

                    'referred_member_by' =>
                    !empty($row[22]) && $row[22] != 'NULL'
                        ? $row[22]
                        : null,

                    'referred_commission' => $row[23] ?? 0,

                    'selected_lockers_data' => json_encode([
                        [
                            'id' => $locker->id,
                            'number' => $locker->locker_number,
                            'size' => $locker->locker_size ?? '',
                            'type' => $locker->locker_type ?? '',
                            'locker_group' => $locker->locker_group ?? '',
                            'locker_position' => $locker->locker_row_column ?? '',
                            'amount' => $lockerAmount,
                        ]
                    ]),

                    'is_appointment' => 1,
                    'tenant_id' => config('app.default_tenant_id', 1),

                    'account_number' => $row[25] ?? 0,
                    'i@port' => $row[26] ?? 0,
                    'nric' => $row[27] ?? 0,
                    'import_contract_start_date' => $row[20] ?? 0,
                    'import_contract_end_date' => $row[21] ?? 0,

                ]);

                /*
                |--------------------------------------------------------------------------
                | REGISTRATION
                |--------------------------------------------------------------------------
                */

                $registration = \App\Models\LockerRegistration::create([

                    'client_id' => $client->id,
                    'auth_count' => $client->auth_count,

                    'locker_password' => '123456',

                    'registration_date' => now()->toDateString(),

                    'safebox_count' => 1,

                    'contract_start_date' => $contractStartDate,
                    'contract_end_date' => $contractEndDate,

                    'payment_terms' => $paymentTerms,

                    'stamping_fee' => $stampingFee,
                    'key_deposit' => $keyDeposit,
                    'insurance_charges' => $insuranceCharges,
                    'discount_amount' => $discountAmount,
                    'tax_amount' => $taxAmount,

                    'referred_member_by' => $client->referred_member_by,
                    'referred_commission' => $client->referred_commission,

                    'status' => 'Active',
                ]);

                /*
                |--------------------------------------------------------------------------
                | REGISTRATION ITEM
                |--------------------------------------------------------------------------
                */

                \App\Models\LockerRegistrationItem::create([

                    'registration_id' => $registration->id,

                    'locker_id' => $locker->id,
                    'locker_number' => $locker->locker_number,

                    'size' => $locker->locker_size ?? '',
                    'type' => $locker->locker_type ?? '',

                    'locker_group' => $locker->locker_group ?? '',
                    'locker_position' => $locker->locker_row_column ?? '',

                    'start_date' => $contractStartDate,
                    'end_date' => $contractEndDate,

                    'amount' => $lockerAmount,

                    'status' => 'active',

                    'added_at' => now(),
                ]);

                /*
                |--------------------------------------------------------------------------
                | UPDATE LOCKER
                |--------------------------------------------------------------------------
                */

                $locker->update([
                    'status' => 'Occupied'
                ]);

                /*
                |--------------------------------------------------------------------------
                | INVOICE
                |--------------------------------------------------------------------------
                */

                \App\Models\Invoice::create([

                    'client_id' => $client->id,
                    'registration_id' => $registration->id,

                    'invoice_number' => \App\Models\Invoice::generateInvoiceNumber(),

                    'invoice_date' => now(),
                    'due_date' => now()->addDays(30),

                    'tax_amount' => $taxAmount,

                    'stamping_fee' => $stampingFee,
                    'key_deposit' => $keyDeposit,
                    'insurance_charges' => $insuranceCharges,
                    'discount_amount' => $discountAmount,

                    'total_amount' => $finalAmount,

                    'paid_amount' => 0,
                    'balance_amount' => $finalAmount,

                    'payment_mode' => $client->payment_mode ?? 'cash',

                    'status' => 'pending',
                    'payment_type' => 'create',

                    'client_name' => $client->name,
                    'client_phone' => $client->phone,
                    'client_address' => $client->address,

                    'account_type_id' => $client->account_type_id,

                    'email' => $client->email,

                    'postal_code' => $client->postal_code ?? 0,

                    'city' => $client->city_id ?? 0,
                    'state' => $client->state_id ?? 0,
                    'country' => $client->country_id ?? 0,

                    'registration_number' => $client->registration_number,

                    'tax_type' => $client->tax_type,

                    'safebox_count' => 1,

                    'payment_terms' => $paymentTerms,

                    'referred_member_by' => $client->referred_member_by,
                    'referred_commission' => $client->referred_commission,

                    'commission_used' => 0,

                    'registration_date' => now(),

                    'locker_id' => $locker->id,
                    'locker_number' => $locker->locker_number,

                    'size' => $locker->locker_size ?? '',
                    'type' => $locker->locker_type ?? '',

                    'locker_group' => $locker->locker_group ?? '',
                    'locker_position' => $locker->locker_row_column ?? '',

                    'start_date' => $contractStartDate,
                    'end_date' => $contractEndDate,

                    'amount' => $lockerAmount,
                ]);

                DB::commit();

                $results[] = [
                    'name' => $client->name,
                    'phone' => $client->phone,
                    'locker' => $locker->locker_number,
                    'status' => 'success',
                    'message' => 'Inserted Successfully'
                ];
            } catch (\Exception $e) {

                DB::rollBack();

                $results[] = [
                    'name' => $row[3] ?? '',
                    'phone' => $row[4] ?? '',
                    'locker' => $lockerNumber ?? '',
                    'status' => 'failed',
                    'message' => $e->getMessage()
                ];
            }
        }

        fclose($file);

        return back()->with([
            'success' => 'Clients Imported Successfully',
            'import_results' => $results
        ]);
    }

    private function closedClientJsonOrRedirect(Client $client, ?Request $request = null)
    {
        if (!$client->isClosed()) {
            return null;
        }

        $message = 'Account is closed. No further operations are allowed for this client.';

        $request = $request ?? request();
        if ($request->expectsJson()) {
            return response()->json(['success' => false, 'message' => $message], 403);
        }

        return redirect()->back()->with('error', $message);
    }

    private function lockerChangeBlocker(Client $client, bool $lock = false): ?string
    {
        $invoiceQuery = Invoice::with('payments')->where('client_id', $client->id);
        if ($lock) $invoiceQuery->lockForUpdate();

        foreach ($invoiceQuery->get() as $invoice) {
            $paid = max(
                (float) $invoice->paid_amount,
                (float) $invoice->payments
                    ->filter(fn ($payment) => strtolower(trim((string) $payment->status)) === 'paid')
                    ->sum('paid_amount')
            );
            $balance = round(max(0, (float) $invoice->total_amount - $paid), 2);
            $status = strtolower(trim((string) $invoice->status));

            if ($balance > 0.009 || in_array($status, ['draft', 'sent', 'pending', 'partial', 'overdue'], true)) {
                return "Locker cannot be changed. Invoice {$invoice->invoice_number} has RM "
                    .number_format($balance, 2).' outstanding.';
            }
        }

        $paymentQuery = Payment::whereHas('registration', fn ($query) => $query->where('client_id', $client->id))
            ->whereRaw('LOWER(TRIM(status)) IN (?, ?)', ['pending', 'partial']);
        if ($lock) $paymentQuery->lockForUpdate();
        if ($paymentQuery->first()) {
            return 'Locker cannot be changed while a payment is pending or partially paid.';
        }

        $registrationQuery = LockerRegistration::with('items')
            ->where('client_id', $client->id)
            ->whereRaw('LOWER(TRIM(status)) = ?', ['active']);
        if ($lock) $registrationQuery->lockForUpdate();

        foreach ($registrationQuery->get() as $registration) {
            $registrationEnd = $registration->contract_end_date ?: $registration->end_date;
            if ($registrationEnd && \Carbon\Carbon::parse($registrationEnd)->startOfDay()->lt(today())) {
                return 'Locker cannot be changed. The active registration expired on '
                    .\Carbon\Carbon::parse($registrationEnd)->format('d M Y').'. Renew it first.';
            }

            $expiredItem = $registration->items
                ->first(fn ($item) => strtolower(trim((string) $item->status)) === 'active'
                    && $item->end_date
                    && \Carbon\Carbon::parse($item->end_date)->startOfDay()->lt(today()));
            if ($expiredItem) {
                return "Locker {$expiredItem->locker_number} expired on "
                    .\Carbon\Carbon::parse($expiredItem->end_date)->format('d M Y')
                    .' and must be renewed before changing lockers.';
            }
        }

        return null;
    }

    /**
     * Return the latest account-closure blockers for the open customer page.
     */
    public function closureReadiness(Client $customer)
    {
        return response()->json([
            'closed' => $customer->isClosed(),
            'blockers' => $customer->isClosed() ? [] : $this->accountClosureBlockers($customer),
        ]);
    }

    /**
     * Permanently close a client account.
     * - Cannot be reopened
     * - Validates no active lockers / unpaid invoices / pending payments
     * - Stores closed_at, closed_by, close_reason
     */
    public function closeAccount(Request $request, Client $customer)
    {
        $client = $customer;

        if ($client->isClosed()) {
            return response()->json([
                'success' => false,
                'message' => 'Account is already closed.',
            ], 409);
        }

        $validator = Validator::make($request->all(), [
            'close_reason' => ['required', 'string', 'min:5', 'max:2000'],
            'key_returned' => ['required', 'accepted'],
            'key_deposit_settlement' => ['required', 'in:refunded,forfeited,not_applicable'],
            'refund_bank_name' => ['nullable', 'required_if:key_deposit_settlement,refunded', 'string', 'max:150'],
            'refund_account_name' => ['nullable', 'required_if:key_deposit_settlement,refunded', 'string', 'max:150'],
            'refund_account_number' => ['nullable', 'required_if:key_deposit_settlement,refunded', 'string', 'min:5', 'max:100', 'regex:/^[A-Za-z0-9 -]+$/'],
            'refund_ifsc_code' => ['nullable', 'required_if:key_deposit_settlement,refunded', 'string', 'min:5', 'max:30', 'regex:/^[A-Za-z0-9-]+$/'],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => 'Validation error.',
                'errors'  => $validator->errors(),
            ], 422);
        }

        $blockers = $this->accountClosureBlockers($client);
        if ($blockers !== []) {
            return response()->json([
                'success' => false,
                'message' => 'Resolve the following items before closing this account.',
                'blockers' => $blockers,
            ], 422);
        }

        // locker_payments module is not present in all deployments; rely on invoices/payments validations above.

        try {
            DB::transaction(function () use ($client, $request) {
                $lockedClient = Client::whereKey($client->id)->lockForUpdate()->firstOrFail();
                $blockers = $this->accountClosureBlockers($lockedClient, true);
                if ($blockers !== []) {
                    throw ValidationException::withMessages(['account' => array_column($blockers, 'message')]);
                }

                $closedStatusId = Status::firstOrCreate(
                    ['name' => 'closed', 'type' => 'client'],
                    ['class' => 'red']
                )->id;

                // Auto-close active locker registrations (make lockers available)
                $today = now()->toDateString();

                $activeRegistrationsQuery = LockerRegistration::with(['items', 'locker'])
                    ->where('client_id', $client->id)
                    ->where('status', 'Active');

                // Some deployments may not have end_date/contract_end_date; filter only if column exists.
                if (Schema::hasColumn('locker_registrations', 'contract_end_date')) {
                    $activeRegistrationsQuery->whereDate('contract_end_date', '>=', $today);
                } elseif (Schema::hasColumn('locker_registrations', 'end_date')) {
                    $activeRegistrationsQuery->whereDate('end_date', '>=', $today);
                }

                // Active locker determination based on locker_registration_items.end_date (only if column exists)
                // Consider "active" only when there is at least one active item that has not expired yet.
                if (Schema::hasColumn('locker_registration_items', 'end_date')) {
                    $activeRegistrationsQuery->whereHas('items', function ($q) use ($today) {
                        $q->where('status', 'active')->whereDate('end_date', '>=', $today);
                    });
                }

                $activeRegistrations = $activeRegistrationsQuery->get();
                $depositService = app(\App\Services\LockerDepositService::class);
                $depositBalances = [];
                foreach ($activeRegistrations as $activeRegistration) {
                    foreach ($activeRegistration->items->where('status', 'active') as $activeItem) {
                        $balance = $depositService->requireUnambiguousBalance($activeItem);
                        if ($balance > 0) $depositBalances[] = [$activeItem, $balance];
                    }
                }
                $refundAmount = round((float) collect($depositBalances)->sum(fn ($entry) => $entry[1]), 2);
                if ($request->input('key_deposit_settlement') === 'refunded' && $refundAmount <= 0) {
                    throw ValidationException::withMessages(['account' => 'No refundable key deposit was found for the active lockers.']);
                }

                foreach ($activeRegistrations as $registration) {
                    // Close registration itself
                    $registrationUpdates = [
                        'status' => 'closed',
                    ];
                    if (Schema::hasColumn('locker_registrations', 'end_date')) {
                        $registrationUpdates['end_date'] = $today;
                    }
                    if (Schema::hasColumn('locker_registrations', 'contract_end_date')) {
                        $registrationUpdates['contract_end_date'] = $today;
                    }
                    $registration->update($registrationUpdates);

                    // Close active items and free lockers
                    foreach ($registration->items as $item) {
                        if ($item->status === 'active') {
                            $item->update([
                                'status' => 'closed',
                                'removed_at' => now(),
                                'end_date' => now()->toDateString(),
                            ]);
                        }

                        if ($item->locker_id) {
                            Locker::find($item->locker_id)?->update(['status' => 'Available']);
                        }
                    }

                    // Also free the primary locker_id on registration (if present)
                    if ($registration->locker_id) {
                        Locker::find($registration->locker_id)?->update(['status' => 'Available']);
                    }
                }

                // Mark paid invoices as closed for audit (since account is permanently closed)
                Invoice::where('client_id', $client->id)
                    ->where('status', 'paid')
                    ->get()
                    ->each->update(['status' => 'closed']);

                $lockedClient->update([
                    'status_id'    => $closedStatusId,
                    'closed_at'    => now(),
                    'closed_by'    => auth()->id(),
                    'close_reason' => $request->input('close_reason'),
                    'key_returned' => true,
                    'key_deposit_settlement' => $request->input('key_deposit_settlement'),
                    'refund_bank_name' => $request->input('key_deposit_settlement') === 'refunded' ? trim((string) $request->input('refund_bank_name')) : null,
                    'refund_account_name' => $request->input('key_deposit_settlement') === 'refunded' ? trim((string) $request->input('refund_account_name')) : null,
                    'refund_account_number' => $request->input('key_deposit_settlement') === 'refunded' ? trim((string) $request->input('refund_account_number')) : null,
                    'refund_ifsc_code' => $request->input('key_deposit_settlement') === 'refunded' ? strtoupper(trim((string) $request->input('refund_ifsc_code'))) : null,
                ]);

                if ($request->input('key_deposit_settlement') === 'refunded') {
                    $accountRefund = AccountRefund::create([
                        'client_id' => $lockedClient->id,
                        'refund_type' => 'account_closure',
                        'source_id' => $lockedClient->id,
                        'amount' => $refundAmount,
                        'bank_name' => trim((string) $request->input('refund_bank_name')),
                        'account_name' => trim((string) $request->input('refund_account_name')),
                        'account_number' => trim((string) $request->input('refund_account_number')),
                        'ifsc_code' => strtoupper(trim((string) $request->input('refund_ifsc_code'))),
                        'status' => 'pending',
                        'requested_at' => now(),
                        'created_by' => auth()->id(),
                    ]);
                    foreach ($depositBalances as [$depositItem, $depositBalance]) {
                        $depositService->record($depositItem, 'refund', $depositBalance, 'pending', 'account_closure', $lockedClient->id, [
                            'refund_id' => $accountRefund->id,
                            'idempotency_key' => "account_closure:{$lockedClient->id}:refund:{$depositItem->id}",
                            'notes' => 'Key deposit queued for refund during account closure.',
                        ]);
                    }
                } elseif ($request->input('key_deposit_settlement') === 'forfeited') {
                    foreach ($depositBalances as [$depositItem, $depositBalance]) {
                        $depositService->record($depositItem, 'forfeiture', $depositBalance, 'completed', 'account_closure', $lockedClient->id, [
                            'idempotency_key' => "account_closure:{$lockedClient->id}:forfeiture:{$depositItem->id}",
                            'notes' => 'Key deposit forfeited during account closure.',
                        ]);
                    }
                }

                // Deactivate all authorized members
                CustomerMember::where('client_id', $client->id)
                    ->get()
                    ->each->update(['is_active' => false]);
            });

            return response()->json([
                'success' => true,
                'message' => 'Account closed successfully.',
            ]);
        } catch (ValidationException $e) {
            $blockers = $this->accountClosureBlockers($client);
            if ($blockers === []) {
                $blockers = collect($e->errors())->flatten()->map(fn ($message) => [
                    'message' => $message,
                    'action_url' => null,
                    'action_label' => null,
                ])->all();
            }
            return response()->json([
                'success' => false,
                'message' => 'Account details changed. Resolve the listed items and try again.',
                'blockers' => $blockers,
            ], 422);
        } catch (\Throwable $e) {
            Log::error('Close account failed', [
                'client_id' => $client->id,
                'error' => $e->getMessage(),
            ]);

            return response()->json([
                'success' => false,
                'message' => 'Failed to close account. Please try again.',
            ], 500);
        }
    }

    private function accountClosureBlockers(Client $client, bool $lock = false): array
    {
        $blockers = [];

        $invoiceQuery = Invoice::with('payments')->where('client_id', $client->id);
        if ($lock) $invoiceQuery->lockForUpdate();
        foreach ($invoiceQuery->get() as $invoice) {
            $paid = max(
                (float) $invoice->paid_amount,
                (float) $invoice->payments
                    ->filter(fn ($payment) => strtolower(trim((string) $payment->status)) === 'paid')
                    ->sum('paid_amount')
            );
            $balance = round(max(0, (float) $invoice->total_amount - $paid), 2);
            $status = strtolower(trim((string) $invoice->status));
            if ($balance > 0 || in_array($status, ['draft', 'sent', 'pending', 'partial', 'overdue'], true)) {
                $blockers[] = [
                    'message' => "Invoice {$invoice->invoice_number}: {$invoice->status}, RM " . number_format($balance, 2) . ' outstanding.',
                    'action_url' => route('customers.invoice.show', [
                        'client' => $client->id,
                        'registration' => $invoice->registration_id,
                        'invoice_id' => $invoice->id,
                    ]),
                    'action_label' => 'View & Pay Invoice',
                ];
            }
        }

        $paymentQuery = Payment::whereHas('registration', fn ($query) => $query->where('client_id', $client->id))
            ->whereRaw('LOWER(TRIM(status)) IN (?, ?)', ['pending', 'partial']);
        if ($lock) $paymentQuery->lockForUpdate();
        foreach ($paymentQuery->get() as $payment) {
            $blockers[] = [
                'message' => "Payment #{$payment->id}: {$payment->status}, RM " . number_format($payment->balance_amount ?: $payment->total_amount, 2) . ' unresolved.',
                'action_url' => route('customers.payment', $client->id),
                'action_label' => 'Review Payment',
            ];
        }

        $registrationQuery = LockerRegistration::with(['items.locker', 'locker'])
            ->where('client_id', $client->id);
        if ($lock) $registrationQuery->lockForUpdate();
        foreach ($registrationQuery->get() as $registration) {
            $status = strtolower(trim((string) $registration->status));
            $endDate = $registration->contract_end_date ?: $registration->end_date;
            $expired = $endDate && \Carbon\Carbon::parse($endDate)->startOfDay()->lt(today());
            $numbers = $registration->items->pluck('locker_number')->filter()->implode(', ')
                ?: ($registration->locker?->locker_number ?? 'Unknown');

            if ($status === 'pending') {
                $blockers[] = [
                    'message' => "Locker {$numbers}: registration is pending.",
                    'action_url' => route('customers.payment', $client->id),
                    'action_label' => 'Review Registration',
                ];
            } elseif ($status === 'active' && $expired) {
                $blockers[] = [
                    'message' => "Locker {$numbers}: registration expired on " . \Carbon\Carbon::parse($endDate)->format('d M Y') . ' but has not been released.',
                    'action_url' => route('customers.show', $client->id) . '#locker-registration-' . $registration->id,
                    'action_label' => 'Review Locker',
                ];
            }

            $blocked = $registration->items->filter(fn ($item) => strtolower(trim((string) $item->locker?->status)) === 'blocked')
                ->pluck('locker_number')->filter();
            if ($registration->locker && strtolower(trim((string) $registration->locker->status)) === 'blocked') {
                $blocked->push($registration->locker->locker_number);
            }
            if ($blocked->isNotEmpty()) {
                $blockers[] = [
                    'message' => 'Blocked locker requires review: ' . $blocked->unique()->implode(', ') . '.',
                    'action_url' => route('customers.show', $client->id) . '#locker-registration-' . $registration->id,
                    'action_label' => 'Review Locker',
                ];
            }
        }

        return collect($blockers)->unique('message')->values()->all();
    }

    public function updateBlockedLockers(Request $request, Client $customer)
    {
        $client = $customer;
        if ($resp = $this->closedClientJsonOrRedirect($client, $request)) return $resp;

        $reason = trim((string) $request->input('reason', ''));
        $reason = $reason === '' ? null : $reason;

        $lockerStates = collect($request->input('locker_states', []));

        $selectedLockerIds = ($lockerStates->isNotEmpty()
            ? $lockerStates->filter(fn($state) => $state === 'blocked')->keys()
            : collect($request->input('locker_ids', [])))
            ->filter(fn($v) => $v !== null && $v !== '')
            ->map(fn($v) => (int) $v)
            ->unique()
            ->values();

        $submittedLockerIds = $lockerStates->isNotEmpty()
            ? $lockerStates->keys()->map(fn($v) => (int) $v)->unique()->values()
            : $selectedLockerIds;

        $client->load([
            'lockerRegistrations' => function ($q) {
                $q->where('status', 'Active');
            },
            'lockerRegistrations.locker',
            'lockerRegistrations.items' => function ($q) {
                $q->where('status', 'active');
            },
            'lockerRegistrations.items.locker',
        ]);

        $allowedLockerIds = collect();
        foreach ($client->lockerRegistrations as $registration) {
            if ($registration->items && $registration->items->count() > 0) {
                $allowedLockerIds = $allowedLockerIds->merge(
                    $registration->items
                        ->where('status', 'active')
                        ->pluck('locker_id')
                );
            } elseif ($registration->locker_id) {
                $allowedLockerIds->push((int) $registration->locker_id);
            }
        }
        $allowedLockerIds = $allowedLockerIds->filter()->unique()->values();

        $invalid = $submittedLockerIds->diff($allowedLockerIds);
        if ($invalid->isNotEmpty()) {
            return back()->withErrors([
                'locker_ids' => 'Invalid locker selection for this client.',
            ]);
        }

        \DB::transaction(function () use ($client, $allowedLockerIds, $selectedLockerIds, $reason) {
            $lockers = \App\Models\Locker::whereIn('id', $allowedLockerIds)->get()->keyBy('id');

            foreach ($allowedLockerIds as $lockerId) {
                $locker = $lockers->get((int) $lockerId);
                if (!$locker) continue;

                $isCapitalized = in_array($locker->status, ['Available', 'Reserved', 'Occupied', 'Maintenance', 'Blocked'], true);
                $blockedValue = $isCapitalized ? 'Blocked' : 'blocked';
                $occupiedValue = $isCapitalized ? 'Occupied' : 'occupied';

                $targetStatus = $selectedLockerIds->contains((int) $lockerId) ? $blockedValue : $occupiedValue;
                $oldStatus = $locker->status;

                if ($oldStatus !== $targetStatus) {
                    $action = $targetStatus === $blockedValue ? 'block' : 'unblock';

                    LockerBlockLog::create([
                        'client_id' => $client->id,
                        'locker_id' => (int) $lockerId,
                        'action' => $action,
                        'reason' => $reason,
                        'old_status' => $oldStatus,
                        'new_status' => $targetStatus,
                        'changed_by' => Auth::id(),
                    ]);

                    $locker->status = $targetStatus;
                    $locker->save();
                }
            }
        });

        return back()->with('success', 'Locker block status updated.');
    }

    public function releaseLocker(Request $request, Client $customer)
    {
        if ($resp = $this->closedClientJsonOrRedirect($customer, $request)) return $resp;

        $validated = $request->validate([
            'registration_item_id' => ['required', 'integer', 'exists:locker_registration_items,id'],
            'released_at' => ['required', 'date_format:d-m-Y', 'before_or_equal:today'],
            'reason' => ['required', 'string', 'min:3', 'max:500'],
            'key_returned' => ['required', 'accepted'],
            'bank_name' => ['required', 'string', 'max:150'],
            'account_name' => ['required', 'string', 'max:150'],
            'account_number' => ['required', 'string', 'min:4', 'max:100'],
            'ifsc_code' => ['required', 'string', 'min:5', 'max:30'],
        ]);

        try {
            DB::transaction(function () use ($customer, $validated) {
                $item = LockerRegistrationItem::with(['registration', 'locker'])
                    ->whereKey($validated['registration_item_id'])
                    ->lockForUpdate()
                    ->firstOrFail();

                if ((int) $item->registration?->client_id !== (int) $customer->id
                    || strtolower(trim((string) $item->registration?->status)) !== 'active'
                    || strtolower(trim((string) $item->status)) !== 'active') {
                    throw ValidationException::withMessages(['locker' => 'This locker is no longer active for this customer.']);
                }

                if (strtolower(trim((string) $item->locker?->status)) === 'blocked') {
                    throw ValidationException::withMessages(['locker' => 'Unblock this locker before releasing it.']);
                }

                $activeItemCount = LockerRegistrationItem::whereHas('registration', fn ($query) => $query
                        ->where('client_id', $customer->id)
                        ->whereRaw('LOWER(TRIM(status)) = ?', ['active']))
                    ->whereRaw('LOWER(TRIM(status)) = ?', ['active'])
                    ->lockForUpdate()
                    ->pluck('id')
                    ->count();
                if ($activeItemCount <= 1) {
                    throw ValidationException::withMessages(['locker' => 'The last active locker cannot be released. Use Close Account instead.']);
                }

                foreach (Invoice::with('payments')->where('registration_id', $item->registration_id)->lockForUpdate()->get() as $invoice) {
                    $paid = max((float) $invoice->paid_amount, (float) $invoice->payments
                        ->filter(fn ($payment) => strtolower(trim((string) $payment->status)) === 'paid')
                        ->sum('paid_amount'));
                    $balance = round(max(0, (float) $invoice->total_amount - $paid), 2);
                    if ($balance > 0.009) {
                        throw ValidationException::withMessages([
                            'locker' => "Locker {$item->locker_number} cannot be released because invoice {$invoice->invoice_number} has RM " . number_format($balance, 2) . ' outstanding.',
                        ]);
                    }
                }

                $depositService = app(\App\Services\LockerDepositService::class);
                $depositAmount = $depositService->requireUnambiguousBalance($item);
                if ($depositAmount <= 0) {
                    throw ValidationException::withMessages(['locker' => 'No refundable key deposit was found for this locker.']);
                }

                $releasedAt = \Carbon\Carbon::createFromFormat('d-m-Y', $validated['released_at'])->format('Y-m-d');
                $release = \App\Models\LockerRelease::create([
                    'client_id' => $customer->id,
                    'registration_id' => $item->registration_id,
                    'registration_item_id' => $item->id,
                    'locker_id' => $item->locker_id,
                    'locker_number' => $item->locker_number,
                    'released_at' => $releasedAt,
                    'reason' => trim($validated['reason']),
                    'key_returned' => true,
                    'deposit_amount' => $depositAmount,
                    'deposit_settlement' => 'refunded',
                    'released_by' => auth()->id(),
                ]);

                $refund = AccountRefund::create([
                    'client_id' => $customer->id,
                    'refund_type' => 'locker_release',
                    'source_id' => $release->id,
                    'locker_number' => $item->locker_number,
                    'amount' => $depositAmount,
                    'bank_name' => trim($validated['bank_name']),
                    'account_name' => trim($validated['account_name']),
                    'account_number' => trim($validated['account_number']),
                    'ifsc_code' => strtoupper(trim($validated['ifsc_code'])),
                    'status' => 'pending',
                    'requested_at' => now(),
                    'created_by' => auth()->id(),
                ]);
                $release->update(['refund_id' => $refund->id]);
                $depositService->record($item, 'refund', $depositAmount, 'pending', 'locker_release', $release->id, [
                    'refund_id' => $refund->id,
                    'notes' => 'Key deposit queued for refund when the locker was released.',
                ]);

                $item->update(['status' => 'removed', 'removed_at' => now()]);
                $item->locker->update(['status' => ctype_upper(substr((string) $item->locker->status, 0, 1)) ? 'Available' : 'available']);

                $remainingInRegistration = LockerRegistrationItem::where('registration_id', $item->registration_id)
                    ->whereRaw('LOWER(TRIM(status)) = ?', ['active'])
                    ->count();
                $item->registration->update([
                    'safebox_count' => $remainingInRegistration,
                    'key_deposit' => round(max(0, (float) $item->registration->key_deposit - $depositAmount), 2),
                ]);
            });
        } catch (ValidationException $e) {
            return back()->withErrors($e->errors())->withInput();
        }

        return back()->with('success', 'Locker released successfully. It is now available, and the key-deposit refund is pending.');
    }
}
