<?php

namespace App\Http\Controllers\Admin;

use App\Actions\Purchases\CancelPurchase;
use App\Actions\Purchases\CreatePurchase;
use App\Actions\Purchases\DeletePurchase;
use App\Actions\Purchases\ExportPurchases;
use App\Actions\Purchases\ReceivePurchase;
use App\Actions\Purchases\UpdatePurchase;
use App\Exceptions\PurchaseNotEditable;
use App\Http\Controllers\Concerns\RespondsJsonOrRedirect;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PurchaseRequest;
use App\Models\Attachment;
use App\Models\Company;
use App\Models\Purchase;
use App\Models\Store;
use App\Models\Supplier;
use App\Models\TaxGroup;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * Purchases admin (Slice 3 — draft → received + cancel).
 *
 * Receive runs stock movements + WAC + supplier outstanding update
 * via {@see ReceivePurchase}. Cancel pre-receive is a clean status
 * flip via {@see CancelPurchase}; cancel post-receive is refused
 * with a flash directing the user to the Return flow (Slice 5).
 *
 * Payment + return flows live in later slices.
 */
class PurchaseController extends Controller
{
    use RespondsJsonOrRedirect;

    public function index(Request $request): View
    {
        $this->authorize('viewAny', Purchase::class);

        $q          = trim((string) $request->query('q', ''));
        $status     = (string) $request->query('status', 'all');
        $supplierId = $request->integer('supplier_id') ?: null;
        // Scope to the active store from the topbar switcher unless an
        // explicit `?store_id=` overrides — matches the StockMovement /
        // StockLevel convention. Without this, a multi-store install
        // showed every store's POs in every store's view.
        $storeId    = enforce_store_access($request->integer('store_id') ?: current_store_id());
        $from       = $request->query('from');
        $to         = $request->query('to');

        $purchases = Purchase::query()
            ->with(['supplier:id,code,name', 'store:id,code,name'])
            ->withCount('attachments')
            ->when($status !== 'all', fn ($qb) => $qb->where('status', $status))
            ->when($storeId, fn ($qb) => $qb->where('store_id', $storeId))
            ->when($supplierId, fn ($qb) => $qb->where('supplier_id', $supplierId))
            ->when($from, fn ($qb) => $qb->whereDate('purchase_date', '>=', $from))
            ->when($to,   fn ($qb) => $qb->whereDate('purchase_date', '<=', $to))
            ->when($q !== '', function ($qb) use ($q) {
                $qb->where(function ($w) use ($q) {
                    $w->where('number', 'like', "%{$q}%")
                      ->orWhere('supplier_invoice_number', 'like', "%{$q}%")
                      ->orWhereHas('supplier', fn ($s) => $s
                          ->where('name', 'like', "%{$q}%")
                          ->orWhere('code', 'like', "%{$q}%"));
                });
            })
            ->orderByDesc('id')
            ->get();

        // Summary cards — computed from the same filtered set (the full
        // collection is already loaded, so no extra query).
        $summaryCards = [
            ['label' => __('purchases.summary.total'),       'value' => format_money($purchases->sum('grand_total'))],
            ['label' => __('purchases.summary.count'),       'value' => number_format($purchases->count())],
            ['label' => __('purchases.summary.paid'),        'value' => format_money($purchases->sum('paid_total')), 'tone' => 'positive'],
            ['label' => __('purchases.summary.outstanding'), 'value' => format_money($purchases->sum('balance_due')), 'tone' => 'warning'],
        ];

        return view('admin.purchases.index', [
            'purchases'    => $purchases,
            'summaryCards' => $summaryCards,
            'suppliers'    => Supplier::query()->active()->orderBy('name')->get(['id', 'name']),
            'filters'      => compact('q', 'status', 'supplierId', 'from', 'to'),
        ]);
    }

    public function create(): View
    {
        $this->authorize('create', Purchase::class);

        $purchase = new Purchase([
            'status'                => Purchase::STATUS_DRAFT,
            'purchase_date'         => now()->toDateString(),
            'exchange_rate_to_base' => 1,
        ]);
        $purchase->setRelation('items', collect());

        return view('admin.purchases.edit', $this->editorPayload($purchase));
    }

