<?php

namespace App\Http\Controllers\Finance;

use App\Enums\DocumentStatus;
use App\Http\Controllers\Controller;
use App\Models\Account;
use App\Models\Branch;
use App\Models\CustomerInvoice;
use App\Models\Party;
use App\Models\Receipt;
use App\Services\Accounting\ReceiptPostingService;
use App\Services\Documents\DocumentNumberService;
use App\Services\Documents\PdfService;
use App\Services\Workflow\ApprovalWorkflowService;
use App\Support\Decimal;
use DomainException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

final class ReceiptController extends Controller
{
    public function __construct(private readonly DocumentNumberService $numbers,private readonly ApprovalWorkflowService $workflow,private readonly ReceiptPostingService $posting,private readonly PdfService $pdf){}

    public function index(Request $request): View
    {
        $this->permission($request,'view');$query=Receipt::query()->with(['party','branch'])->where('company_id',$request->user()->company_id);$this->scope($request,$query);if($s=trim((string)$request->query('search')))$query->where(fn($q)=>$q->where('receipt_number','like',"%$s%")->orWhere('reference_number','like',"%$s%")->orWhereHas('party',fn($p)=>$p->where('legal_name','like',"%$s%")));if($status=$request->query('status'))$query->where('status',$status);$receipts=$query->latest('receipt_date')->paginate(25)->withQueryString();return view('receipts.index',compact('receipts'));
    }

    public function create(Request $request): View {$this->permission($request,'create');$receipt=new Receipt(['receipt_date'=>today(),'currency_code'=>'AED','exchange_rate'=>'1','receipt_type'=>'customer_receipt','payment_method'=>'bank_transfer']);$receipt->setRelation('allocations',collect());return $this->form($request,$receipt);}

    public function store(Request $request): RedirectResponse
    {
        $this->permission($request,'create');[$data,$allocations]=$this->validated($request);$data+=['company_id'=>$request->user()->company_id,'created_by'=>$request->user()->id,'status'=>DocumentStatus::DRAFT,'allocated_total'=>'0'];$data['receipt_number']=$this->numbers->next((int)$request->user()->company_id,(int)$data['branch_id'],'RCPT',$data['receipt_date']);$receipt=DB::transaction(function()use($data,$allocations){$r=Receipt::query()->create($data);foreach($allocations as $invoiceId=>$amount)$r->allocations()->create(['customer_invoice_id'=>$invoiceId,'amount'=>$amount]);return $r;});return redirect()->route('receipts.show',$receipt)->with('success','Receipt draft created.');
    }

    public function show(Request $request,Receipt $receipt): View {$this->access($request,$receipt,'view');$receipt->load(['company','branch','party','allocations.invoice','creator','approver','journal.lines.account']);return view('receipts.show',compact('receipt'));}
    public function edit(Request $request,Receipt $receipt): View {$this->access($request,$receipt,'edit_draft');$this->editable($receipt);return $this->form($request,$receipt->load('allocations'));}
    public function update(Request $request,Receipt $receipt): RedirectResponse {$this->access($request,$receipt,'edit_draft');$this->editable($receipt);[$data,$allocations]=$this->validated($request);$data+=['status'=>DocumentStatus::DRAFT,'approved_by'=>null,'approved_at'=>null];DB::transaction(function()use($receipt,$data,$allocations){$receipt->update($data);$receipt->allocations()->delete();foreach($allocations as $invoiceId=>$amount)$receipt->allocations()->create(['customer_invoice_id'=>$invoiceId,'amount'=>$amount]);});return redirect()->route('receipts.show',$receipt)->with('success','Receipt updated.');}
    public function destroy(Request $request,Receipt $receipt): RedirectResponse {$this->access($request,$receipt,'delete_draft');$this->editable($receipt);$receipt->delete();return redirect()->route('receipts.index')->with('success','Receipt draft deleted.');}
    public function submit(Request $request,Receipt $receipt): RedirectResponse{return $this->transition($request,$receipt,'submit',fn()=> $this->workflow->submit($receipt),'Receipt submitted.');}
    public function approve(Request $request,Receipt $receipt): RedirectResponse{return $this->transition($request,$receipt,'approve',fn()=> $this->workflow->approve($receipt),'Receipt approved.');}
    public function reject(Request $request,Receipt $receipt): RedirectResponse{$request->validate(['reason'=>'required|string|max:1000']);return $this->transition($request,$receipt,'reject',fn()=> $this->workflow->reject($receipt,$request->string('reason')->toString()),'Receipt rejected.');}
    public function post(Request $request,Receipt $receipt): RedirectResponse{return $this->transition($request,$receipt,'post',fn()=> $this->posting->post($receipt),'Receipt posted.');}
    public function print(Request $request,Receipt $receipt): View{$this->access($request,$receipt,'print');$receipt->load(['company','branch','party','allocations.invoice']);return view('documents.receipt',compact('receipt'));}
    public function pdf(Request $request,Receipt $receipt): Response{$this->access($request,$receipt,'print');$receipt->load(['company','branch','party','allocations.invoice']);return $this->pdf->download(view('documents.receipt',compact('receipt'))->render(),$receipt->receipt_number.'.pdf');}

