<?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\Party;
use App\Models\Payment;
use App\Models\VendorBill;
use App\Services\Accounting\PaymentPostingService;
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 PaymentController extends Controller
{
    public function __construct(private readonly DocumentNumberService $numbers,private readonly ApprovalWorkflowService $workflow,private readonly PaymentPostingService $posting,private readonly PdfService $pdf){}
    public function index(Request $request):View{$this->permission($request,'view');$q=Payment::query()->with(['party','branch'])->where('company_id',$request->user()->company_id);$this->scope($request,$q);if($s=trim((string)$request->query('search')))$q->where(fn($q)=>$q->where('payment_number','like',"%$s%")->orWhere('reference_number','like',"%$s%")->orWhereHas('party',fn($p)=>$p->where('legal_name','like',"%$s%")));if($status=$request->query('status'))$q->where('status',$status);$payments=$q->latest('payment_date')->paginate(25)->withQueryString();return view('payments.index',compact('payments'));}
    public function create(Request $request):View{$this->permission($request,'create');$payment=new Payment(['payment_date'=>today(),'currency_code'=>'AED','exchange_rate'=>'1','payment_type'=>'vendor_payment','payment_method'=>'bank_transfer']);$payment->setRelation('allocations',collect());return $this->form($request,$payment);}
    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['payment_number']=$this->numbers->next((int)$request->user()->company_id,(int)$data['branch_id'],'PAY',$data['payment_date']);$payment=DB::transaction(function()use($data,$allocations){$p=Payment::query()->create($data);foreach($allocations as $billId=>$amount)$p->allocations()->create(['vendor_bill_id'=>$billId,'amount'=>$amount]);return $p;});return redirect()->route('payments.show',$payment)->with('success','Payment draft created.');}
    public function show(Request $request,Payment $payment):View{$this->access($request,$payment,'view');$payment->load(['company','branch','party','allocations.bill','creator','approver','journal.lines.account']);return view('payments.show',compact('payment'));}
    public function edit(Request $request,Payment $payment):View{$this->access($request,$payment,'edit_draft');$this->editable($payment);return $this->form($request,$payment->load('allocations'));}
    public function update(Request $request,Payment $payment):RedirectResponse{$this->access($request,$payment,'edit_draft');$this->editable($payment);[$data,$allocations]=$this->validated($request);$data+=['status'=>DocumentStatus::DRAFT,'approved_by'=>null,'approved_at'=>null];DB::transaction(function()use($payment,$data,$allocations){$payment->update($data);$payment->allocations()->delete();foreach($allocations as $billId=>$amount)$payment->allocations()->create(['vendor_bill_id'=>$billId,'amount'=>$amount]);});return redirect()->route('payments.show',$payment)->with('success','Payment updated.');}
    public function destroy(Request $request,Payment $payment):RedirectResponse{$this->access($request,$payment,'delete_draft');$this->editable($payment);$payment->delete();return redirect()->route('payments.index')->with('success','Payment draft deleted.');}
    public function submit(Request $request,Payment $payment):RedirectResponse{return $this->transition($request,$payment,'submit',fn()=> $this->workflow->submit($payment),'Payment submitted.');}
    public function approve(Request $request,Payment $payment):RedirectResponse{return $this->transition($request,$payment,'approve',fn()=> $this->workflow->approve($payment),'Payment approved.');}
    public function reject(Request $request,Payment $payment):RedirectResponse{$request->validate(['reason'=>'required|string|max:1000']);return $this->transition($request,$payment,'reject',fn()=> $this->workflow->reject($payment,$request->string('reason')->toString()),'Payment rejected.');}
    public function post(Request $request,Payment $payment):RedirectResponse{return $this->transition($request,$payment,'post',fn()=> $this->posting->post($payment),'Payment posted.');}
    public function print(Request $request,Payment $payment):View{$this->access($request,$payment,'print');$payment->load(['company','branch','party','allocations.bill']);return view('documents.payment',compact('payment'));}
    public function pdf(Request $request,Payment $payment):Response{$this->access($request,$payment,'print');$payment->load(['company','branch','party','allocations.bill']);return $this->pdf->download(view('documents.payment',compact('payment'))->render(),$payment->payment_number.'.pdf');}
    private function transition(Request $request,Payment $payment,string $action,callable $callback,string $message):RedirectResponse{$this->access($request,$payment,$action);try{$callback();}catch(Throwable $e){return back()->with('error',$e->getMessage());}return back()->with('success',$message);}
    private function form(Request $request, Payment $payment): 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);
        $bills = VendorBill::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));
            $bills->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();
        $bills = $bills->orderBy('party_id')->orderBy('due_date')->get()->filter(
            fn (VendorBill $bill): bool => Decimal::compare(
                Decimal::subtract(
                    (string) $bill->grand_total,
                    Decimal::add((string) $bill->paid_total, (string) $bill->allocated_total),
                ),
                '0',
            ) > 0,
        );

        return view('payments.form', compact('payment', 'branches', 'parties', 'accounts', 'bills'));
    }
    /** @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','payment_type'=>['required',Rule::in(['vendor_payment','customer_refund','other_payment'])],'payment_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','payable_account_id'=>'required|integer','vendor_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 payment 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['payment_type'] === 'vendor_payment') {
            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.');
        }
        if ($data['payment_type'] === 'customer_refund') {
            abort_unless($party->roles->contains(fn ($role) => ($role->role->value ?? $role->role) === 'customer'), 422, 'The selected party is not a customer.');
        }

        $accountIds = collect([$data['cash_bank_account_id'], $data['payable_account_id'], $data['vendor_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 $billId => $amount) {
            if (Decimal::compare((string) ($amount ?? '0'), '0') <= 0) {
                continue;
            }
            $billQuery = VendorBill::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')) {
                $billQuery->where('branch_id', $request->user()->branch_id);
            }
            $bill = $billQuery->findOrFail($billId);
            $available = Decimal::subtract((string) $bill->grand_total, Decimal::add((string) $bill->paid_total, (string) $bill->allocated_total));
            $amount = Decimal::scale((string) $amount);
            if (Decimal::compare($amount, $available) > 0) {
                throw new DomainException('An allocation exceeds the selected vendor bill balance.');
            }
            $allocations[(int) $billId] = $amount;
            $sum = Decimal::add($sum, $amount);
        }
        if ($data['payment_type'] !== 'vendor_payment' && $allocations !== []) {
            throw new DomainException('Only vendor payments can be allocated to vendor bills.');
        }
        if (Decimal::compare($sum, (string) $data['amount']) > 0) {
            throw new DomainException('Total allocation exceeds the payment amount.');
        }
        if ($data['payment_type'] === 'vendor_payment' && Decimal::compare($sum, (string) $data['amount']) < 0 && empty($data['vendor_advance_account_id'])) {
            throw new DomainException('Select a vendor advance account for the unallocated payment amount.');
        }
        unset($data['allocations']);

        return [$data, $allocations];
    }
    private function editable(Payment $payment):void{$s=$payment->status instanceof DocumentStatus?$payment->status:DocumentStatus::from($payment->status);if(!in_array($s,[DocumentStatus::DRAFT,DocumentStatus::REJECTED],true))throw new DomainException('Only draft or rejected payments can be edited.');}
    private function permission(Request $request,string $action):void{abort_unless($request->user()->hasPermission('payments.'.$action),403);}
    private function access(Request $request,Payment $payment,string $action):void{$this->permission($request,$action);abort_unless((int)$payment->company_id===(int)$request->user()->company_id,404);if($request->user()->branch_id&&!$request->user()->hasPermission('branches.view_all'))abort_unless((int)$payment->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);}
}
