<?php

namespace App\Services;

use App\Models\Returns\ReturnDoc;
use App\Models\Sales\CashMovement;
use App\Models\Sales\Invoice;
use App\Models\Sales\PosSession;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Every cash movement gets its own ledger row — the till balance is always
 * derived from this ledger (opening balance + in - out - refunds), never
 * hand-updated on a `balance` column.
 */
class CashMovementService
{
    /**
     * A manual cash withdrawal (paid-out) from the till drawer. Requires a
     * reason — the real audit trail, not just a free-text note. The drawer can
     * never go negative: a withdrawal that exceeds what is actually expected
     * in the till is refused up front.
     */
    public function withdraw(PosSession $session, float $amount, int $reasonId, ?string $note): CashMovement
    {
        if (! $session->isOpen()) {
            throw new RuntimeException('Cannot withdraw cash: the till session is not open.');
        }

        if ($amount <= 0) {
            throw new RuntimeException('Withdrawal amount must be greater than zero.');
        }

        $available = $this->expectedBalance($session);
        if ($amount > $available) {
            throw new RuntimeException(
                'Cannot withdraw more than the available till balance of '.number_format($available, 2).'.'
            );
        }

        return DB::transaction(function () use ($session, $amount, $reasonId, $note) {
            $movement = CashMovement::create([
                'pos_session_id' => $session->id,
                'type' => CashMovement::OUT,
                'source' => CashMovement::SOURCE_MANUAL,
                'amount' => $amount,
                'reason_id' => $reasonId,
                'note' => $note,
                'created_by' => Auth::id(),
                'created_at' => now(),
            ]);

            AuditLog::record('cash_movement.withdrawn', $movement, [
                'till_session_id' => $session->id,
                'amount' => (string) $amount,
                'reason' => $movement->reason?->name,
            ]);

            return $movement;
        });
    }

    /**
     * A manual cash-in (pay-in) to the till drawer — a float top-up, or petty
     * cash being returned. The reason is optional here, unlike a withdrawal:
     * money arriving in the drawer is far less sensitive than money leaving it.
     */
    public function cashIn(PosSession $session, float $amount, ?string $note, ?int $reasonId = null): CashMovement
    {
        if (! $session->isOpen()) {
            throw new RuntimeException('Cannot add cash: the till session is not open.');
        }

        if ($amount <= 0) {
            throw new RuntimeException('Cash-in amount must be greater than zero.');
        }

        return DB::transaction(function () use ($session, $amount, $note, $reasonId) {
            $movement = CashMovement::create([
                'pos_session_id' => $session->id,
                'type' => CashMovement::IN,
                'source' => CashMovement::SOURCE_MANUAL,
                'amount' => $amount,
                'reason_id' => $reasonId,
                'note' => $note,
                'created_by' => Auth::id(),
                'created_at' => now(),
            ]);

            AuditLog::record('cash_movement.cash_in', $movement, [
                'till_session_id' => $session->id,
                'amount' => (string) $amount,
                'reason' => $movement->reason?->name,
            ]);

            return $movement;
        });
    }

    /**
     * Records the cash portion of a completed sale — called automatically
     * from InvoiceService::create(), never user-facing. Only the amount
     * actually settled in cash enters the till; card/bank/cheque splits
     * never touch this ledger.
     */
    public function recordSaleCash(Invoice $invoice, float $cashAmount): void
    {
        if ($cashAmount <= 0.001 || ! $invoice->pos_session_id) {
            return;
        }

        CashMovement::create([
            'pos_session_id' => $invoice->pos_session_id,
            'type' => CashMovement::IN,
            'source' => CashMovement::SOURCE_SALE,
            'amount' => round($cashAmount, 2),
            'note' => "Sale {$invoice->invoice_number}",
            'created_by' => Auth::id(),
            'created_at' => now(),
        ]);
    }

    /**
     * Records the cash portion of a refund against a specific open till
     * session — called automatically from ReturnService::create(). Returns
     * don't carry their own `pos_session_id` (they're not always processed
     * at a till), so the caller resolves and passes the session explicitly.
     * The caller is responsible for refusing the refund up front when no
     * session is open for the branch — see ReturnService::create() — so an
     * open $session is guaranteed here, never optional.
     */
    public function recordRefundCash(ReturnDoc $return, PosSession $session, float $cashAmount): void
    {
        if ($cashAmount <= 0.001) {
            return;
        }

        CashMovement::create([
            'pos_session_id' => $session->id,
            'type' => CashMovement::CASH_REFUND,
            'source' => CashMovement::SOURCE_REFUND,
            'amount' => round($cashAmount, 2),
            'note' => "Return {$return->return_number}",
            'created_by' => Auth::id(),
            'created_at' => now(),
        ]);
    }

    /**
     * Expected cash in the drawer right now: opening balance plus every
     * cash movement recorded against the session since it opened.
     *
     * Single aggregated query (was 3 separate SUMs) — the cash_movements
     * table grows per shift and was the bottleneck for "owner takes out"
     * (cash withdrawal) latency.
     */
    public function expectedBalance(PosSession $session): float
    {
        $sums = DB::table('cash_movements')
            ->where('pos_session_id', $session->id)
            ->selectRaw("
                SUM(CASE WHEN type = '".CashMovement::IN."' THEN amount ELSE 0 END) as sum_in,
                SUM(CASE WHEN type = '".CashMovement::OUT."' THEN amount ELSE 0 END) as sum_out,
                SUM(CASE WHEN type = '".CashMovement::CASH_REFUND."' THEN amount ELSE 0 END) as sum_refunds
            ")
            ->first();

        $in = (float) ($sums->sum_in ?? 0);
        $out = (float) ($sums->sum_out ?? 0);
        $refunds = (float) ($sums->sum_refunds ?? 0);

        return round((float) $session->opening_balance + $in - $out - $refunds, 2);
    }
}