    private function transition(Request $request,Receipt $receipt,string $action,callable $callback,string $message):RedirectResponse{$this->access($request,$receipt,$action);try{$callback();}catch(Throwable $e){return back()->with('error',$e->getMessage());}return back()->with('success',$message);}
    private function form(Request $request, Receipt $receipt): View
    {
        $companyId = (int) $request->user()->company_id;
        $branches = Branch::query()->where('company_id', $companyId)->where('is_active', true);
        $parties = Party::query()->where('company_id', $companyId)->where('is_active', true);
        $invoices = CustomerInvoice::query()
            ->with('party')
            ->where('company_id', $companyId)
            ->whereIn('status', [DocumentStatus::POSTED->value, DocumentStatus::PARTIALLY_PAID->value]);

        if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
            $branchId = (int) $request->user()->branch_id;
            $branches->whereKey($branchId);
            $parties->where(fn ($query) => $query->whereNull('assigned_branch_id')->orWhere('assigned_branch_id', $branchId));
            $invoices->where('branch_id', $branchId);
        }

        $branches = $branches->orderBy('code')->get();
        $parties = $parties->orderBy('legal_name')->get();
        $accounts = Account::query()->where('company_id', $companyId)->where('is_active', true)->orderBy('code')->get();
        $invoices = $invoices->orderBy('party_id')->orderBy('due_date')->get()->filter(
            fn (CustomerInvoice $invoice): bool => Decimal::compare(
                Decimal::subtract(
                    (string) $invoice->grand_total,
                    Decimal::add((string) $invoice->paid_total, (string) $invoice->allocated_total),
                ),
                '0',
            ) > 0,
        );

