<?php
namespace App\Core;

use RuntimeException;
use Throwable;

/**
 * Schema-aware form metadata, validation and secure upload handling used by
 * administration CRUD pages. The class intentionally keeps configuration in
 * PHP so the project remains Composer-free and deployable on shared hosting.
 */
class SmartForm
{
    private static array $columnDetails = [];
    private static array $optionCache = [];

    public static function field(string $table, string $field): array
    {
        $details = self::columnDetails($table)[$field] ?? [];
        $type = strtolower((string)($details['Type'] ?? 'varchar(255)'));
        $nullable = strtoupper((string)($details['Null'] ?? 'YES')) === 'YES';
        $default = $details['Default'] ?? null;

        $config = [
            'name' => $field,
            'label' => self::friendlyLabel($table, $field),
            'type' => 'text',
            'required' => !$nullable && $default === null,
            'section' => self::section($field),
            'col' => 6,
            'help' => self::help($table, $field),
            'placeholder' => self::placeholder($field),
            'options' => [],
            'accept' => '',
            'rich' => false,
            'image' => false,
            'file' => false,
            'json' => false,
            'auto' => null,
            'min' => null,
            'max' => null,
            'step' => null,
        ];

        if (self::isRelation($table, $field)) {
            $config['type'] = 'select';
            $config['options'] = self::relationOptions($table, $field);
            $config['col'] = 6;
            return self::override($table, $field, $config);
        }

        $selectOptions = self::selectOptions($table, $field);
        if ($selectOptions !== []) {
            $config['type'] = 'select';
            $config['options'] = $selectOptions;
            return self::override($table, $field, $config);
        }

        if (self::isBoolean($field, $type)) {
            $config['type'] = 'select';
            $config['options'] = [['value' => '1', 'label' => 'Yes'], ['value' => '0', 'label' => 'No']];
            $config['col'] = 4;
        } elseif (self::isImageField($field)) {
            $config['type'] = 'file';
            $config['image'] = true;
            $config['file'] = true;
            $config['accept'] = 'image/jpeg,image/png,image/webp';
            $config['col'] = 6;
        } elseif (self::isDocumentField($field)) {
            $config['type'] = 'file';
            $config['file'] = true;
            $config['accept'] = '.pdf,.zip,.csv,.txt,image/jpeg,image/png,image/webp';
            $config['col'] = 6;
        } elseif (self::isRichText($field)) {
            $config['type'] = 'textarea';
            $config['rich'] = true;
            $config['col'] = 12;
        } elseif (self::isLongText($field, $type)) {
            $config['type'] = 'textarea';
            $config['col'] = 12;
            $config['json'] = self::isJsonField($field);
        } elseif (self::isJsonField($field)) {
            $config['type'] = 'textarea';
            $config['json'] = true;
            $config['col'] = 12;
        } elseif (preg_match('/(^|_)(email)$/', $field)) {
            $config['type'] = 'email';
        } elseif (str_contains($field, 'url') || in_array($field, ['link', 'endpoint'], true)) {
            $config['type'] = 'url';
        } elseif ($field === 'color') {
            $config['type'] = 'color';
            $config['col'] = 4;
        } elseif (preg_match('/(^|_)(date|business_date|period_start|period_end|starts_on|ends_on)$/', $field) || $type === 'date') {
            $config['type'] = 'date';
            $config['col'] = 4;
        } elseif (preg_match('/(_at)$/', $field) || str_contains($type, 'datetime') || str_contains($type, 'timestamp')) {
            $config['type'] = 'datetime-local';
            $config['col'] = 4;
        } elseif (self::isNumeric($type, $field)) {
            $config['type'] = 'number';
            $config['step'] = str_contains($type, 'decimal') || str_contains($type, 'float') || self::isMoneyField($field) ? '0.01' : '1';
            $config['min'] = self::allowsNegative($field) ? null : '0';
            $config['col'] = 4;
        } elseif (preg_match('/(^|_)(phone|mobile|contact_phone)$/', $field)) {
            $config['type'] = 'tel';
        }

        if ($field === 'slug') $config['auto'] = 'slug';
        if ($field === 'meta_title') $config['auto'] = 'meta-title';
        if ($field === 'meta_keywords') {
            $config['auto'] = 'meta-keywords';
            $config['col'] = 12;
        }
        if (in_array($field, ['code','sku','barcode','product_code','referral_code','register_code','order_no','invoice_no','sale_no','refund_no','hold_no'], true)) {
            $config['auto'] = 'code';
        }

        return self::override($table, $field, $config);
    }

