<?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\Driver;
use App\Models\Party;
use App\Models\TaxCode;
use App\Models\TransportJob;
use App\Models\Trip;
use App\Models\Vehicle;
use App\Models\VendorBill;
use App\Services\Accounting\VendorBillPostingService;
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 VendorBillController extends Controller
{
    public function __construct(
        private readonly DocumentNumberService $numbers,
        private readonly ApprovalWorkflowService $workflow,
        private readonly VendorBillPostingService $posting,
        private readonly PdfService $pdf,
    ) {}

    public function index(Request $request): View
    {
        $this->permission($request, 'view');
        $query = VendorBill::query()->with(['party', 'branch'])->where('company_id', $request->user()->company_id);
        $this->scopeBranch($request, $query);
        if ($search = trim((string) $request->query('search'))) {
            $query->where(function ($query) use ($search): void {
                $query->where('bill_number', 'like', "%{$search}%")
                    ->orWhere('vendor_invoice_number', 'like', "%{$search}%")
                    ->orWhereHas('party', fn ($party) => $party->where('legal_name', 'like', "%{$search}%"));
            });
        }
        if ($status = $request->query('status')) $query->where('status', $status);
        $bills = $query->latest('invoice_date')->latest('id')->paginate(25)->withQueryString();
        return view('bills.index', compact('bills'));
    }

    public function create(Request $request): View
    {
        $this->permission($request, 'create');
        $bill = new VendorBill(['invoice_date'=>today(),'supply_date'=>today(),'due_date'=>today()->addDays(30),'currency_code'=>'AED','exchange_rate'=>'1','pricing_mode'=>'exclusive']);
        $bill->setRelation('items', collect());
        return $this->form($request, $bill);
    }

    public function store(Request $request): RedirectResponse
    {
        $this->permission($request, 'create');
        [$header,$items]=$this->validatedAndCalculated($request);
        $header += ['company_id'=>$request->user()->company_id,'created_by'=>$request->user()->id,'status'=>DocumentStatus::DRAFT];
        $header['bill_number']=$this->numbers->next((int)$request->user()->company_id,(int)$header['branch_id'],'BILL',$header['invoice_date']);
        $bill=DB::transaction(function() use($header,$items){$bill=VendorBill::query()->create($header);foreach($items as $i=>$item)$bill->items()->create(['line_number'=>$i+1,...$item]);return $bill;});
        return redirect()->route('bills.show',$bill)->with('success','Vendor bill draft created.');
    }

    public function show(Request $request, VendorBill $bill): View
    {
        $this->ensureAccess($request,$bill,'view');
        $bill->load(['company','branch','party','items','transportJob','trip','vehicle','driver','creator','approver','journal.lines.account']);
        return view('bills.show',compact('bill'));
    }

    public function edit(Request $request, VendorBill $bill): View
    {
        $this->ensureAccess($request,$bill,'edit_draft');$this->ensureEditable($bill);return $this->form($request,$bill->load('items'));
    }

    public function update(Request $request, VendorBill $bill): RedirectResponse
    {
        $this->ensureAccess($request,$bill,'edit_draft');$this->ensureEditable($bill);[$header,$items]=$this->validatedAndCalculated($request,$bill);$header+=['status'=>DocumentStatus::DRAFT,'approved_by'=>null,'approved_at'=>null];
        DB::transaction(function() use($bill,$header,$items){$bill->update($header);$bill->items()->delete();foreach($items as $i=>$item)$bill->items()->create(['line_number'=>$i+1,...$item]);});
        return redirect()->route('bills.show',$bill)->with('success','Vendor bill draft updated.');
    }

    public function destroy(Request $request, VendorBill $bill): RedirectResponse
    {
        $this->ensureAccess($request,$bill,'delete_draft');$this->ensureEditable($bill);$bill->delete();return redirect()->route('bills.index')->with('success','Vendor bill draft deleted.');
    }

    public function submit(Request $request, VendorBill $bill): RedirectResponse { return $this->transition($request,$bill,'submit',fn()=> $this->workflow->submit($bill,$request->input('reason')),'Vendor bill submitted.'); }
    public function approve(Request $request, VendorBill $bill): RedirectResponse { return $this->transition($request,$bill,'approve',fn()=> $this->workflow->approve($bill,$request->input('reason')),'Vendor bill approved.'); }
    public function reject(Request $request, VendorBill $bill): RedirectResponse { $request->validate(['reason'=>'required|string|max:1000']);return $this->transition($request,$bill,'reject',fn()=> $this->workflow->reject($bill,$request->string('reason')->toString()),'Vendor bill rejected.'); }
    public function post(Request $request, VendorBill $bill): RedirectResponse { return $this->transition($request,$bill,'post',fn()=> $this->posting->post($bill),'Vendor bill posted to the general ledger.'); }

    public function print(Request $request, VendorBill $bill): View { $this->ensureAccess($request,$bill,'print');$bill->load(['company','branch','party','items']);return view('documents.vendor_bill',compact('bill')); }
    public function pdf(Request $request, VendorBill $bill): Response { $this->ensureAccess($request,$bill,'print');$bill->load(['company','branch','party','items']);return $this->pdf->download(view('documents.vendor_bill',compact('bill'))->render(),$bill->bill_number.'.pdf'); }

    private function transition(Request $request, VendorBill $bill, string $action, callable $callback, string $message): RedirectResponse
    {
        $this->ensureAccess($request,$bill,$action);try{$callback();}catch(Throwable $e){return back()->with('error',$e->getMessage());}return back()->with('success',$message);
    }

    private function form(Request $request, VendorBill $bill): View
    {
        $companyId=$request->user()->company_id;
        $branches=Branch::query()->where('company_id',$companyId)->where('is_active',true)->orderBy('code')->get();
        $vendors=Party::query()->where('company_id',$companyId)->where('is_active',true)->whereHas('roles',fn($q)=>$q->whereIn('role',['vendor','subcontractor','vehicle_owner']))->orderBy('legal_name')->get();
        $accounts=Account::query()->where('company_id',$companyId)->where('is_active',true)->orderBy('code')->get();
        $taxCodes=TaxCode::query()->where('company_id',$companyId)->where('is_active',true)->orderBy('code')->get();
        $jobs=TransportJob::query()->where('company_id',$companyId)->latest('job_date')->limit(1000)->get();
        $trips=Trip::query()->where('company_id',$companyId)->latest('id')->limit(1000)->get();
        $vehicles=Vehicle::query()->where('company_id',$companyId)->where('is_active',true)->orderBy('plate_number')->get();
        $drivers=Driver::query()->where('company_id',$companyId)->where('is_active',true)->orderBy('name')->get();
        return view('bills.form',compact('bill','branches','vendors','accounts','taxCodes','jobs','trips','vehicles','drivers'));
    }

    /** @return array{0:array<string,mixed>,1:array<int,array<string,mixed>>} */
    private function validatedAndCalculated(Request $request, ?VendorBill $bill=null): array
    {
        $data=$request->validate([
            'branch_id'=>'required|integer','party_id'=>'required|integer','transport_job_id'=>'nullable|integer','trip_id'=>'nullable|integer','vehicle_id'=>'nullable|integer','driver_id'=>'nullable|integer',
            'vendor_invoice_number'=>'required|string|max:150','invoice_date'=>'required|date','supply_date'=>'nullable|date','due_date'=>'nullable|date|after_or_equal:invoice_date',
            'currency_code'=>'required|string|size:3','exchange_rate'=>'required|numeric|gt:0','pricing_mode'=>['required',Rule::in(['exclusive','inclusive'])],
            'payable_account_id'=>'required|integer','notes'=>'nullable|string','items'=>'required|array|min:1',
            'items.*.description'=>'required|string|max:1000','items.*.quantity'=>'required|numeric|gt:0','items.*.unit'=>'nullable|string|max:20','items.*.unit_rate'=>'required|numeric|min:0','items.*.discount_amount'=>'nullable|numeric|min:0','items.*.expense_account_id'=>'required|integer','items.*.tax_code_id'=>'nullable|integer',
        ]);
        $this->validateOwnership($request,$data,$bill);
        $items=[];$taxableTotal=Decimal::scale('0');$recoverableTotal=Decimal::scale('0');$nonrecoverableTotal=Decimal::scale('0');$grandTotal=Decimal::scale('0');
        foreach($data['items'] as $input){$tax=!empty($input['tax_code_id'])?TaxCode::query()->where('company_id',$request->user()->company_id)->findOrFail($input['tax_code_id']):null;$rate=$tax?(string)$tax->rate:'0';$recoverPct=$tax?(string)$tax->recoverable_percentage:'0';$qty=Decimal::scale((string)$input['quantity']);$unitRate=Decimal::scale((string)$input['unit_rate']);$discount=Decimal::scale((string)($input['discount_amount']??0));$base=Decimal::multiply($qty,$unitRate);if(Decimal::compare($discount,$base)>0)throw new DomainException('A line discount cannot exceed the line amount.');$gross=Decimal::subtract($base,$discount);if($data['pricing_mode']==='inclusive'&&Decimal::compare($rate,'0')>0){$factor=Decimal::add('1',Decimal::divide($rate,'100'));$taxable=Decimal::divide($gross,$factor);$totalVat=Decimal::subtract($gross,$taxable);$lineTotal=$gross;}else{$taxable=$gross;$totalVat=Decimal::divide(Decimal::multiply($taxable,$rate),'100');$lineTotal=Decimal::add($taxable,$totalVat);} $recoverable=Decimal::divide(Decimal::multiply($totalVat,$recoverPct),'100');$nonrecoverable=Decimal::subtract($totalVat,$recoverable);$items[]=['description'=>$input['description'],'quantity'=>$qty,'unit'=>$input['unit']??null,'unit_rate'=>$unitRate,'discount_amount'=>$discount,'taxable_amount'=>$taxable,'recoverable_vat_amount'=>$recoverable,'nonrecoverable_vat_amount'=>$nonrecoverable,'line_total'=>$lineTotal,'expense_account_id'=>$input['expense_account_id'],'input_vat_account_id'=>$tax?->input_account_id,'tax_code_id'=>$tax?->id];$taxableTotal=Decimal::add($taxableTotal,$taxable);$recoverableTotal=Decimal::add($recoverableTotal,$recoverable);$nonrecoverableTotal=Decimal::add($nonrecoverableTotal,$nonrecoverable);$grandTotal=Decimal::add($grandTotal,$lineTotal);}
        $first=$items[0];unset($data['items']);$data += ['taxable_total'=>$taxableTotal,'recoverable_vat_total'=>$recoverableTotal,'nonrecoverable_vat_total'=>$nonrecoverableTotal,'grand_total'=>$grandTotal,'paid_total'=>$bill?->paid_total??'0','allocated_total'=>$bill?->allocated_total??'0','expense_account_id'=>$first['expense_account_id'],'input_vat_account_id'=>$first['input_vat_account_id'],'tax_code_id'=>$first['tax_code_id']];
        return [$data,$items];
    }

    private function validateOwnership(Request $request,array $data,?VendorBill $bill): void
    {
        $companyId=(int)$request->user()->company_id;
        abort_unless(Branch::query()->where('company_id',$companyId)->whereKey($data['branch_id'])->exists(),422,'Invalid branch.');
        abort_unless(Party::query()->where('company_id',$companyId)->whereKey($data['party_id'])->whereHas('roles',fn($q)=>$q->whereIn('role',['vendor','subcontractor','vehicle_owner']))->exists(),422,'The selected party is not configured as a vendor.');
        $accountIds=collect($data['items'])->pluck('expense_account_id')->push($data['payable_account_id'])->filter()->unique();
        abort_unless(Account::query()->where('company_id',$companyId)->whereIn('id',$accountIds)->count()===$accountIds->count(),422,'One or more accounts are invalid.');
        $taxCodeIds = collect($data['items'])->pluck('tax_code_id')->filter()->unique();
        abort_unless(TaxCode::query()->where('company_id', $companyId)->whereIn('id', $taxCodeIds)->count() === $taxCodeIds->count(), 422, 'One or more VAT codes are invalid.');
        if (! empty($data['transport_job_id'])) {
            abort_unless(TransportJob::query()->where('company_id', $companyId)->where('branch_id', $data['branch_id'])->whereKey($data['transport_job_id'])->exists(), 422, 'Invalid transport job.');
        }
        if (! empty($data['trip_id'])) {
            $tripQuery = Trip::query()->where('company_id', $companyId)->where('branch_id', $data['branch_id'])->whereKey($data['trip_id']);
            if (! empty($data['transport_job_id'])) {
                $tripQuery->where('transport_job_id', $data['transport_job_id']);
            }
            abort_unless($tripQuery->exists(), 422, 'Invalid trip for the selected job and branch.');
        }
        if (! empty($data['vehicle_id'])) {
            abort_unless(Vehicle::query()->where('company_id', $companyId)->whereKey($data['vehicle_id'])->exists(), 422, 'Invalid vehicle.');
        }
        if (! empty($data['driver_id'])) {
            abort_unless(Driver::query()->where('company_id', $companyId)->whereKey($data['driver_id'])->exists(), 422, 'Invalid driver.');
        }
        $duplicate=VendorBill::query()->where('company_id',$companyId)->where('party_id',$data['party_id'])->where('vendor_invoice_number',$data['vendor_invoice_number'])->when($bill,fn($q)=>$q->where('id','!=',$bill->id))->exists();
        if($duplicate)throw new DomainException('This vendor invoice number already exists for the selected vendor.');
    }

    private function ensureEditable(VendorBill $bill): void {$status=$bill->status instanceof DocumentStatus?$bill->status:DocumentStatus::from($bill->status);if(!in_array($status,[DocumentStatus::DRAFT,DocumentStatus::REJECTED],true))throw new DomainException('Only draft or rejected vendor bills can be edited.');}
    private function permission(Request $request,string $action): void {abort_unless($request->user()->hasPermission('vendor_bills.'.$action),403);}
    private function ensureAccess(Request $request,VendorBill $bill,string $action): void {$this->permission($request,$action);abort_unless((int)$bill->company_id===(int)$request->user()->company_id,404);if($request->user()->branch_id&&!$request->user()->hasPermission('branches.view_all'))abort_unless((int)$bill->branch_id===(int)$request->user()->branch_id,403);}
    private function scopeBranch(Request $request,$query): void {if($request->user()->branch_id&&!$request->user()->hasPermission('branches.view_all'))$query->where('branch_id',$request->user()->branch_id);}
}
