<?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\CostCenter;
use App\Models\Department;
use App\Models\Driver;
use App\Models\Expense;
use App\Models\Party;
use App\Models\TaxCode;
use App\Models\TransportJob;
use App\Models\Trip;
use App\Models\Vehicle;
use App\Services\Accounting\ExpensePostingService;
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\Validation\Rule;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

final class ExpenseController extends Controller
{
    public function __construct(private readonly DocumentNumberService $numbers, private readonly ApprovalWorkflowService $workflow, private readonly ExpensePostingService $posting, private readonly PdfService $pdf) {}
    public function index(Request $r):View{$this->permission($r,'view');$q=Expense::query()->with(['party','branch','expenseAccount'])->where('company_id',$r->user()->company_id);$this->scope($r,$q);if($s=trim((string)$r->query('search')))$q->where(fn($q)=>$q->where('expense_number','like',"%$s%")->orWhere('description','like',"%$s%")->orWhere('category','like',"%$s%"));if($status=$r->query('status'))$q->where('status',$status);$expenses=$q->latest('expense_date')->paginate(25)->withQueryString();return view('expenses.index',compact('expenses'));}
    public function create(Request $r):View{$this->permission($r,'create');return $this->form($r,new Expense(['branch_id'=>$r->user()->branch_id,'expense_date'=>today(),'payment_method'=>'cash','status'=>DocumentStatus::DRAFT,'amount_before_vat'=>'0','recoverable_vat'=>'0','nonrecoverable_vat'=>'0','total_amount'=>'0']));}
    public function store(Request $r):RedirectResponse{$this->permission($r,'create');$data=$this->validated($r);$data+=['company_id'=>$r->user()->company_id,'created_by'=>$r->user()->id,'status'=>DocumentStatus::DRAFT];$data['expense_number']=$this->numbers->next((int)$r->user()->company_id,(int)$data['branch_id'],'EXP',$data['expense_date']);$expense=Expense::query()->create($data);return redirect()->route('expenses.show',$expense)->with('success','Expense draft created.');}
    public function show(Request $r,Expense $expense):View{$this->access($r,$expense,'view');$expense->load(['branch','party','transportJob','trip','vehicle','driver','expenseAccount','paymentAccount','journal.lines.account']);return view('expenses.show',compact('expense'));}
    public function edit(Request $r,Expense $expense):View{$this->access($r,$expense,'edit_draft');$this->editable($expense);return $this->form($r,$expense);}
    public function update(Request $r,Expense $expense):RedirectResponse{$this->access($r,$expense,'edit_draft');$this->editable($expense);$expense->update($this->validated($r)+['status'=>DocumentStatus::DRAFT,'approved_by'=>null,'approved_at'=>null]);return redirect()->route('expenses.show',$expense)->with('success','Expense updated.');}
    public function destroy(Request $r,Expense $expense):RedirectResponse{$this->access($r,$expense,'delete_draft');$this->editable($expense);$expense->delete();return redirect()->route('expenses.index')->with('success','Expense draft deleted.');}
    public function submit(Request $r,Expense $expense):RedirectResponse{return $this->transition($r,$expense,'submit',fn()=>$this->workflow->submit($expense),'Expense submitted.');}
    public function approve(Request $r,Expense $expense):RedirectResponse{return $this->transition($r,$expense,'approve',fn()=>$this->workflow->approve($expense),'Expense approved.');}
    public function reject(Request $r,Expense $expense):RedirectResponse{$r->validate(['reason'=>'required|string|max:1000']);return $this->transition($r,$expense,'reject',fn()=>$this->workflow->reject($expense,$r->string('reason')->toString()),'Expense rejected.');}
    public function post(Request $r,Expense $expense):RedirectResponse{return $this->transition($r,$expense,'post',fn()=>$this->posting->post($expense),'Expense posted.');}
    public function print(Request $r,Expense $expense):View{$this->access($r,$expense,'print');$expense->load(['branch','party','expenseAccount','paymentAccount']);return view('documents.expense',compact('expense'));}
    public function pdf(Request $r,Expense $expense):Response{$this->access($r,$expense,'print');$expense->load(['branch','party','expenseAccount','paymentAccount']);return $this->pdf->download(view('documents.expense',compact('expense'))->render(),$expense->expense_number.'.pdf');}
    private function transition(Request $r,Expense $e,string $a,callable $fn,string $m):RedirectResponse{$this->access($r,$e,$a);try{$fn();}catch(Throwable $x){return back()->with('error',$x->getMessage());}return back()->with('success',$m);}
    private function form(Request $request, Expense $expense): 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);
        $jobs = TransportJob::query()->where('company_id', $companyId);
        $trips = Trip::query()->where('company_id', $companyId);
        $departments = Department::query()->where('company_id', $companyId)->where('is_active', true);
        $costCenters = CostCenter::query()->where('company_id', $companyId)->where('is_active', true);

        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));
            $jobs->where('branch_id', $branchId);
            $trips->where('branch_id', $branchId);
            $departments->where(fn ($query) => $query->whereNull('branch_id')->orWhere('branch_id', $branchId));
            $costCenters->where(fn ($query) => $query->whereNull('branch_id')->orWhere('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();
        $taxCodes = TaxCode::query()->where('company_id', $companyId)->where('is_active', true)->orderBy('code')->get();
        $jobs = $jobs->latest('job_date')->limit(1000)->get();
        $trips = $trips->latest('pickup_at')->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();
        $departments = $departments->orderBy('name')->get();
        $costCenters = $costCenters->orderBy('name')->get();

        return view('expenses.form', compact('expense', 'branches', 'parties', 'accounts', 'taxCodes', 'jobs', 'trips', 'vehicles', 'drivers', 'departments', 'costCenters'));
    }
    private function validated(Request $request): array
    {
        $data = $request->validate([
            'branch_id' => 'required|integer',
            'party_id' => 'nullable|integer',
            'transport_job_id' => 'nullable|integer',
            'trip_id' => 'nullable|integer',
            'vehicle_id' => 'nullable|integer',
            'driver_id' => 'nullable|integer',
            'department_id' => 'nullable|integer',
            'cost_center_id' => 'nullable|integer',
            'expense_date' => 'required|date',
            'category' => 'required|string|max:60',
            'description' => 'required|string|max:1000',
            'expense_account_id' => 'required|integer',
            'input_vat_account_id' => 'nullable|integer',
            'payment_account_id' => 'required|integer',
            'tax_code_id' => 'nullable|integer',
            'amount_before_vat' => 'required|numeric|min:0',
            'recoverable_vat' => 'nullable|numeric|min:0',
            'nonrecoverable_vat' => 'nullable|numeric|min:0',
            'payment_method' => ['required', Rule::in(['cash', 'bank', 'card', 'employee_paid', 'driver_paid', 'credit_card'])],
            'is_billable' => 'nullable|boolean',
            'reimbursement_status' => 'nullable|string|max:30',
        ]);

        $data['recoverable_vat'] = Decimal::scale((string) ($data['recoverable_vat'] ?? '0'));
        $data['nonrecoverable_vat'] = Decimal::scale((string) ($data['nonrecoverable_vat'] ?? '0'));
        $data['amount_before_vat'] = Decimal::scale((string) $data['amount_before_vat']);
        $data['total_amount'] = Decimal::add($data['amount_before_vat'], $data['recoverable_vat'], $data['nonrecoverable_vat']);
        $data['is_billable'] = $request->boolean('is_billable');

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

        if (! empty($data['party_id'])) {
            $partyQuery = Party::query()->where('company_id', $companyId)->where('is_active', true);
            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));
            }
            abort_unless($partyQuery->whereKey($data['party_id'])->exists(), 422, 'Invalid payee.');
        }

        $job = null;
        if (! empty($data['transport_job_id'])) {
            $job = TransportJob::query()->where('company_id', $companyId)->where('branch_id', $branchId)->find($data['transport_job_id']);
            abort_unless($job, 422, 'Invalid transport job.');
        }
        if (! empty($data['trip_id'])) {
            $trip = Trip::query()->where('company_id', $companyId)->where('branch_id', $branchId)->find($data['trip_id']);
            abort_unless($trip, 422, 'Invalid trip.');
            if ($job) {
                abort_unless((int) $trip->transport_job_id === (int) $job->id, 422, 'The selected trip does not belong to the selected job.');
            } else {
                $data['transport_job_id'] = $trip->transport_job_id;
            }
        }

        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.');
        }
        if (! empty($data['department_id'])) {
            abort_unless(Department::query()->where('company_id', $companyId)->whereKey($data['department_id'])->where(fn ($query) => $query->whereNull('branch_id')->orWhere('branch_id', $branchId))->exists(), 422, 'Invalid department.');
        }
        if (! empty($data['cost_center_id'])) {
            abort_unless(CostCenter::query()->where('company_id', $companyId)->whereKey($data['cost_center_id'])->where(fn ($query) => $query->whereNull('branch_id')->orWhere('branch_id', $branchId))->exists(), 422, 'Invalid cost centre.');
        }
        if (! empty($data['tax_code_id'])) {
            abort_unless(TaxCode::query()->where('company_id', $companyId)->whereKey($data['tax_code_id'])->where('is_active', true)->exists(), 422, 'Invalid tax code.');
        }

        $accountIds = collect([$data['expense_account_id'], $data['input_vat_account_id'] ?? null, $data['payment_account_id']])->filter()->unique();
        abort_unless(Account::query()->where('company_id', $companyId)->whereIn('id', $accountIds)->where('is_active', true)->count() === $accountIds->count(), 422, 'Invalid account selection.');
        if (Decimal::compare($data['recoverable_vat'], '0') > 0 && empty($data['input_vat_account_id'])) {
            throw new DomainException('Input VAT account is required when recoverable VAT is entered.');
        }

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