    public function store(PurchaseRequest $request, CreatePurchase $create): JsonResponse|RedirectResponse
    {
        $this->authorize('create', Purchase::class);

        $purchase = $create($request->headerData(), $request->itemData(), $request->user());

        $this->syncAttachment($purchase, $request);

        return $this->jsonOrRedirect(
            $request,
            __('purchases.flash.created', ['number' => $purchase->number]),
            route('admin.purchases.index'),
        );
    }

    public function show(Purchase $purchase): View
    {
        $this->authorize('view', $purchase);

        // `product_variants` has no `name` column — the variant's
        // label is derived from `attributes` JSON via the `label`
        // accessor, so we load `id,attributes` (not `id,name`).
        // Also pull `markup_percent` so the receive button can decide
        // whether to render the per-PO "Update selling prices" checkbox.
        $purchase->load(['supplier', 'store', 'items.product:id,name,sku,markup_percent', 'items.variant:id,sku,attributes', 'items.taxGroup', 'creator:id,name', 'returns', 'attachments']);

        // Markup-eligibility — true when at least one line's product
        // has a non-null `markup_percent`. Drives whether the receive
        // confirm dialog shows the per-PO checkbox.
        $hasMarkupEligibleLines = $purchase->items
            ->contains(fn ($item) => $item->product?->markup_percent !== null);

        // Default state of the per-PO checkbox = company-level default.
        $companyMarkupDefault = (bool) (\App\Models\Company::current()?->auto_apply_markup_on_receive ?? false);

        return view('admin.purchases.show', [
            'purchase'               => $purchase,
            'hasMarkupEligibleLines' => $hasMarkupEligibleLines,
            'companyMarkupDefault'   => $companyMarkupDefault,
        ]);
    }

    public function edit(Purchase $purchase): View|RedirectResponse
    {
        $this->authorize('update', $purchase);

        if ($purchase->status !== Purchase::STATUS_DRAFT) {
            return redirect()
                ->route('admin.purchases.show', $purchase)
                ->with('error', __('purchases.errors.not_editable', ['status' => __('purchases.status.'.$purchase->status)]));
        }

        $purchase->load('items.product:id,name,sku', 'items.variant:id,attributes', 'attachments');

        return view('admin.purchases.edit', $this->editorPayload($purchase));
    }

    public function update(PurchaseRequest $request, Purchase $purchase, UpdatePurchase $update): JsonResponse|RedirectResponse
    {
        $this->authorize('update', $purchase);

        try {
            $update($purchase, $request->headerData(), $request->itemData(), $request->user());
        } catch (PurchaseNotEditable $e) {
            return $this->jsonOrError(
                $request,
                __('purchases.errors.not_editable', ['status' => __('purchases.status.'.$e->status)]),
                route('admin.purchases.show', $purchase),
            );
        }

        $this->syncAttachment($purchase, $request);

        return $this->jsonOrRedirect(
            $request,
            __('purchases.flash.updated', ['number' => $purchase->number]),
            route('admin.purchases.index'),
        );
    }

    /**
     * Replace / remove the purchase's single invoice attachment.
     * A new upload supersedes any existing file; `remove_attachment`
     * clears it. Files live on the private `local` disk.
     */
    private function syncAttachment(Purchase $purchase, PurchaseRequest $request): void
    {
        if ($request->boolean('remove_attachment') || $request->hasFile('attachment')) {
            foreach ($purchase->attachments()->get() as $existing) {
                Storage::disk('local')->delete($existing->path);
                $existing->delete();
            }
        }

        if ($request->hasFile('attachment')) {
            $file = $request->file('attachment');
            $path = $file->store('purchase-attachments/'.$purchase->id, 'local');

            $purchase->attachments()->create([
                'path'              => $path,
                'original_filename' => $file->getClientOriginalName(),
                'mime_type'         => $file->getClientMimeType(),
                'size_bytes'        => $file->getSize(),
                'uploaded_by'       => $request->user()?->id,
            ]);
        }
    }

