<?php

namespace App\Http\Controllers\Admin;

use App\Actions\Products\CreateProduct;
use App\Actions\Products\DeleteProduct;
use App\Actions\Products\ExportProducts;
use App\Actions\Products\SyncProductKitItems;
use App\Actions\Products\SyncProductPrices;
use App\Actions\Products\SyncProductVariants;
use App\Actions\Products\ToggleProductFlag;
use App\Actions\Products\UpdateProduct;
use App\Exceptions\ProductHasSales;
use App\Exceptions\ProductVariantHasSales;
use App\Http\Controllers\Concerns\RespondsJsonOrRedirect;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ProductRequest;
use App\Models\Brand;
use App\Models\Category;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\TaxGroup;
use App\Models\Unit;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Illuminate\View\View;

/**
 * Products module — full-page CRUD.
 *
 * Unlike the master-detail flows used by Categories / Brands / Units,
 * Products has too many fields to fit a side-editor, so create + edit
 * each get their own dedicated page. The list view is plain table-mode
 * data-table with sortable columns + search + pagination.
 *
 * Image upload follows the same shape as Brand:
 *   - `image` (file) → store and overwrite path
 *   - `image_remove=1` → drop the stored file, blank the column
 *   - field absent → leave existing image alone
 */
class ProductController extends Controller
{
    use RespondsJsonOrRedirect;

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

        $products = Product::query()
            ->with([
                'category:id,name',
                'brand:id,name,logo_path',
                'unit:id,code,name',
                'taxGroup:id,name',
            ])
            // Counts drive the "Variant · N" / "Kit · N" type badges.
            ->withCount(['variants', 'kitItems'])
            ->ordered()
            ->get();

        // Summary cards — initial values from the full set. Products filter
        // CLIENT-side (productsPage / dataTable), so `_dtOnRender` recomputes
        // these from the visible rows on every search/filter. `key` ties each
        // value to its DOM node so the JS can update it.
        $summaryCards = [
            ['label' => __('products.summary.total'),    'value' => number_format($products->count()),                           'key' => 'total'],
            ['label' => __('products.summary.active'),   'value' => number_format($products->where('is_active', true)->count()),  'key' => 'active',   'tone' => 'positive'],
            ['label' => __('products.summary.featured'), 'value' => number_format($products->where('is_featured', true)->count()), 'key' => 'featured'],
        ];

