<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Device;
use App\Models\SyncLog;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class SyncController extends Controller
{
    private const BATCH_LIMIT = 500;

    /**
     * Child tables that have no direct user_id column.
     * They are linked to the user via a parent table.
     * Format: 'table' => ['fk' => 'foreign_key', 'parent' => 'parent_table']
     */
    private const CHILD_TABLES = [
        'order_items'            => ['fk' => 'order_id',    'parent' => 'orders'],
        'purchase_items'         => ['fk' => 'purchase_id', 'parent' => 'purchases'],
        // [INTEGRATION] Additional child tables that resolve ownership via parent
        'order_fees'             => ['fk' => 'order_id',    'parent' => 'orders'],
        'supplier_return_items'  => ['fk' => 'return_id',   'parent' => 'supplier_returns'],
        'customer_return_items'  => ['fk' => 'return_id',   'parent' => 'customer_returns'],
    ];

    /**
     * Table → Model class map (only syncable tables)
     */
    private array $tableMap = [
        'stores'             => \App\Models\Store::class,
        'categories'         => \App\Models\Category::class,
        'products'           => \App\Models\Product::class,
        'customers'          => \App\Models\Customer::class,
        'customer_ledger'    => \App\Models\CustomerLedger::class,
        'orders'             => \App\Models\Order::class,
        'order_items'        => \App\Models\OrderItem::class,
        'expenses'           => \App\Models\Expense::class,
        'suppliers'          => \App\Models\Supplier::class,
        'purchases'          => \App\Models\Purchase::class,
        'purchase_items'     => \App\Models\PurchaseItem::class,
        'quotations'         => \App\Models\Quotation::class,
        'ledger_entries'     => \App\Models\LedgerEntry::class,
        'cash_registers'     => \App\Models\CashRegister::class,
        'stock_adjustments'  => \App\Models\StockAdjustment::class,
        'payment_methods'    => \App\Models\PaymentMethod::class,
        'customer_invoices'  => \App\Models\CustomerInvoice::class,
        // [INTEGRATION] New tables added during Flutter v25 integration
        'units'                  => \App\Models\Unit::class,
        'discounts'              => \App\Models\Discount::class,
        'taxes'                  => \App\Models\Tax::class,
        'fee_configs'            => \App\Models\FeeConfig::class,
        'order_fees'             => \App\Models\OrderFee::class,
        'product_variants'       => \App\Models\ProductVariant::class,
        'product_batches'        => \App\Models\ProductBatch::class,
        'stock_movements'        => \App\Models\StockMovement::class,
        'service_product_usage'  => \App\Models\ServiceProductUsage::class,
        'supplier_returns'       => \App\Models\SupplierReturn::class,
        'supplier_return_items'  => \App\Models\SupplierReturnItem::class,
        'customer_returns'       => \App\Models\CustomerReturn::class,
        'customer_return_items'  => \App\Models\CustomerReturnItem::class,
        // Aliases used by the Flutter client
        'cash_register'      => \App\Models\CashRegister::class,
        'ledger'             => \App\Models\LedgerEntry::class,
    ];

    /**
     * Upload records from device → server (LWW)
     */
    public function upload(Request $request): JsonResponse
    {
        $request->validate([
            'table'     => 'required|string',
            'device_id' => 'required|string',
            'records'   => 'required|array|max:' . self::BATCH_LIMIT,
        ]);

        $table = $request->input('table');
        $deviceId = $request->input('device_id');
        $records = $request->input('records');

        if (!array_key_exists($table, $this->tableMap)) {
            return response()->json(['error' => 'Unknown table: ' . $table], 422);
        }

        $user = $request->user();
        $requiredModule = config('quickropos.sync_table_modules.' . $table);
        if ($requiredModule && !$user->hasModule($requiredModule)) {
            return response()->json(['error' => 'Your plan does not include access to ' . $table], 403);
        }

        $modelClass = $this->tableMap[$table];
        $start = microtime(true);
        $conflicts = 0;
        $upserted = 0;

        if ($table === 'stores') {
            $newRecords = collect($records)->filter(fn ($record) => empty($record['id']));
            $allowed = (int) $user->planLimit('stores', 1);
            $current = \App\Models\Store::where('user_id', $user->id)->count();
            if ($current + $newRecords->count() > $allowed) {
                return response()->json(['error' => 'Store limit reached for your plan'], 422);
            }
        }

        if ($table === 'products') {
            $newRecords = collect($records)->filter(fn ($record) => empty($record['id']));
            $allowed = (int) $user->planLimit('products', 60);
            $current = \App\Models\Product::where('user_id', $user->id)->count();
            if ($current + $newRecords->count() > $allowed) {
                return response()->json(['error' => 'Product limit reached for your plan'], 422);
            }
        }

        DB::transaction(function () use ($records, $modelClass, $user, $table, $deviceId, &$upserted, &$conflicts) {
            $isChildTable = array_key_exists($table, self::CHILD_TABLES);

            // Build a whitelist of columns that actually exist on the server table
            // so Flutter-only extra columns (table_label, adjusted_by, etc.) are
            // silently ignored instead of causing SQLSTATE[42S22] errors.
            $serverColumns = array_flip(Schema::getColumnListing((new $modelClass)->getTable()));

            foreach ($records as $record) {
                // Field-name remapping: Flutter uses different column names for some tables.
                // These remaps must run BEFORE array_intersect_key strips unknown keys.
                if ($table === 'expenses') {
                    // Flutter expense model uses 'description'; server schema uses 'title'.
                    if (!isset($record['title']) && isset($record['description'])) {
                        $record['title'] = $record['description'];
                    }
                    if (empty($record['title'])) {
                        $record['title'] = '';
                    }
                }

                // Strip any columns that don't exist in the server schema
                $record = array_intersect_key($record, $serverColumns);

                // Enforce ownership — child tables (order_items, purchase_items) have
                // no user_id column; ownership is via the parent order/purchase record.
                if (!$isChildTable) {
                    $record['user_id'] = $user->id;
                    $record['device_id'] = $deviceId;
                }

                $existing = null;
                if (!empty($record['id'])) {
                    $usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive($modelClass));
                    $existing = $usesSoftDeletes
                        ? $modelClass::withTrashed()->find($record['id'])
                        : $modelClass::find($record['id']);
                }

                if ($existing) {
                    // LWW: server wins if its updated_at is newer
                    $serverUpdatedAt = Carbon::parse($existing->updated_at);
                    $clientUpdatedAt = !empty($record['updated_at']) ? Carbon::parse($record['updated_at']) : now();

                    if ($serverUpdatedAt->greaterThan($clientUpdatedAt)) {
                        $conflicts++;
                        continue; // Server wins
                    }

                    // If the record is soft-deleted on the server but the client is
                    // sending it as active (deleted_at = null / absent), restore it first
                    // so fill() can update all its columns. soft-deleted rows are otherwise
                    // invisible to normal scope queries.
                    $usesSoftDeletesLocal = in_array(SoftDeletes::class, class_uses_recursive($modelClass));
                    if ($usesSoftDeletesLocal && $existing->trashed()) {
                        $clientDeletedAt = $record['deleted_at'] ?? null;
                        if (empty($clientDeletedAt)) {
                            $existing->restore();
                        }
                    }

                    $existing->fill($record);
                    // Explicitly preserve the client's updated_at so Eloquent does not
                    // auto-bump it to now() on save(). Without this every accepted update
                    // advances the server timestamp, causing the next device to conflict.
                    if (!empty($record['updated_at'])) {
                        $existing->updated_at = Carbon::parse($record['updated_at']);
                    }
                    $existing->save();
                } else {
                    $new = new $modelClass;
                    $new->forceFill($record);
                    // Preserve timestamps provided by the client
                    if (!empty($record['updated_at'])) {
                        $new->updated_at = Carbon::parse($record['updated_at']);
                    }
                    if (!empty($record['created_at'])) {
                        $new->created_at = Carbon::parse($record['created_at']);
                    }
                    $new->save();
                }
                $upserted++;
            }
        });

        $duration = (int)((microtime(true) - $start) * 1000);

        // Update device last sync time
        Device::where('device_id', $deviceId)->update(['last_sync_at' => now()]);

        // When stores are synced, keep the user's profile fields up to date
        // from their primary (oldest) store so the admin shows correct data.
        if ($table === 'stores') {
            $primaryStore = \App\Models\Store::where('user_id', $user->id)
                ->orderBy('created_at')
                ->first();
            if ($primaryStore) {
                $profileUpdates = [];
                if (!empty($primaryStore->name)) {
                    $profileUpdates['business_name'] = $primaryStore->name;
                }
                if (!empty($primaryStore->phone)) {
                    $profileUpdates['phone'] = $primaryStore->phone;
                }
                if (!empty($primaryStore->country_code)) {
                    $profileUpdates['country_code'] = $primaryStore->country_code;
                }
                if (!empty($primaryStore->currency_code)) {
                    $profileUpdates['currency_code'] = $primaryStore->currency_code;
                }
                if (!empty($profileUpdates)) {
                    $user->update($profileUpdates);
                }
            }
        }

        SyncLog::create([
            'user_id' => $user->id,
            'device_id' => $deviceId,
            'direction' => 'upload',
            'table_name' => $table,
            'records_count' => $upserted,
            'conflicts_count' => $conflicts,
            'status' => 'success',
            'duration_ms' => $duration,
        ]);

        return response()->json([
            'upserted' => $upserted,
            'conflicts' => $conflicts,
            'server_time' => now()->toIso8601String(),
        ]);
    }

    /**
     * Download records changed since a timestamp
     */
    public function download(Request $request): JsonResponse
    {
        $request->validate([
            'table'      => 'required|string',
            'since'      => 'nullable|date',
            'device_id'  => 'required|string',
            'page'       => 'nullable|integer|min:1',
        ]);

        $table = $request->input('table');
        $since = $request->input('since');
        $deviceId = $request->input('device_id');
        $page = (int) $request->input('page', 1);

        if (!array_key_exists($table, $this->tableMap)) {
            return response()->json(['error' => 'Unknown table: ' . $table], 422);
        }

        $user = $request->user();
        $requiredModule = config('quickropos.sync_table_modules.' . $table);
        if ($requiredModule && !$user->hasModule($requiredModule)) {
            return response()->json(['error' => 'Your plan does not include access to ' . $table], 403);
        }
        $modelClass = $this->tableMap[$table];
        $usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive($modelClass));
        $childConfig = self::CHILD_TABLES[$table] ?? null;

        if ($childConfig) {
            // Child table (e.g. order_items, purchase_items) — no direct user_id column;
            // filter via parent table subquery. These tables have no device_id column so
            // we cannot filter out the requesting device's own records here.
            $fk     = $childConfig['fk'];
            $parent = $childConfig['parent'];
            $query  = ($usesSoftDeletes ? $modelClass::withTrashed() : $modelClass::query())
                ->whereIn($fk, function ($q) use ($parent, $user) {
                    $q->select('id')->from($parent)->where('user_id', $user->id);
                })
                ->orderBy('updated_at');
        } else {
            $query = ($usesSoftDeletes ? $modelClass::withTrashed() : $modelClass::query())
                ->where('user_id', $user->id)
                ->where('device_id', '!=', $deviceId)
                ->orderBy('updated_at');
        }

        if ($since) {
            $query->where('updated_at', '>=', $since);
        }

        $total = $query->count();
        $records = $query->offset(($page - 1) * self::BATCH_LIMIT)->limit(self::BATCH_LIMIT)->get();

        // For order_items, embed the parent order's invoice_number so that receiving
        // devices can remap order_id to their own local order id (integer ids differ
        // per device; invoice_number is the stable cross-device key).
        if ($table === 'order_items') {
            $orderIds = $records->pluck('order_id')->unique()->filter();
            $invoiceMap = \App\Models\Order::whereIn('id', $orderIds)
                ->pluck('invoice_number', 'id');
            $records = $records->map(function ($item) use ($invoiceMap) {
                $arr = $item->toArray();
                $arr['order_invoice_number'] = $invoiceMap[$item->order_id] ?? null;
                return $arr;
            });
        }

        SyncLog::create([
            'user_id' => $user->id,
            'device_id' => $deviceId,
            'direction' => 'download',
            'table_name' => $table,
            'records_count' => $records->count(),
            'status' => 'success',
        ]);

        return response()->json([
            'records' => $records,
            'total' => $total,
            'page' => $page,
            'has_more' => ($page * self::BATCH_LIMIT) < $total,
            'server_time' => now()->toIso8601String(),
        ]);
    }

    /**
     * Upload an image by SHA256 hash (deduplication)
     */
    public function uploadImage(Request $request): JsonResponse
    {
        $request->validate([
            'image' => 'required|file|image|max:5120',
            'hash'  => 'required|string|size:64',
            'table_ref' => 'nullable|string',
            'record_id' => 'nullable|string',
        ]);

        $user = $request->user();
        $hash = $request->input('hash');

        // Check if already uploaded
        $existing = \App\Models\ImageIndex::where('user_id', $user->id)->where('hash', $hash)->first();
        if ($existing) {
            return response()->json(['url' => $existing->file_path, 'hash' => $hash, 'cached' => true]);
        }

        $file = $request->file('image');
        $path = $file->store("sync/{$user->id}/images", 'public');

        $img = \App\Models\ImageIndex::create([
            'user_id' => $user->id,
            'hash' => $hash,
            'file_path' => $path,
            'mime_type' => $file->getMimeType(),
            'size_bytes' => $file->getSize(),
            'table_ref' => $request->input('table_ref'),
            'record_id' => $request->input('record_id'),
        ]);

        // Update user storage usage
        $user->increment('storage_used_bytes', $file->getSize());

        return response()->json(['url' => $path, 'hash' => $hash, 'cached' => false], 201);
    }

    /**
     * Get image URL by hash
     */
    public function imageByHash(Request $request, string $hash): JsonResponse
    {
        $user = $request->user();
        $img = \App\Models\ImageIndex::where('user_id', $user->id)->where('hash', $hash)->firstOrFail();
        return response()->json(['url' => asset('storage/' . $img->file_path), 'hash' => $hash]);
    }

    /**
     * List all uploaded images for the authenticated user
     */
    public function listImages(Request $request): JsonResponse
    {
        $user = $request->user();
        $images = \App\Models\ImageIndex::where('user_id', $user->id)
            ->get(['hash', 'file_path', 'mime_type', 'size_bytes', 'table_ref', 'record_id'])
            ->map(fn ($img) => [
                'hash'      => $img->hash,
                'url'       => asset('storage/' . $img->file_path),
                'filename'  => basename($img->file_path),
                'table_ref' => $img->table_ref,
                'record_id' => $img->record_id,
            ]);

        return response()->json(['images' => $images]);
    }

    /**
     * Upload a PDF file by SHA-256 hash (deduplication).
     * Accepts invoices, receipts, quotation, and purchase PDFs.
     */
    public function uploadPdf(Request $request): JsonResponse
    {
        $request->validate([
            'pdf'      => 'required|file|mimes:pdf|max:10240',
            'hash'     => 'required|string|size:64',
            'pdf_type' => 'nullable|string|in:invoice,receipt,quotation,purchase',
            'ref_id'   => 'nullable|string',
        ]);

        $user = $request->user();
        $hash = $request->input('hash');

        // De-duplicate by hash
        $existing = \App\Models\ImageIndex::where('user_id', $user->id)
            ->where('hash', $hash)
            ->where('mime_type', 'application/pdf')
            ->first();
        if ($existing) {
            return response()->json([
                'url'    => asset('storage/' . $existing->file_path),
                'hash'   => $hash,
                'cached' => true,
            ]);
        }

        $file  = $request->file('pdf');
        $path  = $file->store("sync/{$user->id}/pdfs", 'public');

        \App\Models\ImageIndex::create([
            'user_id'   => $user->id,
            'hash'      => $hash,
            'file_path' => $path,
            'mime_type' => 'application/pdf',
            'size_bytes'=> $file->getSize(),
            'table_ref' => $request->input('pdf_type'),
            'record_id' => $request->input('ref_id'),
        ]);

        $user->increment('storage_used_bytes', $file->getSize());

        return response()->json([
            'url'    => asset('storage/' . $path),
            'hash'   => $hash,
            'cached' => false,
        ], 201);
    }

    /**
     * List all uploaded PDFs for the authenticated user
     */
    public function listPdfs(Request $request): JsonResponse
    {
        $user = $request->user();
        $pdfs = \App\Models\ImageIndex::where('user_id', $user->id)
            ->where('mime_type', 'application/pdf')
            ->get(['hash', 'file_path', 'size_bytes', 'table_ref', 'record_id'])
            ->map(fn ($pdf) => [
                'hash'     => $pdf->hash,
                'url'      => asset('storage/' . $pdf->file_path),
                'filename' => basename($pdf->file_path),
                'pdf_type' => $pdf->table_ref,
                'ref_id'   => $pdf->record_id,
            ]);

        return response()->json(['pdfs' => $pdfs]);
    }

    /**
     * Get PDF download URL by hash
     */
    public function pdfByHash(Request $request, string $hash): JsonResponse
    {
        $user = $request->user();
        $pdf  = \App\Models\ImageIndex::where('user_id', $user->id)
            ->where('hash', $hash)
            ->where('mime_type', 'application/pdf')
            ->firstOrFail();

        return response()->json([
            'url'  => asset('storage/' . $pdf->file_path),
            'hash' => $hash,
        ]);
    }

    // =========================================================================
    // V1 Compatibility Methods  (X-Api-Key-only auth via api.device middleware)
    // These expose the same logic as the Sanctum-authed methods above, but with
    // a simplified request/response format that matches the Flutter ApiSyncService.
    // =========================================================================

    /**
     * V1: Return active voucher codes the device may use for local redemption.
     */
    public function vouchersV1(Request $request): JsonResponse
    {
        $user = $request->user();
        $vouchers = \App\Models\Voucher::where('is_active', true)
            ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
            ->get(['code', 'type', 'value', 'plan', 'days', 'expires_at'])
            ->map(fn ($v) => [
                'code'        => $v->code,
                'type'        => $v->type,
                'value'       => $v->value,
                'plan'        => $v->plan,
                'valid_until' => optional($v->expires_at)->toIso8601String(),
            ]);

        return response()->json(['data' => $vouchers]);
    }

    /**
     * V1: Redeem a voucher code and activate subscription.
     */
    public function voucherRedeemV1(Request $request): JsonResponse
    {
        // Delegate to SubscriptionController logic
        return app(\App\Http\Controllers\Api\SubscriptionController::class)->redeemVoucher($request);
    }

    /**
     * V1: Push a full snapshot of all SQLite tables.
     * Body: { device_id, email?, name?, store_id?, tables: { tableName: [...rows] } }
     * Response: { accepted: [...tableNames] }
     */
    public function pushSnapshotV1(Request $request): JsonResponse
    {
        $request->validate([
            'device_id' => 'required|string',
            'tables'    => 'required|array',
        ]);

        $user     = $request->user();
        $deviceId = $request->input('device_id');
        $tables   = $request->input('tables', []);
        $accepted = [];

        foreach ($tables as $table => $rows) {
            if (!is_array($rows) || !array_key_exists($table, $this->tableMap)) {
                continue;
            }
            if (empty($rows)) {
                $accepted[] = $table;
                continue;
            }
            // Reuse the per-table upload logic
            $request->merge(['table' => $table, 'records' => $rows]);
            $result = $this->upload($request);
            if ($result->getStatusCode() < 400) {
                $accepted[] = $table;
            }
        }

        return response()->json([
            'accepted'    => $accepted,
            'server_time' => now()->toIso8601String(),
        ]);
    }

    /**
     * V1: Pull all table snapshots stored for this device.
     * Query: device_id, since? (ISO-8601 date/time for incremental pull)
     * Response: { tables: { tableName: { records: [...], record_count: N, synced_at: "..." } } }
     */
    public function pullSnapshotV1(Request $request): JsonResponse
    {
        $request->validate([
            'device_id' => 'required|string',
            'since'     => 'nullable|date',
        ]);

        $user     = $request->user();
        $deviceId = $request->input('device_id');
        $since    = $request->input('since');
        $result   = [];

        foreach (array_keys($this->tableMap) as $table) {
            // Skip alias tables (they share the same underlying table)
            if (in_array($table, ['cash_register', 'ledger'])) {
                continue;
            }
            $request->merge(['table' => $table, 'device_id' => $deviceId, 'since' => $since, 'page' => 1]);
            $resp = $this->download($request);
            if ($resp->getStatusCode() < 400) {
                $data = json_decode($resp->getContent(), true);
                $records = $data['records'] ?? [];
                if (!empty($records)) {
                    $result[$table] = [
                        'records'      => $records,
                        'record_count' => count($records),
                        'synced_at'    => now()->toIso8601String(),
                    ];
                }
            }
        }

        return response()->json([
            'tables'      => empty($result) ? (object) [] : $result,
            'server_time' => now()->toIso8601String(),
        ]);
    }

    /**
     * V1: Push a single completed sale (order + order_items).
     * Body: { invoice_number?, store_id?, ... order fields ..., items: [...] }
     */
    public function pushSaleV1(Request $request): JsonResponse
    {
        $user     = $request->user();
        $deviceId = $request->input('device_id', 'unknown');
        $payload  = $request->all();

        $items = $payload['items'] ?? $payload['order_items'] ?? [];
        unset($payload['items'], $payload['order_items']);

        // Push order record
        $request->merge(['table' => 'orders', 'device_id' => $deviceId, 'records' => [$payload]]);
        $resp = $this->upload($request);
        if ($resp->getStatusCode() >= 400) {
            return $resp;
        }

        // Push order items
        if (!empty($items)) {
            $request->merge(['table' => 'order_items', 'device_id' => $deviceId, 'records' => $items]);
            $this->upload($request);
        }

        return response()->json(['ok' => true, 'server_time' => now()->toIso8601String()]);
    }

    /**
     * V1: Push full product list.
     * Body: { products: [...], device_id? }
     */
    public function pushInventoryV1(Request $request): JsonResponse
    {
        $request->validate(['products' => 'required|array']);

        $deviceId = $request->input('device_id', 'unknown');
        $products = $request->input('products');

        $request->merge(['table' => 'products', 'device_id' => $deviceId, 'records' => $products]);
        $resp = $this->upload($request);
        if ($resp->getStatusCode() >= 400) {
            return $resp;
        }

        $data = json_decode($resp->getContent(), true);
        return response()->json([
            'ok'          => true,
            'upserted'    => $data['upserted'] ?? count($products),
            'server_time' => now()->toIso8601String(),
        ]);
    }

    /**
     * V1: Upload a media file (image / document) by multipart form-data.
     * Fields: file (required), device_id?, entity_table?, entity_id?
     */
    public function uploadMediaV1(Request $request): JsonResponse
    {
        $request->validate([
            'file' => 'required|file|max:10240',
        ]);

        $user  = $request->user();
        $file  = $request->file('file');
        $hash  = hash_file('sha256', $file->getRealPath());
        $mime  = $file->getMimeType() ?? '';
        $table = $request->input('entity_table');
        $recId = $request->input('entity_id');

        // De-duplicate
        $existing = \App\Models\ImageIndex::where('user_id', $user->id)
            ->where('hash', $hash)->first();
        if ($existing) {
            return response()->json([
                'id'     => $existing->id,
                'url'    => asset('storage/' . $existing->file_path),
                'cached' => true,
            ]);
        }

        $subDir = str_contains($mime, 'pdf') ? 'pdfs' : 'images';
        $path   = $file->store("sync/{$user->id}/{$subDir}", 'public');

        $img = \App\Models\ImageIndex::create([
            'user_id'    => $user->id,
            'hash'       => $hash,
            'file_path'  => $path,
            'mime_type'  => $mime,
            'size_bytes' => $file->getSize(),
            'table_ref'  => $table,
            'record_id'  => $recId,
        ]);

        $user->increment('storage_used_bytes', $file->getSize());

        return response()->json([
            'id'    => $img->id,
            'url'   => asset('storage/' . $path),
            'cached'=> false,
        ], 201);
    }

    /**
     * V1: List all uploaded media files for the authenticated device's user.
     * Query: device_id?
     */
    public function listMediaV1(Request $request): JsonResponse
    {
        $user  = $request->user();
        $files = \App\Models\ImageIndex::where('user_id', $user->id)
            ->get(['id', 'hash', 'file_path', 'mime_type', 'size_bytes', 'table_ref', 'record_id'])
            ->map(fn ($f) => [
                'id'         => $f->id,
                'hash'       => $f->hash,
                'url'        => asset('storage/' . $f->file_path),
                'filename'   => basename($f->file_path),
                'mime'       => $f->mime_type,
                'table_ref'  => $f->table_ref,
                'record_id'  => $f->record_id,
            ]);

        return response()->json(['files' => $files]);
    }
}
