<?php

namespace App\Http\Controllers\Finance;

use App\Enums\DocumentStatus;
use App\Http\Controllers\Controller;
use App\Models\Account;
use App\Models\AdjustmentNote;
use App\Models\Branch;
use App\Models\CustomerInvoice;
use App\Models\Party;
use App\Models\TaxCode;
use App\Models\VendorBill;
use App\Services\Accounting\AdjustmentNotePostingService;
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 AdjustmentNoteController extends Controller
{
    public function __construct(private readonly DocumentNumberService $numbers,private readonly ApprovalWorkflowService $workflow,private readonly AdjustmentNotePostingService $posting,private readonly PdfService $pdf){}
    public function index(Request $r):View{$this->permission($r,'view');$q=AdjustmentNote::with(['party','branch'])->where('company_id',$r->user()->company_id);$this->scope($r,$q);if($s=trim((string)$r->query('search')))$q->where(fn($q)=>$q->where('note_number','like',"%$s%")->orWhere('reason','like',"%$s%")->orWhereHas('party',fn($p)=>$p->where('legal_name','like',"%$s%")));if($side=$r->query('side'))$q->where('side',$side);$notes=$q->latest('note_date')->paginate(25)->withQueryString();return view('notes.index',compact('notes'));}
    public function create(Request $r):View{$this->permission($r,'create');$n=new AdjustmentNote(['branch_id'=>$r->user()->branch_id,'note_date'=>today(),'side'=>$r->query('side','customer'),'note_type'=>$r->query('type','credit'),'status'=>DocumentStatus::DRAFT]);$n->setRelation('items',collect());return $this->form($r,$n);}
    public function store(Request $r):RedirectResponse{$this->permission($r,'create');[$d,$items]=$this->validated($r);$d+=['company_id'=>$r->user()->company_id,'created_by'=>$r->user()->id,'status'=>DocumentStatus::DRAFT];$d['note_number']=$this->numbers->next((int)$r->user()->company_id,(int)$d['branch_id'],strtoupper(($d['side']==='customer'?'C':'V').($d['note_type']==='credit'?'CN':'DN')),$d['note_date']);$n=DB::transaction(function()use($d,$items){$n=AdjustmentNote::create($d);foreach($items as $i=>$item)$n->items()->create(['line_number'=>$i+1,...$item]);return $n;});return redirect()->route('notes.show',$n)->with('success','Adjustment note created.');}
    public function show(Request $r,AdjustmentNote $note):View{$this->access($r,$note,'view');$note->load(['branch','party','invoice','bill','items.account','journal.lines.account']);return view('notes.show',compact('note'));}
    public function edit(Request $r,AdjustmentNote $note):View{$this->access($r,$note,'edit_draft');$this->editable($note);return $this->form($r,$note->load('items'));}
    public function update(Request $r,AdjustmentNote $note):RedirectResponse{$this->access($r,$note,'edit_draft');$this->editable($note);[$d,$items]=$this->validated($r);DB::transaction(function()use($note,$d,$items){$note->update($d+['status'=>DocumentStatus::DRAFT,'approved_by'=>null,'approved_at'=>null]);$note->items()->delete();foreach($items as $i=>$item)$note->items()->create(['line_number'=>$i+1,...$item]);});return redirect()->route('notes.show',$note)->with('success','Adjustment note updated.');}
    public function destroy(Request $r,AdjustmentNote $note):RedirectResponse{$this->access($r,$note,'delete_draft');$this->editable($note);$note->delete();return redirect()->route('notes.index')->with('success','Draft deleted.');}
    public function submit(Request $r,AdjustmentNote $note):RedirectResponse{return $this->transition($r,$note,'submit',fn()=>$this->workflow->submit($note),'Note submitted.');} public function approve(Request $r,AdjustmentNote $note):RedirectResponse{return $this->transition($r,$note,'approve',fn()=>$this->workflow->approve($note),'Note approved.');} public function reject(Request $r,AdjustmentNote $note):RedirectResponse{$r->validate(['reason'=>'required|string|max:1000']);return $this->transition($r,$note,'reject',fn()=>$this->workflow->reject($note,$r->string('reason')->toString()),'Note rejected.');} public function post(Request $r,AdjustmentNote $note):RedirectResponse{return $this->transition($r,$note,'post',fn()=>$this->posting->post($note),'Note posted.');}
    public function print(Request $r,AdjustmentNote $note):View{$this->access($r,$note,'print');$note->load(['branch','party','invoice','bill','items']);return view('documents.adjustment_note',compact('note'));} public function pdf(Request $r,AdjustmentNote $note):Response{$this->access($r,$note,'print');$note->load(['branch','party','invoice','bill','items']);return $this->pdf->download(view('documents.adjustment_note',compact('note'))->render(),$note->note_number.'.pdf');}
    private function transition(Request $r,AdjustmentNote $n,string $a,callable $fn,string $m):RedirectResponse{$this->access($r,$n,$a);try{$fn();}catch(Throwable $e){return back()->with('error',$e->getMessage());}return back()->with('success',$m);}
    private function form(Request $request, AdjustmentNote $note): 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', ['posted', 'partially_paid', 'paid']);
        $bills = VendorBill::query()->with('party')->where('company_id', $companyId)->whereIn('status', ['posted', 'partially_paid', 'paid']);

        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);
            $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();
        $taxCodes = TaxCode::query()->where('company_id', $companyId)->where('is_active', true)->orderBy('code')->get();
        $invoices = $invoices->latest('issue_date')->get();
        $bills = $bills->latest('invoice_date')->get();

        return view('notes.form', compact('note', 'branches', 'parties', 'accounts', 'taxCodes', 'invoices', 'bills'));
    }
    private function validated(Request $request): array
    {
        $data = $request->validate([
            'branch_id' => 'required|integer',
            'party_id' => 'required|integer',
            'side' => ['required', Rule::in(['customer', 'vendor'])],
            'note_type' => ['required', Rule::in(['credit', 'debit'])],
            'note_date' => 'required|date',
            'customer_invoice_id' => 'nullable|integer',
            'vendor_bill_id' => 'nullable|integer',
            'reason_code' => 'required|string|max:50',
            'reason' => 'required|string|max:2000',
            'receivable_account_id' => 'nullable|integer',
            'payable_account_id' => 'nullable|integer',
            'income_account_id' => 'nullable|integer',
            'expense_account_id' => 'nullable|integer',
            'vat_account_id' => 'nullable|integer',
            'items' => 'required|array|min:1',
            'items.*.description' => 'required|string|max:1000',
            'items.*.quantity' => 'required|numeric|gt:0',
            'items.*.unit_rate' => 'required|numeric|min:0',
            'items.*.vat_rate' => 'nullable|numeric|min:0|max:100',
            'items.*.account_id' => 'required|integer',
            'items.*.tax_code_id' => 'nullable|integer',
        ]);

        $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 adjustment note 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']);
        $roleNames = $party->roles->map(fn ($role) => $role->role->value ?? $role->role);

        $original = null;
        if ($data['side'] === 'customer') {
            abort_unless($roleNames->contains('customer'), 422, 'The selected party is not a customer.');
            if (empty($data['customer_invoice_id'])) {
                throw new DomainException('Customer notes require an original invoice reference.');
            }
            $original = CustomerInvoice::query()
                ->where('company_id', $companyId)
                ->where('branch_id', $branchId)
                ->where('party_id', $party->id)
                ->whereIn('status', [DocumentStatus::POSTED->value, DocumentStatus::PARTIALLY_PAID->value, DocumentStatus::PAID->value])
                ->findOrFail($data['customer_invoice_id']);
            $data['vendor_bill_id'] = null;
            if (empty($data['receivable_account_id'])) {
                throw new DomainException('Accounts receivable is required for a customer adjustment note.');
            }
        } else {
            abort_unless($roleNames->intersect(['vendor', 'subcontractor', 'vehicle_owner'])->isNotEmpty(), 422, 'The selected party is not a vendor.');
            if (empty($data['vendor_bill_id'])) {
                throw new DomainException('Vendor notes require an original vendor bill reference.');
            }
            $original = VendorBill::query()
                ->where('company_id', $companyId)
                ->where('branch_id', $branchId)
                ->where('party_id', $party->id)
                ->whereIn('status', [DocumentStatus::POSTED->value, DocumentStatus::PARTIALLY_PAID->value, DocumentStatus::PAID->value])
                ->findOrFail($data['vendor_bill_id']);
            $data['customer_invoice_id'] = null;
            if (empty($data['payable_account_id'])) {
                throw new DomainException('Accounts payable is required for a vendor adjustment note.');
            }
        }

        $headerAccountIds = collect([
            $data['receivable_account_id'] ?? null,
            $data['payable_account_id'] ?? null,
            $data['income_account_id'] ?? null,
            $data['expense_account_id'] ?? null,
            $data['vat_account_id'] ?? null,
        ])->filter();
        $lineAccountIds = collect($data['items'])->pluck('account_id')->filter();
        $accountIds = $headerAccountIds->merge($lineAccountIds)->unique();
        abort_unless(Account::query()->where('company_id', $companyId)->where('is_active', true)->whereIn('id', $accountIds)->count() === $accountIds->count(), 422, 'Invalid account selection.');

        $taxCodeIds = collect($data['items'])->pluck('tax_code_id')->filter()->unique();
        $taxCodes = TaxCode::query()->where('company_id', $companyId)->where('is_active', true)->whereIn('id', $taxCodeIds)->get()->keyBy('id');
        abort_unless($taxCodes->count() === $taxCodeIds->count(), 422, 'Invalid tax code selection.');

        $items = [];
        $taxable = Decimal::scale('0');
        $vat = Decimal::scale('0');
        $grand = Decimal::scale('0');
        foreach ($data['items'] as $item) {
            $quantity = Decimal::scale((string) $item['quantity']);
            $rate = Decimal::scale((string) $item['unit_rate']);
            $vatRate = Decimal::scale((string) ($item['vat_rate'] ?? '0'));
            $taxCodeId = $item['tax_code_id'] ?? null;
            if (Decimal::compare($vatRate, '0') > 0 && ! $taxCodeId) {
                throw new DomainException('Select a tax code for every line carrying VAT.');
            }
            if ($taxCodeId && ! Decimal::equals($vatRate, (string) $taxCodes->get($taxCodeId)->rate)) {
                throw new DomainException('A line VAT rate does not match its selected tax code.');
            }

            $base = Decimal::multiply($quantity, $rate);
            $tax = Decimal::divide(Decimal::multiply($base, $vatRate), '100');
            $total = Decimal::add($base, $tax);
            $items[] = [
                'description' => $item['description'],
                'quantity' => $quantity,
                'unit_rate' => $rate,
                'taxable_amount' => $base,
                'vat_rate' => $vatRate,
                'vat_amount' => $tax,
                'line_total' => $total,
                'account_id' => $item['account_id'],
                'tax_code_id' => $taxCodeId,
            ];
            $taxable = Decimal::add($taxable, $base);
            $vat = Decimal::add($vat, $tax);
            $grand = Decimal::add($grand, $total);
        }

        if (Decimal::compare($grand, '0') <= 0) {
            throw new DomainException('Adjustment note total must be greater than zero.');
        }
        if (Decimal::compare($vat, '0') > 0 && empty($data['vat_account_id'])) {
            throw new DomainException('A VAT account is required when the note contains VAT.');
        }
        if (($data['side'] === 'customer' && $data['note_type'] === 'credit') || $data['side'] === 'vendor') {
            if (Decimal::compare($grand, (string) $original->grand_total) > 0) {
                throw new DomainException('The adjustment note cannot exceed the original document total.');
            }
        }

        unset($data['items']);
        $data['taxable_total'] = $taxable;
        $data['vat_total'] = $vat;
        $data['grand_total'] = $grand;

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