    /** Stream a purchase attachment to the browser (auth-gated). */
    public function downloadAttachment(Purchase $purchase, Attachment $attachment): StreamedResponse
    {
        $this->authorize('view', $purchase);

        abort_unless(
            $attachment->attachable_type === $purchase->getMorphClass()
                && (int) $attachment->attachable_id === (int) $purchase->id,
            404,
        );
        abort_unless(Storage::disk('local')->exists($attachment->path), 404);

        return Storage::disk('local')->download($attachment->path, $attachment->original_filename);
    }

    /**
     * Receive a draft — runs stock + WAC + supplier balance update
     * atomically. Idempotent via the DB-level status check inside the
     * action (a second click while the first request is in flight
     * just lands on the friendly flash).
     */
    public function receive(Purchase $purchase, ReceivePurchase $receive, Request $request): JsonResponse|RedirectResponse
    {
        $this->authorize('receive', $purchase);

        // Per-PO override for the markup auto-apply behaviour. Sent
        // by the receive button when the purchase has at least one
        // markup-managed line; absent → action falls back to the
        // company-level default.
        $applyMarkup = $request->has('apply_markup')
            ? $request->boolean('apply_markup')
            : null;

        try {
            $receive($purchase, $request->user(), $applyMarkup);
        } catch (PurchaseNotEditable $e) {
            return $this->jsonOrError(
                $request,
                __('purchases.errors.not_receivable', ['status' => __('purchases.status.'.$e->status)]),
                route('admin.purchases.show', $purchase),
            );
        }

        // Markup auto-apply may have bumped selling prices. Flash the
        // per-SKU change list so the show page can render a callout.
        $extra = [];
        if (! empty($receive->priceChanges)) {
            session()->flash('price_changes', $receive->priceChanges);
            $extra['price_changes'] = $receive->priceChanges;
        }

        return $this->jsonOrRedirect(
            $request,
            __('purchases.flash.received', ['number' => $purchase->number]),
            route('admin.purchases.show', $purchase),
            $extra,
        );
    }

    public function cancel(Purchase $purchase, CancelPurchase $cancel, Request $request): JsonResponse|RedirectResponse
    {
        $this->authorize('update', $purchase);

        try {
            $cancel($purchase, $request->user(), $request->input('reason'));
        } catch (PurchaseNotEditable $e) {
            return $this->jsonOrError(
                $request,
                __('purchases.errors.not_cancellable', ['status' => __('purchases.status.'.$e->status)]),
                route('admin.purchases.show', $purchase),
            );
        }

        return $this->jsonOrRedirect(
            $request,
            __('purchases.flash.cancelled', ['number' => $purchase->number]),
            route('admin.purchases.index'),
        );
    }

    public function destroy(Request $request, Purchase $purchase, DeletePurchase $delete): JsonResponse|RedirectResponse
    {
        $this->authorize('delete', $purchase);

        $number = $purchase->number;
        try {
            $delete($purchase);
        } catch (PurchaseNotEditable $e) {
            return $this->jsonOrError(
                $request,
                __('purchases.errors.not_deletable', ['status' => __('purchases.status.'.$e->status)]),
                route('admin.purchases.show', $purchase),
            );
        }

        return $this->jsonOrRedirect(
            $request,
            __('purchases.flash.deleted', ['number' => $number]),
            route('admin.purchases.index'),
        );
    }

    public function export(Request $request, ExportPurchases $export): \Symfony\Component\HttpFoundation\StreamedResponse
    {
        $this->authorize('viewAny', Purchase::class);

        $format = $request->query('format', 'csv');
        if (! in_array($format, ['csv', 'xlsx'], true)) {
            abort(422, 'Unsupported export format. Use csv or xlsx.');
        }

        return ($export)($format);
    }

    /** @return array<string, mixed> */
    private function editorPayload(Purchase $purchase): array
    {
        return [
            'purchase'     => $purchase,
            'suppliers'    => Supplier::query()->active()->orderBy('name')->get(['id', 'code', 'name', 'default_currency_code', 'payment_terms_days']),
            'stores'       => accessible_stores(),
            'taxGroups'    => TaxGroup::query()->with('components:id,name,rate')->orderBy('name')->get(['id', 'name']),
            'baseCurrency' => Company::current()?->base_currency_code ?? 'USD',
            'currencies'   => \Illuminate\Support\Facades\DB::table('currencies')->orderBy('code')->get(['code', 'name', 'symbol']),
        ];
    }
}
