<?php

namespace App\Http\Controllers\Consumables;

use App\Actions\Acceptances\CreateCheckoutAcceptanceAction;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\CheckoutAcceptance;
use App\Models\Consumable;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class ConsumableCheckoutController extends Controller
{
    /**
     * Return a view to checkout a consumable to a user.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @see ConsumableCheckoutController::store() method that stores the data.
     * @since [v1.0]
     *
     * @param  int  $id
     */
    public function create($id): View|RedirectResponse
    {

        if ($consumable = Consumable::find($id)) {

            $this->authorize('checkout', $consumable);

            // Make sure the category is valid
            if ($consumable->category) {

                // Make sure there is at least one available to checkout
                if ($consumable->numRemaining() <= 0) {
                    return redirect()->route('consumables.index')
                        ->with('error', trans('admin/consumables/message.checkout.unavailable', ['requested' => 1, 'remaining' => $consumable->numRemaining()]));
                }

                // Return the checkout view
                return view('consumables/checkout', compact('consumable'));
            }

            // Invalid category
            return redirect()->route('consumables.edit', ['consumable' => $consumable->id])
                ->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.consumable')]));
        }

        // Not found
        return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));

    }

    /**
     * Saves the checkout information
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @see ConsumableCheckoutController::create() method that returns the form.
     * @since [v1.0]
     *
     * @param  int  $consumableId
     * @return RedirectResponse
     *
     * @throws AuthorizationException
     */
    public function store(Request $request, $consumableId)
    {
        if (is_null($consumable = Consumable::with('users')->find($consumableId))) {
            return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.not_found'));
        }

        $this->authorize('checkout', $consumable);

        // If the quantity is not present in the request or is not a positive integer, set it to 1
        $quantity = $request->input('checkout_qty');
        if (! isset($quantity) || ! ctype_digit((string) $quantity) || $quantity <= 0) {
            $quantity = 1;
        }

        // Make sure there is at least one available to checkout
        if ($consumable->numRemaining() <= 0 || $quantity > $consumable->numRemaining()) {
            return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.checkout.unavailable', ['requested' => $quantity, 'remaining' => $consumable->numRemaining()]));
        }

        $admin_user = auth()->user();
        $assigned_to = e($request->input('assigned_to'));

        // Check if the user exists
        if (is_null($user = User::find($assigned_to))) {
            // Redirect to the consumable management page with error
            return redirect()->route('consumables.checkout.show', $consumable)->with('error', trans('admin/consumables/message.checkout.user_does_not_exist'))->withInput();
        }

        if (! $consumable->canCheckoutTo($user)) {
            return redirect()->back()->with('error', trans('general.error_checkout_company_mismatch', [
                'item' => trans('general.consumable').' "'.$consumable->name.'"',
                'item_company' => $consumable->company?->name ?? trans('general.unassigned'),
                'target' => trans('general.user').' "'.$user->username.'"',
            ]));
        }

        // Update the consumable data
        $consumable->assigned_to = e($request->input('assigned_to'));
        $consumable->checkout_qty = $quantity;

        // Concurrency guard. The unlocked numRemaining() check above is
        // advisory only — two simultaneous checkout requests could both
        // read "1 remaining", both pass the check, both attach a pivot
        // row, and land the register at -1. Re-fetch the parent row under
        // lockForUpdate INSIDE a transaction, re-check availability
        // against the locked snapshot, and only then write. Mirrors the
        // License checkout locking pattern.
        $overAllocated = false;

        DB::transaction(function () use ($consumable, $request, $admin_user, $quantity, &$overAllocated): void {
            $locked = Consumable::whereKey($consumable->id)->lockForUpdate()->first();

            if (! $locked || $locked->numRemaining() < $quantity) {
                $overAllocated = true;

                return;
            }

            for ($i = 0; $i < $quantity; $i++) {
                $consumable->users()->attach($consumable->id, [
                    'consumable_id' => $consumable->id,
                    'created_by' => $admin_user->id,
                    'assigned_to' => e($request->input('assigned_to')),
                    'note' => $request->input('note'),
                ]);
            }
        });

        if ($overAllocated) {
            return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.checkout.unavailable', [
                'requested' => $quantity,
                'remaining' => $consumable->fresh()->numRemaining(),
            ]));
        }

        event(new CheckoutableCheckedOut(
            $consumable,
            $user,
            auth()->user(),
            $request->input('note'),
            [],
            $consumable->checkout_qty,
            $request->boolean('sign_in_place'),
        ));

        $request->request->add(['checkout_to_type' => 'user']);
        $request->request->add(['assigned_user' => $user->id]);

        session()->put([
            'redirect_option' => $request->input('redirect_option'),
            'checkout_to_type' => $request->input('checkout_to_type'),
            'sign_in_place' => $request->boolean('sign_in_place'),
        ]);

        // When sign_in_place is requested, redirect to the acceptance/signature page
        // so the user can sign in person. The signature is attributed to the target user.
        if ($request->boolean('sign_in_place')) {
            $acceptance = CheckoutAcceptance::where('checkoutable_type', Consumable::class)
                ->where('checkoutable_id', $consumable->id)
                ->where('assigned_to_id', $user->id)
                ->pending()
                ->latest()
                ->first();

            // If requireAcceptance() is false the listener won't have created one; create it now.
            if (! $acceptance) {
                $acceptance = CreateCheckoutAcceptanceAction::run($consumable, $user, $quantity);
            }

            session([
                'sign_in_place_acceptance_id' => $acceptance->id,
                'sign_in_place_item_id' => $consumable->id,
                'sign_in_place_resource_type' => 'Consumables',
            ]);

            return redirect()->route('account.accept.item', $acceptance->id)
                ->with('success', trans('admin/consumables/message.checkout.success'));
        }

        // Redirect to the new consumable page
        return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
            ->with('success', trans('admin/consumables/message.checkout.success'));
    }
}