    public static function sections(string $table, array $fields): array
    {
        $result = [];
        foreach ($fields as $field) {
            $config = self::field($table, $field);
            $result[$config['section']][] = $config;
        }
        return $result;
    }

    public static function prepare(string $table, array $input, array $files, ?int $id = null, ?array $existing = null): array
    {
        $allowed = ModuleCrud::allowedColumns($table);
        $payload = [];
        $existing ??= $id ? ModuleCrud::find($table, $id) : [];

        foreach ($allowed as $field) {
            $config = self::field($table, $field);
            $value = $input[$field] ?? null;

            if ($config['file']) {
                $remove = !empty($input['remove_' . $field]);
                $old = (string)($existing[$field] ?? '');
                if ($remove) {
                    self::removeOwnedUpload($old);
                    $old = '';
                }
                if (!empty($files[$field]) && (int)($files[$field]['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
                    $newPath = self::storeUpload($table, $field, $files[$field], (bool)$config['image']);
                    self::removeOwnedUpload($old);
                    $value = $newPath;
                } else {
                    $value = $old;
                }
            }

            if ($value === null && $config['type'] === 'select' && self::isBoolean($field, strtolower((string)((self::columnDetails($table)[$field]['Type'] ?? ''))))) {
                $value = '0';
            }

            if (is_string($value)) $value = trim($value);
            if ($config['required'] && ($value === '' || $value === null)) {
                throw new RuntimeException($config['label'] . ' is required.');
            }

            if ($value === '' && !$config['required']) {
                $payload[$field] = null;
                continue;
            }

            if ($config['rich']) {
                $value = \sanitize_rich_html((string)$value);
            } elseif ($config['json'] && $value !== null && $value !== '') {
                json_decode((string)$value, true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    throw new RuntimeException($config['label'] . ' must contain valid JSON.');
                }
                $value = json_encode(json_decode((string)$value, true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            } elseif ($config['type'] === 'email' && $value !== null && !filter_var((string)$value, FILTER_VALIDATE_EMAIL)) {
                throw new RuntimeException($config['label'] . ' must be a valid email address.');
            } elseif ($config['type'] === 'url' && $value !== null && !self::isSafeUrl((string)$value)) {
                throw new RuntimeException($config['label'] . ' must be a valid HTTP(S) URL or a relative path.');
            } elseif ($config['type'] === 'number' && $value !== null && !is_numeric($value)) {
                throw new RuntimeException($config['label'] . ' must be numeric.');
            } elseif ($config['type'] === 'select' && $config['options'] !== [] && $value !== null) {
                $validValues = array_map(static fn(array $option): string => (string)$option['value'], $config['options']);
                if (!in_array((string)$value, $validValues, true)) {
                    throw new RuntimeException('Select a valid ' . strtolower($config['label']) . '.');
                }
            }

            if ($field === 'slug') {
                $source = (string)($value ?: ($input['name'] ?? $input['title'] ?? $input['shop_name'] ?? 'item'));
                $value = \unique_slug($table, $source, $id);
            }
            if ($field === 'meta_title' && ($value === null || $value === '')) {
                $value = mb_substr((string)($input['name'] ?? $input['title'] ?? $input['shop_name'] ?? ''), 0, 190);
            }
            if ($field === 'meta_keywords' && ($value === null || $value === '')) {
                $value = self::keywords($input);
            }
            if (in_array($field, ['code','sku','barcode','product_code','referral_code','register_code'], true) && ($value === null || $value === '')) {
                $value = self::generateCode($table, $field, $input);
            }

            if (is_string($value)) {
                $max = self::varcharLength((string)(self::columnDetails($table)[$field]['Type'] ?? ''));
                if ($max > 0 && mb_strlen($value) > $max) $value = mb_substr($value, 0, $max);
            }
            $payload[$field] = $value;
        }

        return $payload;
    }

    public static function storeFile(string $table, string $field, array $file, bool $image = true): string
    {
        return self::storeUpload($table, $field, $file, $image);
    }

    public static function removeFile(string $path): void
    {
        self::removeOwnedUpload($path);
    }

    public static function normalizeFiles(array $files): array
    {
        if (!isset($files['name']) || !is_array($files['name'])) return [];
        $result = [];
        foreach ($files['name'] as $index => $name) {
            $result[] = [
                'name' => $name,
                'type' => $files['type'][$index] ?? '',
                'tmp_name' => $files['tmp_name'][$index] ?? '',
                'error' => $files['error'][$index] ?? UPLOAD_ERR_NO_FILE,
                'size' => $files['size'][$index] ?? 0,
            ];
        }
        return $result;
    }

    public static function displayValue(string $table, string $field, mixed $value): string
    {
        if ($value === null || $value === '') return '—';
        $config = self::field($table, $field);
        if ($config['image']) return (string)$value;
        if ($config['type'] === 'select') {
            foreach ($config['options'] as $option) {
                if ((string)$option['value'] === (string)$value) return (string)$option['label'];
            }
        }
        if ($config['rich']) return trim(strip_tags((string)$value));
        if ($config['json']) return mb_strimwidth((string)$value, 0, 60, '…');
        if ($config['type'] === 'datetime-local' && strtotime((string)$value)) return date('d M Y, h:i A', strtotime((string)$value));
        if ($config['type'] === 'date' && strtotime((string)$value)) return date('d M Y', strtotime((string)$value));
        if ($config['type'] === 'number' && self::isMoneyField($field)) return \money((float)$value);
        return (string)$value;
    }

    public static function isImageField(string $field): bool
    {
        return in_array($field, ['image','logo','banner','avatar','og_image','landing_banner','landing_image','media_path'], true)
            || preg_match('/(^|_)(image|logo|banner|avatar)$/', $field) === 1;
    }

    public static function isDocumentField(string $field): bool
    {
        return in_array($field, ['file_path','document_path','attachment','download_file'], true)
            || str_contains($field, 'document_file');
    }

    public static function relationOptions(string $table, string $field): array
    {
        $key = $table . ':' . $field;
        if (isset(self::$optionCache[$key])) return self::$optionCache[$key];
        $relation = self::relation($table, $field);
        if (!$relation || !Database::tableExists($relation['table'])) return self::$optionCache[$key] = [];

        $label = $relation['label'];
        $secondary = $relation['secondary'] ?? null;
        $columns = Database::columns($relation['table']);
        if (!in_array($label, $columns, true)) $label = in_array('name', $columns, true) ? 'name' : (in_array('title', $columns, true) ? 'title' : 'id');
        $select = "id,`{$label}` label";
        if ($secondary && in_array($secondary, $columns, true)) $select .= ",`{$secondary}` secondary";
        $where = $relation['where'] ?? '';
        if ($where !== '' && !self::safeWhere($where)) $where = '';
        $sql = "SELECT {$select} FROM `{$relation['table']}`" . ($where ? " WHERE {$where}" : '') . " ORDER BY `{$label}` LIMIT 1500";
        $rows = Database::safeFetchAll($sql);
        $options = [];
        foreach ($rows as $row) {
            $text = (string)($row['label'] ?? ('#' . $row['id']));
            if (!empty($row['secondary'])) $text .= ' — ' . $row['secondary'];
            $options[] = ['value' => (string)$row['id'], 'label' => $text];
        }
        return self::$optionCache[$key] = $options;
    }

    public static function selectOptions(string $table, string $field): array
    {
        $sets = [
            'status' => ['active','inactive','pending','approved','published','confirmed','processing','shipped','delivered','paid','completed','open','closed','held','synced','suspended','cancelled','refunded','failed','disabled','rejected','expired','deleted'],
            'role' => ['admin','staff','vendor','customer','affiliate','pos_manager','pos_cashier'],
            'product_type' => ['physical','digital'],
            'discount_type' => ['fixed','percentage'],
            'commission_type' => ['fixed','percentage'],
            'payment_method' => ['cash','cod','card','bkash','nagad','sslcommerz','wallet','bank','partial'],
            'payment_status' => ['pending','partial','paid','failed','refunded','cancelled'],
            'order_status' => ['pending','confirmed','processing','shipped','delivered','cancelled','returned','refunded'],
            'sale_status' => ['completed','held','cancelled','refunded'],
            'movement_type' => ['in','out','sale','refund','adjustment','opening','closing','cash_in','cash_out'],
            'transaction_type' => ['credit','debit','hold','release','refund','commission','royalty','withdrawal'],
            'wallet_type' => ['cash','commission','royalty','reward','vendor','customer'],
            'channel' => ['database','email','sms','push'],
            'method' => ['cash','bkash','nagad','bank','card','wallet','sslcommerz'],
            'rule_type' => ['fixed','percentage','nationwide','free_delivery','vendor','product','category','district','area'],
            'product_mode' => ['all','packaged','individual'],
            'statement_type' => ['weekly','monthly','custom'],
            'cycle_type' => ['weekly','monthly','quarterly','annual'],
            'width' => ['58mm','80mm','A4'],
            'printer_type' => ['browser','network','usb','bluetooth'],
            'device_type' => ['desktop','tablet','mobile','terminal'],
            'scan_type' => ['barcode','qr','manual'],
            'position' => ['home_top','home_middle','home_bottom','sidebar','category','product','footer'],
            'location' => ['header','footer','mobile','sidebar'],
            'applies_to' => ['all','product','category','service'],
            'preferred_solution' => ['refund','replacement','store_credit','repair'],
            'entry_type' => ['vat','tax','fee','credit','debit'],
            'disk' => ['public','private'],
        ];
        if ($field === 'status') {
            $specific = self::statusSet($table);
            $values = $specific ?: $sets['status'];
        } else {
            $values = $sets[$field] ?? [];
        }
        return array_map(static fn(string $value): array => ['value' => $value, 'label' => ucwords(str_replace('_', ' ', $value))], $values);
    }

    public static function statusSet(string $table): array
    {
        $map = [
            'users' => ['active','pending','suspended','disabled'],
            'vendors' => ['pending','approved','suspended','rejected','disabled'],
            'products' => ['pending','published','active','disabled','deleted'],
            'orders' => ['pending','confirmed','processing','shipped','delivered','cancelled','returned','refunded'],
            'payments' => ['pending','paid','failed','refunded','cancelled'],
            'withdraw_requests' => ['pending','approved','paid','rejected','cancelled'],
            'returns' => ['pending','approved','picked_up','received','completed','rejected','cancelled'],
            'refunds' => ['pending','approved','processing','completed','rejected','cancelled'],
            'pos_shifts' => ['open','closed'],
            'pos_held_sales' => ['held','resumed','completed','cancelled'],
            'pos_sync_queue' => ['pending','processing','synced','failed'],
            'support_tickets' => ['open','pending','answered','closed'],
            'vendor_verifications' => ['pending','approved','rejected'],
            'affiliate_member_plans' => ['active','expired','cancelled'],
            'vendor_subscriptions' => ['active','expired','cancelled','suspended'],
        ];
        return $map[$table] ?? [];
    }

    public static function fieldLabel(string $field): string
    {
        return ModuleCrud::fieldLabel($field);
    }

    private static function friendlyLabel(string $table, string $field): string
    {
        if ($field === 'parent_id') return 'Parent ' . rtrim(ucwords(str_replace('_', ' ', $table)), 's');
        $labels = [
            'user_id'=>'User','customer_id'=>'Customer','cashier_id'=>'Cashier','created_by'=>'Created by','uploaded_by'=>'Uploaded by',
            'approved_by'=>'Approved by','verified_by'=>'Verified by','closed_by'=>'Closed by','vendor_id'=>'Vendor','product_id'=>'Product',
            'related_product_id'=>'Related product','matched_product_id'=>'Matched product','wallet_id'=>'Wallet','category_id'=>'Category','brand_id'=>'Brand','order_id'=>'Order','coupon_id'=>'Coupon',
            'country_id'=>'Country','district_id'=>'District','area_id'=>'Area','courier_id'=>'Courier','page_category_id'=>'Page category',
            'flash_sale_id'=>'Flash sale','variant_id'=>'Product variant','plan_id'=>'Plan','affiliate_member_id'=>'Affiliate member',
            'sponsor_id'=>'Sponsor','ancestor_id'=>'Ancestor member','descendant_id'=>'Descendant member','royalty_cycle_id'=>'Royalty cycle',
            'psa_transaction_id'=>'PSA transaction','permission_id'=>'Permission','role_id'=>'Role','pos_role_id'=>'POS role',
            'outlet_id'=>'POS outlet','register_id'=>'POS register','shift_id'=>'POS shift','sale_id'=>'POS sale','package_id'=>'Package',
            'ticket_id'=>'Support ticket','referrer_user_id'=>'Referrer','referred_user_id'=>'Referred user','from_user_id'=>'From user',
            'to_user_id'=>'To user','og_image'=>'OG / social sharing image','meta_title'=>'Meta title','meta_description'=>'Meta description',
            'meta_keywords'=>'Meta keywords','config_json'=>'Configuration JSON','schema_json'=>'Schema JSON-LD','api_config'=>'API configuration',
        ];
        return $labels[$field] ?? ModuleCrud::fieldLabel($field);
    }

    private static function relation(string $table, string $field): ?array
    {
        if ($field === 'parent_id') {
            return ['table' => $table, 'label' => self::bestLabel($table)];
        }
        $map = [
            'user_id' => ['users','name','email'], 'customer_id' => ['users','name','email'], 'cashier_id' => ['users','name','email'],
            'created_by' => ['users','name','email'], 'uploaded_by' => ['users','name','email'], 'approved_by' => ['users','name','email'],
            'verified_by' => ['users','name','email'], 'closed_by' => ['users','name','email'], 'referred_user_id' => ['users','name','email'],
            'referrer_user_id' => ['users','name','email'], 'from_user_id' => ['users','name','email'], 'to_user_id' => ['users','name','email'], 'wallet_id' => ['wallets','wallet_type','balance'],
            'vendor_id' => ['vendors','shop_name','slug'], 'product_id' => ['products','name','sku'], 'related_product_id' => ['products','name','sku'], 'matched_product_id' => ['products','name','sku'],
            'category_id' => ['categories','name','slug'], 'brand_id' => ['brands','name','slug'], 'order_id' => ['orders','order_no','customer_name'],
            'coupon_id' => ['coupons','code',null], 'country_id' => ['countries','name','code'], 'district_id' => ['districts','name',null],
            'area_id' => ['areas','name',null], 'courier_id' => ['couriers','name','phone'], 'page_category_id' => ['page_categories','name','slug'],
            'flash_sale_id' => ['flash_sales','title',null], 'variant_id' => ['product_variants','variant_name','sku'], 'plan_id' => ['affiliate_plans','name','code'],
            'affiliate_member_id' => ['affiliate_members','referral_code','rank_code'], 'sponsor_id' => ['affiliate_members','referral_code','rank_code'],
            'ancestor_id' => ['affiliate_members','referral_code','rank_code'], 'descendant_id' => ['affiliate_members','referral_code','rank_code'],
            'royalty_cycle_id' => ['royalty_cycles','period_start','cycle_type'], 'psa_transaction_id' => ['psa_transactions','id',null],
            'permission_id' => ['permissions','description','action'], 'role_id' => ['roles','name','code'], 'pos_role_id' => ['pos_roles','name','code'],
            'outlet_id' => ['pos_outlets','name','code'], 'register_id' => ['pos_registers','name','register_code'], 'shift_id' => ['pos_shifts','id','status'],
            'sale_id' => ['pos_sales','sale_no','sale_status'], 'package_id' => ['vendor_packages','name',null], 'ticket_id' => ['support_tickets','subject','status'],
            'pickup_point_id' => ['pickup_points','name','contact_phone'], 'delivery_zone_id' => ['delivery_zones','name','status'],
        ];
        if (!isset($map[$field])) return null;
        [$relatedTable, $label, $secondary] = $map[$field];
        $where = '';
        if ($relatedTable === 'products') $where = "status NOT IN ('deleted','disabled')";
        if ($relatedTable === 'users') $where = "status NOT IN ('deleted','disabled')";
        return ['table' => $relatedTable, 'label' => $label, 'secondary' => $secondary, 'where' => $where];
    }

    private static function isRelation(string $table, string $field): bool
    {
        return self::relation($table, $field) !== null;
    }

    private static function override(string $table, string $field, array $config): array
    {
        $overrides = [
            'settings.setting_value' => ['type' => 'textarea', 'col' => 12],
            'email_templates.body' => ['rich' => true, 'type' => 'textarea', 'col' => 12],
            'sms_templates.body' => ['type' => 'textarea', 'col' => 12],
            'pages.content' => ['rich' => true, 'type' => 'textarea', 'col' => 12],
            'banners.image' => ['help' => 'Upload the banner artwork. Recommended dimensions depend on the selected position.'],
            'sliders.image' => ['help' => 'Recommended homepage slider size: 1600 × 600 pixels.'],
            'brands.logo' => ['help' => 'Upload a square or transparent brand logo.'],
            'categories.banner' => ['help' => 'Displayed on the category landing page and social previews.'],
            'social_links.icon' => ['help' => 'Bootstrap Icons class, for example bi-facebook.'],
            'payment_gateways.config_json' => ['help' => 'Valid JSON only. Keep live secrets in app/config.local.php where supported.'],
            'product_variants.options_json' => ['help' => 'Example: {"Color":"Black","Storage":"128GB"}'],
            'homepage_sections.content_json' => ['help' => 'Valid JSON used to render the section.'],
            'schema_markups.schema_json' => ['help' => 'Valid schema.org JSON-LD object.'],
            'seo_meta.schema_json' => ['help' => 'Valid schema.org JSON-LD object.'],
            'affiliate_members.social_json' => ['help' => 'Example: {"facebook":"https://...","whatsapp":"8801..."}'],
            'vendors.social_json' => ['help' => 'Example: {"facebook":"https://...","whatsapp":"8801..."}'],
        ];
        return array_replace($config, $overrides[$table . '.' . $field] ?? []);
    }

    private static function columnDetails(string $table): array
    {
        if (isset(self::$columnDetails[$table])) return self::$columnDetails[$table];
        if (!Database::tableExists($table)) return self::$columnDetails[$table] = [];
        $rows = Database::safeFetchAll("SHOW COLUMNS FROM `{$table}`");
        $result = [];
        foreach ($rows as $row) $result[(string)($row['Field'] ?? '')] = $row;
        return self::$columnDetails[$table] = $result;
    }

    private static function storeUpload(string $table, string $field, array $file, bool $image): string
    {
        $error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($error !== UPLOAD_ERR_OK || !is_uploaded_file((string)($file['tmp_name'] ?? ''))) {
            throw new RuntimeException(ModuleCrud::fieldLabel($field) . ' could not be uploaded.');
        }
        $size = (int)($file['size'] ?? 0);
        if ($size < 1 || $size > MAX_UPLOAD_BYTES) {
            throw new RuntimeException(ModuleCrud::fieldLabel($field) . ' exceeds the upload size limit.');
        }
        $tmp = (string)$file['tmp_name'];
        $mime = (new \finfo(FILEINFO_MIME_TYPE))->file($tmp) ?: '';
        $imageTypes = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
        $documentTypes = $imageTypes + ['application/pdf' => 'pdf', 'application/zip' => 'zip', 'text/plain' => 'txt', 'text/csv' => 'csv', 'application/vnd.ms-excel' => 'csv'];
        $types = $image ? $imageTypes : $documentTypes;
        if (!isset($types[$mime])) throw new RuntimeException('Unsupported file type for ' . ModuleCrud::fieldLabel($field) . '.');

        if (isset($imageTypes[$mime])) {
            $info = @getimagesize($tmp);
            if (!$info || ($info['mime'] ?? '') !== $mime) throw new RuntimeException('The uploaded image is invalid.');
            if ((int)$info[0] * (int)$info[1] > MAX_IMAGE_PIXELS) throw new RuntimeException('The uploaded image dimensions are too large.');
            $relativeDirectory = 'public/uploads/forms/' . preg_replace('/[^a-z0-9_-]/i', '-', $table) . '/' . date('Y/m');
            $directory = ROOT_PATH . '/' . $relativeDirectory;
            $disk = 'public';
        } else {
            if ($mime === 'application/pdf' && (string)file_get_contents($tmp, false, null, 0, 5) !== '%PDF-') {
                throw new RuntimeException('The uploaded PDF is invalid.');
            }
            if ($mime === 'application/zip') {
                $zip = new \ZipArchive();
                if ($zip->open($tmp) !== true) throw new RuntimeException('The uploaded ZIP archive is invalid.');
                $zip->close();
            }
            $relativeDirectory = 'storage/private_uploads/forms/' . preg_replace('/[^a-z0-9_-]/i', '-', $table) . '/' . date('Y/m');
            $directory = ROOT_PATH . '/' . $relativeDirectory;
            $disk = 'private';
        }
        if (!is_dir($directory) && !mkdir($directory, $disk === 'public' ? 0755 : 0750, true) && !is_dir($directory)) {
            throw new RuntimeException('The upload directory is not writable.');
        }
        $filename = preg_replace('/[^a-z0-9_-]/i', '-', $field) . '-' . bin2hex(random_bytes(16)) . '.' . $types[$mime];
        $absolute = $directory . '/' . $filename;
        if (!move_uploaded_file($tmp, $absolute)) throw new RuntimeException('The uploaded file could not be saved.');
        @chmod($absolute, $disk === 'public' ? 0644 : 0640);

        $storedPath = $relativeDirectory . '/' . $filename;
        $mediaPath = $disk === 'public' ? $storedPath : str_replace('storage/private_uploads/', '', $storedPath);
        Database::safeExecute(
            'INSERT INTO media_files (disk,file_name,file_path,mime_type,file_size,uploaded_by,created_at) VALUES (?,?,?,?,?,?,NOW())',
            [$disk, Security::safeFilename((string)($file['name'] ?? $filename)), $mediaPath, $mime, $size, Auth::id()]
        );
        if ($disk === 'private') return 'private:' . Database::lastInsertId();
        return $storedPath;
    }

    private static function removeOwnedUpload(string $path): void
    {
        $path = trim($path);
        if ($path === '' || str_starts_with($path, 'private:')) return;
        $normalized = ltrim(str_replace('\\', '/', $path), '/');
        if (!str_starts_with($normalized, 'public/uploads/forms/')) return;
        $root = realpath(ROOT_PATH . '/public/uploads/forms');
        $absolute = ROOT_PATH . '/' . $normalized;
        $parent = realpath(dirname($absolute));
        if ($root && $parent && str_starts_with($parent, $root) && is_file($absolute)) @unlink($absolute);
    }

    private static function generateCode(string $table, string $field, array $input): string
    {
        $source = (string)($input['name'] ?? $input['title'] ?? $input['shop_name'] ?? $table);
        $prefix = strtoupper(substr(preg_replace('/[^A-Z0-9]/', '', strtoupper($table)), 0, 4));
        if ($field === 'barcode') {
            $base = '2';
            for ($i = 0; $i < 11; $i++) $base .= (string)random_int(0, 9);
            $sum = 0;
            for ($i = 0; $i < 12; $i++) $sum += ((int)$base[$i]) * ($i % 2 === 0 ? 1 : 3);
            return $base . ((10 - ($sum % 10)) % 10);
        }
        if ($field === 'sku') {
            $namePrefix = strtoupper(substr(preg_replace('/[^A-Z0-9]/', '', strtoupper($source)), 0, 8));
            return ($namePrefix ?: 'ITEM') . '-' . strtoupper(bin2hex(random_bytes(2)));
        }
        if ($field === 'referral_code') return 'REF-' . strtoupper(bin2hex(random_bytes(4)));
        return ($prefix ?: 'ITEM') . '-' . date('ymd') . '-' . strtoupper(bin2hex(random_bytes(3)));
    }

    private static function keywords(array $input): string
    {
        $text = implode(' ', array_filter([
            $input['name'] ?? '', $input['title'] ?? '', $input['shop_name'] ?? '',
            $input['meta_title'] ?? '', $input['short_description'] ?? '',
        ], static fn($value): bool => is_scalar($value)));
        $words = [];
        foreach (preg_split('/[^\pL\pN]+/u', mb_strtolower(strip_tags($text))) ?: [] as $word) {
            if (mb_strlen($word) > 2) $words[$word] = true;
        }
        foreach (['bangladesh','online','smart tech omp'] as $word) $words[$word] = true;
        return implode(', ', array_slice(array_keys($words), 0, 20));
    }

    private static function section(string $field): string
    {
        if (self::isImageField($field) || self::isDocumentField($field) || str_contains($field, 'video')) return 'Media & Files';
        if (str_starts_with($field, 'meta_') || in_array($field, ['slug','og_image','schema_json'], true)) return 'SEO & Sharing';
        if (self::isRelation('', $field) || str_ends_with($field, '_id')) return 'Relationships';
        if (self::isMoneyField($field) || preg_match('/(price|amount|balance|fee|charge|percent|stock|qty|limit|rate|value|points|rating|weight)/', $field)) return 'Values & Rules';
        if (self::isRichText($field) || preg_match('/(description|content|body|message|note|address|policies|details|bio|html|json)/', $field)) return 'Content';
        if (preg_match('/(status|enabled|active|default|show_|allow_|notify_|sort_order)/', $field)) return 'Status & Display';
        return 'Identity';
    }

    private static function isBoolean(string $field, string $type): bool
    {
        return str_starts_with($type, 'tinyint(1)') || preg_match('/^(is_|has_|show_|allow_|notify_)/', $field) === 1 || str_ends_with($field, '_enabled') || in_array($field, ['royalty_eligible','sandbox_mode','two_factor_enabled','monthly_royalty_required','auto_reactivate_on_royalty'], true);
    }

    private static function isNumeric(string $type, string $field): bool
    {
        return preg_match('/^(tinyint|smallint|mediumint|int|bigint|decimal|float|double)/', $type) === 1
            || preg_match('/(price|amount|balance|fee|charge|percent|stock|qty|limit|rate|value|points|rating|weight|sort_order)$/', $field) === 1;
    }

    private static function isMoneyField(string $field): bool
    {
        return preg_match('/(price|amount|balance|fee|charge|sales|income|bonus|royalty|withdraw|subtotal|total|cash|profit|commission|vat|tax)/', $field) === 1;
    }

    private static function allowsNegative(string $field): bool
    {
        return in_array($field, ['variance','quantity','qty','points'], true);
    }

    private static function isLongText(string $field, string $type): bool
    {
        return str_contains($type, 'text') || preg_match('/(description|content|body|message|note|address|policies|details|bio|response|payload)/', $field) === 1;
    }

    private static function isRichText(string $field): bool
    {
        return in_array($field, ['description','short_description','content','body','landing_bio','policies','header_html','footer_html'], true);
    }

    private static function isJsonField(string $field): bool
    {
        return str_ends_with($field, '_json') || in_array($field, ['payload','raw_response','api_config','config_json','response_json','cart_json','social_json','schema_json','options_json','specifications','content_json'], true);
    }

    private static function help(string $table, string $field): string
    {
        $map = [
            'slug' => 'URL-friendly value. It updates automatically from the main name or title until manually changed.',
            'meta_title' => 'Browser and search-result title. Defaults to the record name/title.',
            'meta_description' => 'Concise search-result summary; keep it around 150–160 characters.',
            'meta_keywords' => 'Comma-separated keyword suggestions generated from the record name and content.',
            'og_image' => 'Social-sharing preview used by Facebook, WhatsApp, Messenger and LinkedIn. Recommended: 1200 × 630 pixels.',
            'sort_order' => 'Smaller numbers appear first.',
            'status' => 'Controls availability and visibility of this record.',
            'config_json' => 'Enter valid JSON. Invalid JSON will not be saved.',
            'schema_json' => 'Enter valid schema.org JSON-LD data.',
        ];
        return $map[$field] ?? '';
    }

    private static function placeholder(string $field): string
    {
        if ($field === 'meta_keywords') return 'keyword one, keyword two, bangladesh';
        if (str_contains($field, 'url') || $field === 'link') return 'https://example.com/...';
        if (self::isJsonField($field)) return '{}';
        return '';
    }

    private static function varcharLength(string $type): int
    {
        return preg_match('/(?:var)?char\((\d+)\)/i', $type, $match) ? (int)$match[1] : 0;
    }

    private static function bestLabel(string $table): string
    {
        $columns = Database::columns($table);
        foreach (['name','title','shop_name','code','slug'] as $candidate) if (in_array($candidate, $columns, true)) return $candidate;
        return 'id';
    }

    private static function safeWhere(string $where): bool
    {
        return preg_match('/^[a-zA-Z0-9_\s\'",().<>=!-]+$/', $where) === 1 && !str_contains($where, ';');
    }

    private static function isSafeUrl(string $url): bool
    {
        if ($url === '') return true;
        if (str_starts_with($url, '/') || str_starts_with($url, '#')) return true;
        return (bool)filter_var($url, FILTER_VALIDATE_URL) && preg_match('#^https?://#i', $url) === 1;
    }
}
