<?php

namespace App\Http\Controllers;

use App\Models\TradeDocument;
use App\Models\TradeDocumentPayment;
use App\Models\DocumentNumberSetting;
use App\Models\DocumentTemplate;
use App\Models\Transaction;
use App\Models\Product;
use App\Models\AuditLog;
use App\Models\OperationalDocument;
use App\Services\TradeDocumentService;
use App\Services\SalesOrderPaymentService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\View\View;

class TradeDocumentController extends Controller
{
    public function addLine(Request $request, TradeDocument $document): RedirectResponse
    {
        $this->authorizeBusiness($document);
        abort_if(in_array($document->status, ['approved', 'accepted', 'paid', 'cancelled']), 422, 'Locked documents cannot be edited.');
        $data = $request->validate([
            'product_id' => 'nullable|integer|exists:products,id',
            'description' => 'required|string|max:255',
            'quantity' => 'required|numeric|min:0.001',
            'unit_price' => 'required|numeric|min:0',
            'discount' => 'nullable|numeric|min:0',
            'tax' => 'nullable|numeric|min:0',
        ]);
        if (!empty($data['product_id'])) {
            abort_unless(Product::whereKey($data['product_id'])->where('business_id', $document->business_id)->exists(), 403);
        }
        $data['discount'] ??= 0; $data['tax'] ??= 0;
        $data['line_total'] = max(0, $data['quantity'] * $data['unit_price'] - $data['discount'] + $data['tax']);
        $document->lines()->create($data);
        $this->recalculate($document);
        return back()->with('success', 'Document line added.');
    }

    public function removeLine(TradeDocument $document, int $line): RedirectResponse
    {
        $this->authorizeBusiness($document);
        abort_if(in_array($document->status, ['approved', 'accepted', 'paid', 'cancelled']), 422, 'Locked documents cannot be edited.');
        $document->lines()->whereKey($line)->firstOrFail()->delete();
        $this->recalculate($document);
        return back()->with('success', 'Document line removed.');
    }

    public function transition(Request $request, TradeDocument $document): RedirectResponse
    {
        $this->authorizeBusiness($document);
        $target = $request->validate([
            'status' => ['required', Rule::in(['draft','pending_approval','approved','sent','accepted','rejected','paid','cancelled'])],
            'note' => 'nullable|string',
        ])['status'];
        $allowed = [
            'draft' => ['pending_approval','cancelled'], 'pending_approval' => ['approved','rejected'],
            'approved' => ['sent','cancelled'], 'rejected' => ['pending_approval','cancelled'], 'sent' => ['accepted','rejected'],
            'accepted' => ['paid'], 'paid' => [], 'cancelled' => [],
        ];
        abort_unless(in_array($target, $allowed[$document->status] ?? [], true), 422, 'Invalid document status transition.');
        if (in_array($target, ['approved','rejected'], true)) {
            $requester = $document->requester;
            $user = $request->user();
            $isManager = $requester && (int) $requester->reporting_manager_id === (int) $user->id;
            abort_unless($user->hasRole('admin') || $user->can('approve_trade_documents') || $isManager, 403, 'Only the requester’s reporting manager or an authorized approver may decide this document.');
            abort_if((int)$document->requested_by === (int)$user->id && !$user->hasRole('admin'), 403, 'Requesters cannot approve their own document.');
        }
        if ($target === 'rejected') $request->validate(['note'=>'required|string|max:2000']);
        $from = $document->status;
        $values = ['status' => $target];
        if ($target === 'pending_approval') $values += [
            'requested_by'=>auth()->id(),'requested_at'=>now(),
            'resubmission_count'=>$from === 'rejected' ? $document->resubmission_count + 1 : $document->resubmission_count,
            'rejection_reason'=>null,
        ];
        if ($target === 'approved') $values += ['approved_by' => auth()->id(), 'approved_at' => now(), 'approval_note' => $request->note];
        if ($target === 'rejected') $values += ['rejection_reason'=>$request->note,'customer_decision'=>null];
        if ($target === 'paid') $values += ['payment_status' => 'paid', 'paid_amount' => $document->total, 'paid_at' => now()];
        $document->update($values);
        $document->approvalHistories()->create(['user_id'=>auth()->id(),'action'=>$target,'from_status'=>$from,'to_status'=>$target,'comment'=>$request->note]);
        return back()->with('success', 'Document status updated.');
    }