        return view('receipts.form', compact('receipt', 'branches', 'parties', 'accounts', 'invoices'));
    }

    /** @return array{0:array<string,mixed>,1:array<int,string>} */
    private function validated(Request $request): array
    {
        $data = $request->validate([
            'branch_id' => 'required|integer',
            'party_id' => 'required|integer',
            'receipt_type' => ['required', Rule::in(['customer_receipt', 'vendor_refund', 'other_receipt'])],
            'receipt_date' => 'required|date',
            'amount' => 'required|numeric|gt:0',
            'currency_code' => 'required|string|size:3',
            'exchange_rate' => 'required|numeric|gt:0',
            'payment_method' => 'required|string|max:30',
            'cash_bank_account_id' => 'required|integer',
            'receivable_account_id' => 'required|integer',
            'customer_advance_account_id' => 'nullable|integer',
            'reference_number' => 'nullable|string|max:150',
            'description' => 'nullable|string',
            'allocations' => 'nullable|array',
            'allocations.*' => 'nullable|numeric|min:0',
        ]);

        $companyId = (int) $request->user()->company_id;
        abort_unless(Branch::query()->where('company_id', $companyId)->whereKey($data['branch_id'])->where('is_active', true)->exists(), 422, 'Invalid branch.');
        if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
            abort_unless((int) $data['branch_id'] === (int) $request->user()->branch_id, 403, 'You cannot create a receipt for another branch.');
        }

        $partyQuery = Party::query()->where('company_id', $companyId)->where('is_active', true)->with('roles');
        if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
            $partyQuery->where(fn ($query) => $query->whereNull('assigned_branch_id')->orWhere('assigned_branch_id', $request->user()->branch_id));
        }
        $party = $partyQuery->findOrFail($data['party_id']);
        if ($data['receipt_type'] === 'customer_receipt') {
            abort_unless($party->roles->contains(fn ($role) => ($role->role->value ?? $role->role) === 'customer'), 422, 'The selected party is not a customer.');
        }
        if ($data['receipt_type'] === 'vendor_refund') {
            abort_unless($party->roles->contains(fn ($role) => in_array(($role->role->value ?? $role->role), ['vendor', 'subcontractor', 'vehicle_owner'], true)), 422, 'The selected party is not a vendor.');
        }

        $accountIds = collect([$data['cash_bank_account_id'], $data['receivable_account_id'], $data['customer_advance_account_id'] ?? null])->filter()->unique();
        abort_unless(Account::query()->where('company_id', $companyId)->whereIn('id', $accountIds)->count() === $accountIds->count(), 422, 'Invalid account selection.');

        $allocations = [];
        $sum = Decimal::scale('0');
        foreach (($data['allocations'] ?? []) as $invoiceId => $amount) {
            if (Decimal::compare((string) ($amount ?? '0'), '0') <= 0) {
                continue;
            }
            $invoiceQuery = CustomerInvoice::query()
                ->where('company_id', $companyId)
                ->where('party_id', $data['party_id'])
                ->whereIn('status', [DocumentStatus::POSTED->value, DocumentStatus::PARTIALLY_PAID->value]);
            if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
                $invoiceQuery->where('branch_id', $request->user()->branch_id);
            }
            $invoice = $invoiceQuery->findOrFail($invoiceId);
            $available = Decimal::subtract($invoice->grand_total, Decimal::add($invoice->paid_total, $invoice->allocated_total));
            $amount = Decimal::scale((string) $amount);
            if (Decimal::compare($amount, $available) > 0) {
                throw new DomainException('An allocation exceeds the selected invoice balance.');
            }
            $allocations[(int) $invoiceId] = $amount;
            $sum = Decimal::add($sum, $amount);
        }
        if ($data['receipt_type'] !== 'customer_receipt' && $allocations !== []) {
            throw new DomainException('Only customer receipts can be allocated to customer invoices.');
        }
        if (Decimal::compare($sum, (string) $data['amount']) > 0) {
            throw new DomainException('Total allocation exceeds the receipt amount.');
        }
        if ($data['receipt_type'] === 'customer_receipt' && Decimal::compare($sum, (string) $data['amount']) < 0 && empty($data['customer_advance_account_id'])) {
            throw new DomainException('Select a customer advance account for the unallocated receipt amount.');
        }
        unset($data['allocations']);

        return [$data, $allocations];
    }

    private function editable(Receipt $receipt):void{$s=$receipt->status instanceof DocumentStatus?$receipt->status:DocumentStatus::from($receipt->status);if(!in_array($s,[DocumentStatus::DRAFT,DocumentStatus::REJECTED],true))throw new DomainException('Only draft or rejected receipts can be edited.');}
    private function permission(Request $request,string $action):void{abort_unless($request->user()->hasPermission('receipts.'.$action),403);}
    private function access(Request $request,Receipt $receipt,string $action):void{$this->permission($request,$action);abort_unless((int)$receipt->company_id===(int)$request->user()->company_id,404);if($request->user()->branch_id&&!$request->user()->hasPermission('branches.view_all'))abort_unless((int)$receipt->branch_id===(int)$request->user()->branch_id,403);}
    private function scope(Request $request,$query):void{if($request->user()->branch_id&&!$request->user()->hasPermission('branches.view_all'))$query->where('branch_id',$request->user()->branch_id);}
}