        return view('admin.products.index', [
            'products'         => $products,
            'summaryCards'     => $summaryCards,
            'filterCategories' => Category::query()->active()->ordered()->get(['id', 'name']),
        ]);
    }

    /**
     * JSON search for the remote product picker (kit components, and any
     * future product-select). Returns at most 25 active products matching
     * the query on name / SKU / barcode, each with its variants so a
     * picker can offer "lock to variant" without a second round-trip.
     *
     * Shape: `[{ value, label, variants: [{id, label}] }]`.
     */
    public function search(Request $request): JsonResponse
    {
        $this->authorize('viewAny', Product::class);

        $q       = trim((string) $request->query('q', ''));
        $exclude = (int) $request->query('exclude', 0);

        $products = Product::query()
            ->active()
            ->when($exclude > 0, fn ($qb) => $qb->where('id', '!=', $exclude))
            ->when($q !== '', fn ($qb) => $qb->where(fn ($w) =>
                $w->where('name', 'like', "%{$q}%")
                  ->orWhere('sku', 'like', "%{$q}%")
                  ->orWhere('barcode', 'like', "%{$q}%")
            ))
            ->orderBy('name')
            ->limit(25)
            ->get(['id', 'name', 'sku', 'track_batches', 'track_expiry']);

        $variantsByProduct = ProductVariant::query()
            ->whereIn('product_id', $products->pluck('id'))
            ->orderBy('id')
            ->get(['id', 'product_id', 'sku', 'attributes'])
            ->groupBy('product_id');

        return response()->json(
            $products->map(fn (Product $p) => [
                'value'         => (string) $p->id,
                'label'         => $p->name.' ('.$p->sku.')',
                'track_batches' => (bool) $p->track_batches,
                'track_expiry'  => (bool) $p->track_expiry,
                'variants'      => $variantsByProduct->get($p->id, collect())
                    ->map(fn (ProductVariant $v) => ['id' => (string) $v->id, 'label' => (string) $v->label])
                    ->values(),
            ])->all()
        );
    }

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

        return view('admin.products.create', $this->formContext());
    }

    public function store(ProductRequest $request, CreateProduct $create, SyncProductVariants $syncVariants, SyncProductKitItems $syncKitItems, SyncProductPrices $syncPrices): JsonResponse|RedirectResponse
    {
        $this->authorize('create', Product::class);

        $data = $request->persistedAttributes();
        $data['created_by'] = $request->user()?->id;
        $data['updated_by'] = $request->user()?->id;

        if ($request->hasFile('image')) {
            $data['image_path'] = $request->file('image')->store('products', 'public');
        }

        $product = ($create)($data);

        // Sync variants. For `simple` / `kit`, the payload is empty
        // — sync becomes a no-op (nothing to delete, nothing to add).
        // For `variant`, we persist whatever the user provided.
        ($syncVariants)(
            $product,
            $product->type === 'variant' ? $request->variantsPayload() : []
        );

        // Sync kit components — mirror shape for `type=kit`.
        ($syncKitItems)(
            $product,
            $product->type === 'kit' ? $request->kitItemsPayload() : []
        );

        // Product-level per-store overrides apply only to simple / kit
        // products. Variant products price per-child, so their store
        // overrides ride along inside SyncProductVariants above.
        if ($product->type !== 'variant') {
            ($syncPrices)($product, $request->storePricesPayload());
        }

        return $this->jsonOrRedirect(
            $request,
            __('products.flash.created', ['name' => $product->name]),
            route('admin.products.edit', $product),
        );
    }

    public function edit(Product $product): View
    {
        $this->authorize('update', $product);

        // Eager-load variants + kit items so the editor renders existing
        // rows without an N+1 problem on first paint. Kit items pull in
        // their component product / variant so the picker can render the
        // current selection by name without a follow-up query.
        $product->load([
            'variants' => fn ($q) => $q->orderBy('id'),
            'kitItems' => fn ($q) => $q->orderBy('sort_order')->orderBy('id'),
            'kitItems.component:id,name,sku',
            'kitItems.variant:id,product_id,sku,attributes',
            'prices',
        ]);

        return view('admin.products.edit', array_merge($this->formContext(), [
            'product' => $product,
        ]));
    }

    public function update(ProductRequest $request, Product $product, UpdateProduct $update, SyncProductVariants $syncVariants, SyncProductKitItems $syncKitItems, SyncProductPrices $syncPrices): JsonResponse|RedirectResponse
    {
        $this->authorize('update', $product);

        $data = $request->persistedAttributes();
        $data['updated_by'] = $request->user()?->id;

        // Three image lifecycles — same as BrandController::update().
        if ($request->hasFile('image')) {
            $this->deleteStoredImage($product);
            $data['image_path'] = $request->file('image')->store('products', 'public');
        } elseif ($request->boolean('image_remove')) {
            $this->deleteStoredImage($product);
            $data['image_path'] = null;
        }

        ($update)($product, $data);

        // Always run sync — payload is the user's variants[] when the
        // product is `type=variant`, empty otherwise. The empty case
        // matters on type transitions away from variant: it wipes any
        // orphaned children left behind. Sales-guard inside sync may
        // refuse if a child variant has historical sale lines.
        $fresh = $product->fresh();
        try {
            ($syncVariants)(
                $fresh,
                $fresh->type === 'variant' ? $request->variantsPayload() : []
            );
        } catch (ProductVariantHasSales $e) {
            return $this->jsonOrError(
                $request,
                __('products.errors.variant_has_sales', [
                    'label' => $e->label,
                    'count' => $e->count,
                ]),
                route('admin.products.edit', $product),
            );
        }

        // Same shape for kit components — empty payload on non-kit
        // types wipes any orphans from a prior `type=kit` state.
        ($syncKitItems)(
            $fresh,
            $fresh->type === 'kit' ? $request->kitItemsPayload() : []
        );

        // Product-level per-store overrides — simple / kit only. Variant
        // products' overrides are written per-child inside the variant
        // sync. On a type-down-switch away from variant, the stale
        // variant rows cascade-delete with their variants.
        if ($fresh->type !== 'variant') {
            ($syncPrices)($fresh, $request->storePricesPayload());
        }

        // Redirect to the list instead of back to the edit page — the
        // user can immediately scan / pick another product to edit.
        // Successful saves should feel "done", not "still here".
        return $this->jsonOrRedirect(
            $request,
            __('products.flash.updated', ['name' => $product->name]),
            route('admin.products.index'),
        );
    }

    public function destroy(Request $request, Product $product, DeleteProduct $delete): JsonResponse|RedirectResponse
    {
        $this->authorize('delete', $product);

        $name = $product->name;

        try {
            ($delete)($product);
        } catch (ProductHasSales $e) {
            return $this->jsonOrError(
                $request,
                __('products.errors.has_sales', ['count' => $e->count, 'name' => $name]),
                route('admin.products.edit', $product),
            );
        }

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

    /**
     * Row-level quick-action: flip a single boolean flag (is_active or
     * is_featured) without running through the full validation
     * pipeline. Returns JSON so the products list page can update
     * optimistically without a full reload.
     */
    public function toggle(Request $request, Product $product, ToggleProductFlag $toggle): JsonResponse
    {
        $this->authorize('update', $product);

        $data = $request->validate([
            'field' => ['required', Rule::in(['is_active', 'is_featured'])],
            'value' => ['required', 'boolean'],
        ]);

        ($toggle)($product, $data['field'], (bool) $data['value']);

        return response()->json([
            'ok'          => true,
            'id'          => $product->id,
            'is_active'   => (bool) $product->is_active,
            'is_featured' => (bool) $product->is_featured,
            'message'     => __('products.flash.updated', ['name' => $product->name]),
        ]);
    }

    public function export(Request $request, ExportProducts $export): \Symfony\Component\HttpFoundation\StreamedResponse
    {
        $this->authorize('viewAny', Product::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);
    }

    /* ── Helpers ────────────────────────────────────────────────── */

    /**
     * Shared dropdown data for the create + edit forms. Eager-load just
     * what the option lists need so the form payload stays light.
     *
     * @return array<string, mixed>
     */
    private function formContext(): array
    {
        // Include `tax_group_id` on categories — the form uses this to
        // pre-fill the Tax rule field when the user picks a category
        // that has a default rule configured.
        $categories = Category::query()->active()->ordered()->get(['id', 'name', 'tax_group_id']);

        return [
            'categories'     => $categories,
            'brands'         => Brand::query()->active()->ordered()->get(['id', 'name']),
            'units'          => Unit::query()->active()->ordered()->get(['id', 'code', 'name', 'category']),
            'taxGroups'      => TaxGroup::query()->active()->ordered()->get(['id', 'name']),
            // Per-store price-override UI uses this list. Hidden when
            // empty (single-store installs that haven't yet seeded a
            // store row).
            'stores'         => Store::query()->active()->ordered()->get(['id', 'name', 'code', 'currency_code']),
            // Plain map { category_id (string) → tax_group_id (string|null) }
            // for the editor's JS — categoryId / taxGroupId watcher.
            'categoryTaxMap' => $categories
                ->mapWithKeys(fn ($c) => [(string) $c->id => $c->tax_group_id ? (string) $c->tax_group_id : ''])
                ->all(),
            // Dynamic lookup — managed via /admin/drug-schedules.
            'pharmacySchedules' => \App\Models\DrugSchedule::query()
                ->active()
                ->ordered()
                ->get(['code', 'name', 'country_code']),
        ];
    }

    private function deleteStoredImage(Product $product): void
    {
        if (! $product->image_path) return;
        if (Storage::disk('public')->exists($product->image_path)) {
            Storage::disk('public')->delete($product->image_path);
        }
    }
}