    public function convert(Request $request, TradeDocument $document, TradeDocumentService $service): RedirectResponse
    {
        $this->authorizeBusiness($document);
        $target = $request->validate(['type' => ['required', Rule::in(['quotation','invoice'])]])['type'];
        abort_unless(($document->type === 'proposal' && $target === 'quotation') ||
            ($document->type === 'quotation' && $target === 'invoice'), 422, 'Invalid document conversion.');
        $converted = DB::transaction(function () use ($document, $target, $service) {
            $copy = $document->replicate(['share_token','approved_by','approved_at','approval_note','paid_at']);
            $copy->type = $target; $copy->parent_document_id = $document->id;
            $copy->document_number = $service->nextNumber($document->business_id, $target);
            $copy->status = 'draft'; $copy->payment_status = 'unpaid'; $copy->paid_amount = 0; $copy->version = 1;
            $copy->save();
            foreach ($document->lines as $line) $copy->lines()->create($line->only(['product_id','description','quantity','unit_price','discount','tax','line_total']));
            return $copy;
        });
        return redirect()->route('workflow.index', 'trade-documents')->with('success', ucfirst($target).' '.$converted->document_number.' created.');
    }

    public function share(TradeDocument $document): RedirectResponse
    {
        $this->authorizeBusiness($document);
        $document->update([
            'share_token' => $document->share_token ?: (string) Str::uuid(),
            'acceptance_token' => $document->acceptance_token ?: (string) Str::uuid(),
        ]);
        return back()->with('success', 'Public link: '.route('trade-documents.public', $document->share_token));
    }

    public function publicView(string $token): View
    {
        $document = TradeDocument::withoutGlobalScopes()->with(['deal','lines.product','template','payments'])->where('share_token', $token)->firstOrFail();
        $this->auditPublicAccess($document, 'document_public_access');
        return view('trade-documents.public', compact('document'));
    }

    public function customerDecision(Request $request, string $token): RedirectResponse
    {
        $document=TradeDocument::withoutGlobalScopes()->where('acceptance_token',$token)->firstOrFail();
        abort_unless($document->status === 'sent', 422, 'This document is no longer awaiting a customer decision.');
        $data=$request->validate(['decision'=>'required|in:accepted,rejected','comment'=>'nullable|string|max:2000']);
        $old = $document->only(['status','customer_decision','customer_comment','customer_decided_at']);
        $document->update(['status'=>$data['decision'],'customer_decision'=>$data['decision'],'customer_comment'=>$data['comment']??null,'customer_decided_at'=>now()]);
        $document->approvalHistories()->create(['action'=>'customer_'.$data['decision'],'from_status'=>'sent','to_status'=>$data['decision'],'comment'=>$data['comment']??null]);
        $this->auditPublicAccess($document, 'document_customer_decision', $old, $document->only(array_keys($old)));
        return back()->with('success','Your decision has been recorded.');
    }

