<?php

namespace App\Domain\Financial\Services;

use App\Domain\Financial\Events\CashSessionClosed;
use App\Domain\Financial\Models\CashSession;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Facades\DB;

class CashSessionService
{
    public function open(User $user, int $branchId, int $openingBalance, ?string $notes = null): CashSession
    {
        return DB::transaction(function () use ($user, $branchId, $openingBalance, $notes) {
            // Guard: one open drawer per cashier, per branch.
            //
            // This used to be per person across every branch, reasoning that
            // one person cannot stand at two tills. Every branch is somewhere
            // else and they share logins, so what it actually produced was a
            // deadlock that stopped the tills selling: a drawer left open at
            // one branch, and at every other branch the POS asked for a shift
            // (its lookup is branch-scoped, so it saw none here), opening one
            // was refused (this check was not, so it saw the one over there),
            // and the open drawer could not be closed from here either because
            // the manage screen also only ever shows this branch's.
            //
            // A drawer is a physical box standing in one branch, and every
            // amount on it — opening float, cash in, the variance at close —
            // belongs to that branch's reconciliation. So the invariant it can
            // carry is one drawer per cashier per branch; across branches the
            // sessions are separate boxes and never mix.
            $existingOpen = CashSession::forBranch($branchId)
                ->where('user_id', $user->id)
                ->where('status', 'open')
                ->exists();

            if ($existingOpen) {
                throw new DomainException('يوجد وردية مفتوحة بالفعل في هذا الفرع. يجب إغلاقها أولاً');
            }

            return CashSession::create([
                'academy_id' => $user->academy_id,
                'branch_id' => $branchId,
                'user_id' => $user->id,
                'opening_balance' => $openingBalance,
                'status' => 'open',
                'opened_at' => now(),
                'opening_notes' => $notes,
            ]);
        });
    }

    public function close(CashSession $session, int $closingBalance, ?string $notes = null, ?array $denominations = null, ?User $closedBy = null): CashSession
    {
        return DB::transaction(function () use ($session, $closingBalance, $notes, $denominations, $closedBy) {
            if ($session->status !== 'open') {
                throw new DomainException('هذه الوردية ليست مفتوحة');
            }

            $expectedBalance = $session->opening_balance + $session->total_cash_in - $session->total_cash_out;
            $variance = $closingBalance - $expectedBalance;

            $session->update([
                'closing_balance' => $closingBalance,
                'expected_balance' => $expectedBalance,
                'variance' => $variance,
                'status' => 'closed',
                'closed_at' => now(),
                'closing_notes' => $notes,
                'closed_by' => $closedBy?->id ?? $session->user_id,
                'denominations' => $denominations,
            ]);

            $freshSession = $session->fresh();
            if ($closedBy) {
                CashSessionClosed::dispatch($freshSession, $closedBy);
            }

            return $freshSession;
        });
    }

    public function suspend(CashSession $session, string $reason, User $actor): CashSession
    {
        return DB::transaction(function () use ($session, $reason, $actor) {
            if ($session->status !== 'open') {
                throw new DomainException('هذه الوردية ليست مفتوحة');
            }

            $session->update([
                'status' => 'suspended',
                'closing_notes' => $reason,
                'closed_by' => $actor->id,
            ]);

            return $session->fresh();
        });
    }

    public function recordCashIn(CashSession $session, int $amount): void
    {
        if (!$session->isOpen()) {
            throw new DomainException('لا يمكن تسجيل عملية على وردية مغلقة');
        }

        $session->increment('total_cash_in', $amount);
        $session->increment('transactions_count');
    }

    public function recordCashOut(CashSession $session, int $amount): void
    {
        if (!$session->isOpen()) {
            throw new DomainException('لا يمكن تسجيل عملية على وردية مغلقة');
        }

        $session->increment('total_cash_out', $amount);
        $session->increment('transactions_count');
    }

    /**
     * The drawer this cashier has open in this branch.
     *
     * The branch is explicit rather than implied by the request scope so the
     * answer is the same from a queue or a console command, where enforcement
     * is off: a till belongs to one branch, and handing back another branch's
     * drawer would file the sale in one branch's revenue and the cash in
     * another's reconciliation.
     */
    public function getOpenSession(User $user, ?int $branchId = null): ?CashSession
    {
        // A named branch is answered from that branch, not through whichever
        // one the request happens to be in: the caller that matters here is a
        // refund, which asks for the drawer of the branch whose money is going
        // back out. Filtering on top of the request scope answered "no drawer"
        // whenever those two differed, and the refund then silently missed the
        // cash count instead of reducing it.
        $query = $branchId !== null
            ? CashSession::forBranch($branchId)
            : CashSession::query();

        return $query->where('user_id', $user->id)
            ->where('status', 'open')
            ->orderByDesc('opened_at')
            ->first();
    }

    /**
     * This cashier's drawers left open in other branches.
     *
     * Purely so a screen can say where they are. A shift that is open
     * somewhere else no longer blocks anything, but it is still money nobody
     * has counted, and the person standing at this till is usually the one who
     * can go and close it.
     *
     * @return \Illuminate\Database\Eloquent\Collection<int, CashSession>
     */
    public function getOpenSessionsInOtherBranches(User $user, int $branchId)
    {
        return CashSession::withoutBranchScope()
            ->with('branch')
            ->where('user_id', $user->id)
            ->where('status', 'open')
            ->where('branch_id', '!=', $branchId)
            ->orderBy('opened_at')
            ->get();
    }
}
