<?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\Contract;
use App\Models\CustomerInvoice;
use App\Models\Party;
use App\Models\TaxCode;
use App\Models\TransportJob;
use App\Services\Accounting\InvoicePostingService;
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 CustomerInvoiceController extends Controller
{
    public function __construct(
        private readonly DocumentNumberService $numbers,
        private readonly ApprovalWorkflowService $workflow,
        private readonly InvoicePostingService $posting,
        private readonly PdfService $pdf,
    ) {}

    public function index(Request $request): View
    {
        $this->permission($request, 'view');
        $query = CustomerInvoice::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('invoice_number', 'like', "%{$search}%")
                    ->orWhere('customer_po', 'like', "%{$search}%")
                    ->orWhereHas('party', fn ($party) => $party->where('legal_name', 'like', "%{$search}%"));
            });
        }
        if ($status = $request->query('status')) {
            $query->where('status', $status);
        }
        $invoices = $query->latest('issue_date')->latest('id')->paginate(25)->withQueryString();

        return view('invoices.index', compact('invoices'));
    }

    public function create(Request $request): View
    {
        $this->permission($request, 'create');
        $invoice = new CustomerInvoice([
            'issue_date' => today(),
            'supply_date' => today(),
            'due_date' => today()->addDays(30),
            'currency_code' => 'AED',
            'exchange_rate' => '1.00000000',
            'pricing_mode' => 'exclusive',
            'invoice_type' => 'tax_invoice',
        ]);
        $invoice->setRelation('items', collect());

        return $this->form($request, $invoice);
    }

    public function store(Request $request): RedirectResponse
    {
        $this->permission($request, 'create');
        [$header, $items] = $this->validatedAndCalculated($request);
        $header['company_id'] = $request->user()->company_id;
        $header['created_by'] = $request->user()->id;
        $header['status'] = DocumentStatus::DRAFT;
        $header['invoice_number'] = $this->numbers->next(
            (int) $request->user()->company_id,
            (int) $header['branch_id'],
            'INV',
            $header['issue_date'],
        );

        $invoice = DB::transaction(function () use ($header, $items): CustomerInvoice {
            $invoice = CustomerInvoice::query()->create($header);
            foreach ($items as $index => $item) {
                $invoice->items()->create(['line_number' => $index + 1, ...$item]);
            }
            return $invoice;
        });

        return redirect()->route('invoices.show', $invoice)->with('success', 'Invoice draft created.');
    }

    public function show(Request $request, CustomerInvoice $invoice): View
    {
        $this->ensureAccess($request, $invoice, 'view');
        $invoice->load(['company', 'branch', 'party', 'items', 'creator', 'approver', 'journal.lines.account']);

        return view('invoices.show', compact('invoice'));
    }

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

    public function update(Request $request, CustomerInvoice $invoice): RedirectResponse
    {
        $this->ensureAccess($request, $invoice, 'edit_draft');
        $this->ensureEditable($invoice);
        [$header, $items] = $this->validatedAndCalculated($request, $invoice);
        $header['status'] = DocumentStatus::DRAFT;
        $header['approved_by'] = null;
        $header['approved_at'] = null;

        DB::transaction(function () use ($invoice, $header, $items): void {
            $invoice->update($header);
            $invoice->items()->delete();
            foreach ($items as $index => $item) {
                $invoice->items()->create(['line_number' => $index + 1, ...$item]);
            }
        });

        return redirect()->route('invoices.show', $invoice)->with('success', 'Invoice draft updated.');
    }

    public function destroy(Request $request, CustomerInvoice $invoice): RedirectResponse
    {
        $this->ensureAccess($request, $invoice, 'delete_draft');
        $this->ensureEditable($invoice);
        $invoice->delete();
        return redirect()->route('invoices.index')->with('success', 'Invoice draft deleted.');
    }

    public function submit(Request $request, CustomerInvoice $invoice): RedirectResponse
    {
        $this->ensureAccess($request, $invoice, 'submit');
        try { $this->workflow->submit($invoice, $request->input('reason')); }
        catch (Throwable $e) { return back()->with('error', $e->getMessage()); }
        return back()->with('success', 'Invoice submitted for approval.');
    }

    public function approve(Request $request, CustomerInvoice $invoice): RedirectResponse
    {
        $this->ensureAccess($request, $invoice, 'approve');
        try { $this->workflow->approve($invoice, $request->input('reason')); }
        catch (Throwable $e) { return back()->with('error', $e->getMessage()); }
        return back()->with('success', 'Invoice approved.');
    }

    public function reject(Request $request, CustomerInvoice $invoice): RedirectResponse
    {
        $this->ensureAccess($request, $invoice, 'reject');
        $request->validate(['reason' => ['required', 'string', 'max:1000']]);
        try { $this->workflow->reject($invoice, $request->string('reason')->toString()); }
        catch (Throwable $e) { return back()->with('error', $e->getMessage()); }
        return back()->with('success', 'Invoice rejected.');
    }

    public function post(Request $request, CustomerInvoice $invoice): RedirectResponse
    {
        $this->ensureAccess($request, $invoice, 'post');
        try { $this->posting->post($invoice); }
        catch (Throwable $e) { return back()->with('error', $e->getMessage()); }
        return back()->with('success', 'Invoice posted to the general ledger.');
    }

    public function print(Request $request, CustomerInvoice $invoice): View
    {
        $this->ensureAccess($request, $invoice, 'print');
        $invoice->load(['company', 'branch', 'party', 'items']);
        return view('documents.invoice', compact('invoice'));
    }

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

    private function form(Request $request, CustomerInvoice $invoice): View
    {
        $companyId = $request->user()->company_id;
        $branches = Branch::query()->where('company_id', $companyId)->where('is_active', true)->orderBy('code')->get();
        $customers = Party::query()->where('company_id', $companyId)->where('is_active', true)->whereHas('roles', fn ($q) => $q->where('role', 'customer'))->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)->orderByDesc('job_date')->limit(1000)->get();
        $contracts = Contract::query()->where('company_id', $companyId)->where('status', 'active')->orderBy('contract_number')->get();

        return view('invoices.form', compact('invoice', 'branches', 'customers', 'accounts', 'taxCodes', 'jobs', 'contracts'));
    }

    /** @return array{0:array<string,mixed>,1:array<int,array<string,mixed>>} */
    private function validatedAndCalculated(Request $request, ?CustomerInvoice $invoice = null): array
    {
        $data = $request->validate([
            'branch_id' => ['required', 'integer'],
            'party_id' => ['required', 'integer'],
            'transport_job_id' => ['nullable', 'integer'],
            'contract_id' => ['nullable', 'integer'],
            'invoice_type' => ['required', Rule::in(['tax_invoice','simplified_tax_invoice','commercial_invoice','proforma_invoice','advance_invoice'])],
            'customer_po' => ['nullable', 'string', 'max:150'],
            'pricing_mode' => ['required', Rule::in(['exclusive','inclusive'])],
            'issue_date' => ['required', 'date'],
            'supply_date' => ['nullable', 'date'],
            'due_date' => ['nullable', 'date', 'after_or_equal:issue_date'],
            'currency_code' => ['required', 'string', 'size:3'],
            'exchange_rate' => ['required', 'numeric', 'gt:0'],
            'receivable_account_id' => ['required', 'integer'],
            'notes' => ['nullable', 'string'],
            'terms' => ['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.*.income_account_id' => ['required', 'integer'],
            'items.*.tax_code_id' => ['nullable', 'integer'],
        ]);

        $this->validateOwnership($request, $data);
        $items = [];
        $subtotal = Decimal::scale('0');
        $discountTotal = Decimal::scale('0');
        $taxableTotal = Decimal::scale('0');
        $vatTotal = Decimal::scale('0');
        $grandTotal = Decimal::scale('0');

        foreach ($data['items'] as $input) {
            $taxCode = ! empty($input['tax_code_id']) ? TaxCode::query()->where('company_id', $request->user()->company_id)->findOrFail($input['tax_code_id']) : null;
            $rate = $taxCode ? (string) $taxCode->rate : '0';
            $quantity = Decimal::scale((string) $input['quantity']);
            $unitRate = Decimal::scale((string) $input['unit_rate']);
            $discount = Decimal::scale((string) ($input['discount_amount'] ?? '0'));
            $beforeDiscount = Decimal::multiply($quantity, $unitRate);
            if (Decimal::compare($discount, $beforeDiscount) > 0) {
                throw new DomainException('A line discount cannot exceed its line amount.');
            }
            $gross = Decimal::subtract($beforeDiscount, $discount);
            if ($data['pricing_mode'] === 'inclusive' && Decimal::compare($rate, '0') > 0) {
                $factor = Decimal::add('1', Decimal::divide($rate, '100'));
                $taxable = Decimal::divide($gross, $factor);
                $vat = Decimal::subtract($gross, $taxable);
                $lineTotal = $gross;
            } else {
                $taxable = $gross;
                $vat = Decimal::divide(Decimal::multiply($taxable, $rate), '100');
                $lineTotal = Decimal::add($taxable, $vat);
            }
            $items[] = [
                'description' => $input['description'], 'quantity' => $quantity, 'unit' => $input['unit'] ?? null,
                'unit_rate' => $unitRate, 'discount_amount' => $discount, 'taxable_amount' => $taxable,
                'vat_rate' => $rate, 'vat_amount' => $vat, 'line_total' => $lineTotal,
                'income_account_id' => $input['income_account_id'],
                'output_vat_account_id' => $taxCode?->output_account_id,
                'tax_code_id' => $taxCode?->id,
            ];
            $subtotal = Decimal::add($subtotal, $beforeDiscount);
            $discountTotal = Decimal::add($discountTotal, $discount);
            $taxableTotal = Decimal::add($taxableTotal, $taxable);
            $vatTotal = Decimal::add($vatTotal, $vat);
            $grandTotal = Decimal::add($grandTotal, $lineTotal);
        }

        $first = $items[0];
        unset($data['items']);
        $data += [
            'subtotal' => $subtotal,
            'discount_total' => $discountTotal,
            'taxable_total' => $taxableTotal,
            'vat_total' => $vatTotal,
            'grand_total' => $grandTotal,
            'paid_total' => $invoice?->paid_total ?? '0',
            'allocated_total' => $invoice?->allocated_total ?? '0',
            'income_account_id' => $first['income_account_id'],
            'output_vat_account_id' => $first['output_vat_account_id'],
            'tax_code_id' => $first['tax_code_id'],
        ];

        return [$data, $items];
    }

    private function validateOwnership(Request $request, array $data): 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->where('role', 'customer'))->exists(), 422, 'The selected party is not configured as a customer.');
        $accountIds = collect($data['items'])->pluck('income_account_id')->push($data['receivable_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'])->where('customer_id', $data['party_id'])->whereKey($data['transport_job_id'])->exists(), 422, 'Invalid transport job for this customer and branch.');
        }
        if (! empty($data['contract_id'])) {
            abort_unless(Contract::query()->where('company_id', $companyId)->where('party_id', $data['party_id'])->whereKey($data['contract_id'])->exists(), 422, 'Invalid customer contract.');
        }
    }

    private function ensureEditable(CustomerInvoice $invoice): void
    {
        $status = $invoice->status instanceof DocumentStatus ? $invoice->status : DocumentStatus::from($invoice->status);
        if (! in_array($status, [DocumentStatus::DRAFT, DocumentStatus::REJECTED], true)) {
            throw new DomainException('Only draft or rejected invoices can be edited or deleted.');
        }
    }

    private function permission(Request $request, string $action): void
    {
        abort_unless($request->user()->hasPermission('customer_invoices.'.$action), 403);
    }

    private function ensureAccess(Request $request, CustomerInvoice $invoice, string $action): void
    {
        $this->permission($request, $action);
        abort_unless((int) $invoice->company_id === (int) $request->user()->company_id, 404);
        if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
            abort_unless((int) $invoice->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);
        }
    }
}