    public function email(Request $request, TradeDocument $document, TradeDocumentService $service): RedirectResponse
    {
        $this->authorizeBusiness($document);
        abort_unless(in_array($document->status, ['approved','sent'], true), 422, 'Only approved documents can be emailed to customers.');
        $data=$request->validate(['recipient'=>'required|email','message'=>'nullable|string|max:3000']);
        $document->update(['share_token'=>$document->share_token ?: (string)Str::uuid(),'acceptance_token'=>$document->acceptance_token ?: (string)Str::uuid(),'customer_email'=>$data['recipient']]);
        $delivery=$document->deliveries()->create(['sent_by'=>auth()->id(),'recipient'=>$data['recipient'],'status'=>'pending']);
        try {
            $pdf=$service->brandedPdf($document);
            Mail::html(($data['message']??'Please review the attached document.').'<p><a href="'.route('trade-documents.public',$document->share_token).'">Review and respond</a></p>',
                fn($mail)=>$mail->to($data['recipient'])->subject(ucfirst($document->type).' '.$document->document_number)->attachData($pdf,$document->document_number.'.pdf',['mime'=>'application/pdf']));
            $delivery->update(['status'=>'sent','sent_at'=>now()]); $document->update(['status'=>'sent','sent_at'=>now()]);
        } catch (\Throwable $e) {
            $delivery->update(['status'=>'failed','error'=>$e->getMessage()]);
            return back()->withErrors(['email'=>'Delivery failed: '.$e->getMessage()]);
        }
        return back()->with('success','Document emailed successfully.');
    }

    public function payment(Request $request, TradeDocument $document): RedirectResponse
    {
        $this->authorizeBusiness($document);
        abort_unless($document->type === 'invoice' && in_array($document->status,['accepted','paid'],true),422,'Payments can only be recorded for accepted invoices.');
        $data=$this->validatedPayment($request,$document,true);
        $payment = DB::transaction(function()use($document,$data){
            $locked=TradeDocument::whereKey($document->id)->lockForUpdate()->firstOrFail();
            $this->assertUniquePaymentReference($locked,$data['reference']??null);
            $remaining=$this->minor($locked->total,$locked)-$this->netPaidMinor($locked);
            abort_if($this->minor($data['amount'],$locked)>$remaining,422,'Payment exceeds outstanding balance.');
            $payment = $locked->payments()->create($data+[
                'business_id'=>$locked->business_id,'currency'=>$locked->currency,
                'payment_type'=>'payment','status'=>'posted','recorded_by'=>auth()->id(),
            ]);
            $this->refreshPaymentTotals($locked);
            return $payment;
        });
        $payment->update([
            'receipt_number' => 'RCT-'.now()->format('Ymd').'-'.str_pad((string) $payment->id, 6, '0', STR_PAD_LEFT),
            'receipt_issued_at' => now(),
        ]);
        $this->syncOperationalInvoice($document->fresh());
        return back()->with('success','Payment installment recorded. Receipt '.$payment->receipt_number.' issued.');
    }

    public function paymentHistory(TradeDocument $document): View
    {
        $this->authorizeBusiness($document);
        abort_unless($document->type==='invoice',404);
        $document->load(['deal','payments'=>fn($query)=>$query->with(['recorder','corrector','voider','parentPayment'])->orderByDesc('paid_at')->orderByDesc('id')]);
        return view('trade-documents.payments',compact('document'));
    }

    public function reconcilePayment(Request $request, TradeDocument $document, TradeDocumentPayment $payment): RedirectResponse
    {
        $this->authorizePayment($document, $payment);
        abort_unless($request->user()->is_admin || $request->user()->can('update_trade_documents'), 403);
        abort_unless($payment->payment_type === 'payment' && $payment->status === 'posted', 422, 'Only posted payments can be reconciled.');
        $data = $request->validate(['reconciliation_status'=>['required', Rule::in(['reconciled','rejected'])], 'bank_reference'=>'nullable|required_if:reconciliation_status,reconciled|string|max:255', 'note'=>'nullable|string|max:2000']);
        $payment->update(['reconciliation_status'=>$data['reconciliation_status'],'bank_reference'=>$data['bank_reference'] ?? null,'reconciled_at'=>now(),'reconciled_by'=>$request->user()->id,'notes'=>trim(($payment->notes ? $payment->notes."\n" : '').($data['note'] ?? '')) ?: null]);
        return back()->with('success', 'Payment reconciliation recorded.');
    }

    public function receipt(TradeDocument $document, TradeDocumentPayment $payment): View
    {
        $this->authorizeBusiness($document);
        abort_unless($document->type === 'invoice' && (int) $payment->trade_document_id === (int) $document->id, 404);
        abort_unless($payment->payment_type === 'payment' && $payment->status === 'posted' && $payment->receipt_number, 422, 'A receipt is available only for posted payments.');
        $payment->load('recorder');
        return view('trade-documents.receipt', compact('document', 'payment'));
    }

    private function syncOperationalInvoice(TradeDocument $document): void
    {
        $invoice = OperationalDocument::where('trade_document_id',$document->id)->where('type','invoices')->first();
        if (! $invoice) return;
        $details = $invoice->details ?? [];
        $details['amount_paid'] = $document->paid_amount;
        $details['balance_due'] = max(0, $document->total - $document->paid_amount);
        $details['payment_status'] = $document->payment_status;
        $invoiceStatus = $document->payment_status === 'paid'
            ? 'paid'
            : ($document->paid_amount > 0 ? 'partially_paid' : (in_array($invoice->status, ['paid', 'partially_paid'], true) ? 'sent' : $invoice->status));
        $invoice->update(['details'=>$details,'status'=>$invoiceStatus]);
        app(SalesOrderPaymentService::class)->recalculateFromInvoice($invoice->fresh(), auth()->id());
    }

    public function correctPayment(Request $request,TradeDocument $document,TradeDocumentPayment $payment): RedirectResponse
    {
        $this->authorizePayment($document,$payment);
        abort_unless($payment->payment_type==='payment'&&$payment->status==='posted',422,'Only posted payments can be corrected.');
        abort_if($payment->adjustments()->where('status','posted')->exists(),422,'A payment with refunds or reversals cannot be corrected.');
        $data=$this->validatedPayment($request,$document,true)+$request->validate(['reason'=>'required|string|min:5|max:2000']);
        DB::transaction(function()use($document,$payment,$data){
            $lockedDocument=TradeDocument::whereKey($document->id)->lockForUpdate()->firstOrFail();
            $lockedPayment=TradeDocumentPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();
            $this->assertUniquePaymentReference($lockedDocument,$data['reference']??null,$lockedPayment->id);
            $otherPaid=$this->netPaidMinor($lockedDocument,$lockedPayment->id);
            abort_if($otherPaid+$this->minor($data['amount'],$lockedDocument)>$this->minor($lockedDocument->total,$lockedDocument),422,'Corrected payment exceeds the invoice balance.');
            $reason=$data['reason']; unset($data['reason']);
            $lockedPayment->update($data+['corrected_by'=>auth()->id(),'corrected_at'=>now(),'correction_reason'=>$reason]);
            $this->refreshPaymentTotals($lockedDocument);
        });
        $this->syncOperationalInvoice($document->fresh());
        return back()->with('success','Payment correction recorded.');
    }

    public function voidPayment(Request $request,TradeDocument $document,TradeDocumentPayment $payment): RedirectResponse
    {
        $this->authorizePayment($document,$payment);
        $data=$request->validate(['reason'=>'required|string|min:5|max:2000']);
        abort_unless($payment->payment_type==='payment'&&$payment->status==='posted',422,'Only posted payments can be voided.');
        abort_if($payment->adjustments()->where('status','posted')->exists(),422,'Reverse or void the related adjustments first.');
        DB::transaction(function()use($document,$payment,$data){
            $locked=TradeDocumentPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();
            abort_unless($locked->status==='posted',409,'Payment was already changed.');
            $locked->update(['status'=>'voided','voided_by'=>auth()->id(),'voided_at'=>now(),'void_reason'=>$data['reason']]);
            $this->refreshPaymentTotals(TradeDocument::whereKey($document->id)->lockForUpdate()->firstOrFail());
        });
        $this->syncOperationalInvoice($document->fresh());
        return back()->with('success','Payment voided and invoice balance recalculated.');
    }

    public function refundPayment(Request $request,TradeDocument $document,TradeDocumentPayment $payment): RedirectResponse
    {
        $this->authorizePayment($document,$payment);
        abort_unless($payment->payment_type==='payment'&&$payment->status==='posted',422,'Refunds require a posted payment.');
        $data=$this->validatedPayment($request,$document,true);
        DB::transaction(function()use($document,$payment,$data){
            $lockedDocument=TradeDocument::whereKey($document->id)->lockForUpdate()->firstOrFail();
            $lockedPayment=TradeDocumentPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();
            $this->assertUniquePaymentReference($lockedDocument,$data['reference']??null);
            $adjusted=$lockedPayment->adjustments()->where('status','posted')->get()
                ->sum(fn($item)=>$this->minor($item->amount,$lockedDocument));
            abort_if($this->minor($data['amount'],$lockedDocument)>$this->minor($lockedPayment->amount,$lockedDocument)-$adjusted,422,'Refund exceeds the remaining refundable amount.');
            $lockedDocument->payments()->create($data+[
                'business_id'=>$lockedDocument->business_id,'currency'=>$lockedDocument->currency,
                'payment_type'=>'refund','status'=>'posted','parent_payment_id'=>$lockedPayment->id,'recorded_by'=>auth()->id(),
            ]);
            $this->refreshPaymentTotals($lockedDocument);
        });
        $this->syncOperationalInvoice($document->fresh());
        return back()->with('success','Refund recorded and invoice balance recalculated.');
    }

    public function reversePayment(Request $request,TradeDocument $document,TradeDocumentPayment $payment): RedirectResponse
    {
        $this->authorizePayment($document,$payment);
        $data=$request->validate(['reason'=>'required|string|min:5|max:2000','reference'=>'nullable|string|max:255','paid_at'=>'nullable|date']);
        abort_unless($payment->payment_type==='payment'&&$payment->status==='posted',422,'Only a posted payment can be reversed.');
        DB::transaction(function()use($document,$payment,$data){
            $lockedDocument=TradeDocument::whereKey($document->id)->lockForUpdate()->firstOrFail();
            $lockedPayment=TradeDocumentPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();
            abort_if($lockedPayment->adjustments()->where('status','posted')->exists(),422,'This payment already has a refund or reversal.');
            $reference=filled($data['reference']??null)?trim($data['reference']):'REV-'.$lockedPayment->id.'-'.now()->format('YmdHis');
            $this->assertUniquePaymentReference($lockedDocument,$reference);
            $lockedDocument->payments()->create([
                'business_id'=>$lockedDocument->business_id,'currency'=>$lockedDocument->currency,
                'amount'=>$lockedPayment->amount,'payment_type'=>'reversal','status'=>'posted',
                'parent_payment_id'=>$lockedPayment->id,'method'=>$lockedPayment->method,'reference'=>$reference,
                'paid_at'=>$data['paid_at']??now(),'notes'=>$data['reason'],'recorded_by'=>auth()->id(),
            ]);
            $this->refreshPaymentTotals($lockedDocument);
        });
        $this->syncOperationalInvoice($document->fresh());
        return back()->with('success','Payment reversal recorded.');
    }

    public function configure(Request $request): RedirectResponse
    {
        abort_unless($request->user()->hasRole('admin') || $request->user()->can('configure_trade_documents'),403);
        $data=$request->validate(['type'=>'required|in:proposal,quotation,invoice','prefix'=>'required|max:30','next_number'=>'required|integer|min:1','padding'=>'required|integer|min:1|max:12','separator'=>'required|max:3']);
        DocumentNumberSetting::updateOrCreate(['business_id'=>$request->user()->business_id,'type'=>$data['type']],$data);
        return back()->with('success','Sequential numbering configuration saved.');
    }

    public function template(Request $request): RedirectResponse
    {
        abort_unless($request->user()->hasRole('admin') || $request->user()->can('configure_trade_documents'),403);
        $data=$request->validate(['name'=>'required|max:255','type'=>'required|in:all,proposal,quotation,invoice','accent_color'=>'required|regex:/^#[0-9A-Fa-f]{6}$/','header_text'=>'nullable|string','footer_text'=>'nullable|string','is_default'=>'nullable|boolean']);
        $data['business_id']=$request->user()->business_id; $data['is_default']=$request->boolean('is_default');
        if($data['is_default']) DocumentTemplate::where('type',$data['type'])->update(['is_default'=>false]);
        DocumentTemplate::create($data);
        return back()->with('success','Branded document template created.');
    }

    public function syncLegacy(TradeDocumentService $service): RedirectResponse
    {
        abort_unless(auth()->user()->hasRole('admin') || auth()->user()->can('configure_trade_documents'), 403);
        $count=0;
        Transaction::where('business_id',auth()->user()->business_id)->whereNull('trade_document_id')->orderBy('id')->each(function($tx)use(&$count,$service){
            $type=$tx->invoice_id?'invoice':($tx->proposal_id?'proposal':'quotation');
            $doc=TradeDocument::create(['business_id'=>$tx->business_id,'deal_id'=>$tx->deal_id,'created_by'=>$tx->owner_id,'type'=>$type,
                'document_number'=>$service->nextNumber($tx->business_id,$type),'status'=>'draft','currency'=>'USD','subtotal'=>$tx->subtotal??0,
                'discount'=>$tx->discount??0,'tax'=>$tx->tax??0,'total'=>$tx->grand_total??0,'issue_date'=>$tx->transaction_date?:now(),
                'due_date'=>$tx->due_date,'notes'=>$tx->notes,'transaction_id'=>$tx->id]);
            $tx->update(['trade_document_id'=>$doc->id]);$count++;
        });
        return back()->with('success',$count.' legacy transaction(s) unified with Trade Documents.');
    }

    public function pdf(TradeDocument $document, TradeDocumentService $service)
    {
        $this->authorizeBusiness($document);
        return response($service->brandedPdf($document), 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'attachment; filename="'.$document->document_number.'.pdf"',
        ]);
    }

    public function word(TradeDocument $document, TradeDocumentService $service)
    {
        $this->authorizeBusiness($document);
        return response($service->nativeDocx($document), 200, [
            'Content-Type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'Content-Disposition' => 'attachment; filename="'.$document->document_number.'.docx"',
        ]);
    }

    public function csv(TradeDocument $document)
    {
        $this->authorizeBusiness($document); $document->load('lines');
        return response()->streamDownload(function () use ($document) {
            $out = fopen('php://output', 'w');
            fputcsv($out, ['Description','Quantity','Unit Price','Discount','Tax','Line Total']);
            foreach ($document->lines as $line) fputcsv($out, [$line->description,$line->quantity,$line->unit_price,$line->discount,$line->tax,$line->line_total]);
            fputcsv($out, []); fputcsv($out, ['Total','','','','',$document->total]);
            fclose($out);
        }, $document->document_number.'.csv', ['Content-Type' => 'text/csv']);
    }

    private function recalculate(TradeDocument $document): void
    {
        $subtotal = $document->lines()->sum(DB::raw('quantity * unit_price'));
        $discount = $document->lines()->sum('discount'); $tax = $document->lines()->sum('tax');
        $document->update(compact('subtotal','discount','tax') + ['total' => max(0, $subtotal - $discount + $tax)]);
    }

    private function validatedPayment(Request $request,TradeDocument $document,bool $requireReference=false): array
    {
        $scale=$this->currencyScale($document);
        $pattern=$scale===0?'/^\d+$/':'/^\d+(?:\.\d{1,'.$scale.'})?$/';
        $minimum=$scale===0?1:1/(10**$scale);
        $data=$request->validate([
            'amount'=>['required','numeric','min:'.$minimum,'regex:'.$pattern],
            'method'=>'required|string|max:100','payer_name'=>'nullable|string|max:255','payer_account_reference'=>'nullable|string|max:255',
            'reference'=>[$requireReference?'required':'nullable','string','max:255'],
            'paid_at'=>'required|date','notes'=>'nullable|string|max:2000','proof'=>'nullable|file|max:10240',
        ]);
        if($request->hasFile('proof'))$data['proof_path']=$request->file('proof')->store('payment-proofs','public');unset($data['proof']);
        $data['amount']=number_format(round((float)$data['amount'],$scale),$scale,'.','');
        if(isset($data['reference']))$data['reference']=trim($data['reference']);
        return $data;
    }

    private function currencyScale(TradeDocument $document): int
    {
        return in_array(strtoupper((string)$document->currency),['JPY','KRW'],true)?0:2;
    }

    private function minor(mixed $amount,TradeDocument $document): int
    {
        return (int)round((float)$amount*(10**$this->currencyScale($document)));
    }

    private function netPaidMinor(TradeDocument $document,?int $excludePaymentId=null): int
    {
        return max(0,(int)$document->payments()->where('status','posted')
            ->when($excludePaymentId,fn($query)=>$query->where('id','<>',$excludePaymentId))->get()
            ->sum(fn($payment)=>in_array($payment->payment_type,['refund','reversal'],true)
                ?-$this->minor($payment->amount,$document):$this->minor($payment->amount,$document)));
    }

    private function refreshPaymentTotals(TradeDocument $document): void
    {
        $scale=$this->currencyScale($document);$factor=10**$scale;
        $paidMinor=$this->netPaidMinor($document);$totalMinor=$this->minor($document->total,$document);
        $complete=$totalMinor>0&&$paidMinor>=$totalMinor;
        $document->update([
            'paid_amount'=>number_format($paidMinor/$factor,$scale,'.',''),
            'payment_status'=>$complete?'paid':($paidMinor>0?'partial':'unpaid'),
            'paid_at'=>$complete?($document->paid_at?:now()):null,
            'status'=>$complete?'paid':($document->status==='paid'?'accepted':$document->status),
        ]);
    }

    private function assertUniquePaymentReference(TradeDocument $document,?string $reference,?int $exceptId=null): void
    {
        if(blank($reference))return;
        $duplicate=$document->payments()->withTrashed()->where('reference',trim($reference))
            ->when($exceptId,fn($query)=>$query->where('id','<>',$exceptId))->exists();
        abort_if($duplicate,422,'This payment reference is already used for the invoice.');
    }

    private function authorizePayment(TradeDocument $document,TradeDocumentPayment $payment): void
    {
        $this->authorizeBusiness($document);
        abort_unless($document->type==='invoice'&&(int)$payment->trade_document_id===(int)$document->id,404);
        abort_unless(in_array($document->status,['accepted','paid'],true),422,'Payment changes require an accepted invoice.');
    }

    private function authorizeBusiness(TradeDocument $document): void
    {
        abort_unless($document->business_id === auth()->user()->business_id, 403);
    }

    private function auditPublicAccess(
        TradeDocument $document,
        string $action,
        ?array $oldValues = null,
        ?array $newValues = null
    ): void {
        AuditLog::create([
            'business_id' => $document->business_id,
            'user_id' => auth()->id(),
            'employee_name' => auth()->user()?->name ?: 'Public customer',
            'employee_email' => auth()->user()?->email ?: $document->customer_email,
            'action' => $action,
            'entity_type' => 'TradeDocument',
            'entity_id' => (string) $document->id,
            'route_name' => request()->route()?->getName(),
            'method' => request()->method(),
            'url' => request()->route()?->getName().' [public token redacted]',
            'response_status' => 200,
            'old_values' => $oldValues,
            'new_values' => $newValues,
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
            'performed_at' => now(),
        ]);
    }

}
